iOS 消息轉發

級別: ★★☆☆☆
標簽:「iOS」「消息轉發」「null([NSNull null])」
作者: WYW
審校: QiShare團隊


前言:
我們在開發過程中,可能遇到服務端返回數據中有null([NSNull null])的情況,當取到null值,并且對null發送消息的時候,就可能出現,unrecognized selector sent to instance,應用crash的情況。
針對這種情況,在每次取值的時候去做判斷處理又不大合適,以前筆者在GitHub上發現了一個神奇的文件NullSafehttps://github.com/nicklockwood/NullSafe。把這個文件拖到項目中,即使出現null的情況,也不會報出unrecognized selector sent to instance的問題。
筆者近期分析了一下NullSafe文件,并且通過做了一個Demo:QiSafeType,筆者將通過介紹消息轉發流程的方式,揭開NullSafe神秘的面紗。

Demo(QiSafeType)消息轉發部分解讀

  • 筆者將通過演示調用 QiMessage的實例qiMessage 沒有實現的length方法,演示消息轉發過程。
  • QiSafeType消息轉發效果如下:


    QiMessageForwardGif.gif

QiSafeType消息轉發效果說明:

  1. qiMessage消息轉發的整個過程主要涉及的3個方法:
    • + (BOOL)resolveInstanceMethod:(SEL)sel
    • - (id)forwardingTargetForSelector:(SEL)aSelector
    • - (void)forwardInvocation:(NSInvocation *)anInvocation
  2. 其中在+ (BOOL)resolveInstanceMethod:(SEL)sel的時候,會有相應的方法緩存操作,這個操作是系統幫我們做的。

QiSafeType消息轉發部分解析

  1. 首先貼一張消息轉發的圖,筆者聊到的內容會圍繞著這張圖展開。

    消息轉發
  2. 下邊筆者依次分析消息轉發的過程

下文還是以qiMessage調用length方法為例,分析消息轉發的過程。

  • (1)首先qiMessage在調用length方法后,會先進行動態方法解析,調用+ (BOOL)resolveInstanceMethod:(SEL)sel,我們可以在這里動態添加方法,而且如果在這里動態添加方法成功后,系統會把動態添加的length方法進行緩存,當qiMessage再次調用length方法的時候,將不會調用+ (BOOL)resolveInstanceMethod:(SEL)sel。會直接調用動態添加成功的length方法。
  • (2)如果動態方法解析部分我們沒有做操作,或者動態添加方法失敗了的話,會進行尋找備援接收者的過程- (id)forwardingTargetForSelector:(SEL)aSelector,這個過程用于尋找一個接收者,可以響應未知的方法aSelector。
  • (3)如果尋找備援接收者的過程中返回值為nil的話,那么會進入到完整的消息轉發流程中。

完整的消息轉發流程:首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封于其中,此對象包含選擇子、目標(target)及參數。在觸發NSInvocation對象時,“消息派發系統”(message-dispatch system)將親自出馬,把消息指派給目標對象。(摘抄自Effective Objective-C 2.0編寫高質量iOS與OS X的52個有效方法)

  1. 結合QiMessage中的代碼對消息轉發流程進一步分析
  • (1)先看第一部分qiMessage在調用length方法后,會先進行動態方法解析,調用+ (BOOL)resolveInstanceMethod:(SEL)sel,如果我們在這里為qiMessage動態添加方法。那么也能處理消息。
    相關代碼如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    printf("%s:%s \n", __func__ ,NSStringFromSelector(sel).UTF8String);
    
    if (sel == @selector(length)) {
        BOOL addSuc = class_addMethod([self class], sel, (IMP)(length), "q@:");
        if (addSuc) {
            return addSuc;
        }
    }
    return [super resolveInstanceMethod:sel];
}

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
參數types傳入的"q@:"分別代表:

”q“:返回值long long ;
”@“:調用方法的的實例為對象類型
“:”:表示方法
  • 如有其它需要,看下圖應該會更直觀一些
    type Encodings
  • (2)qiMessage在調用length方法后,動態方法解析部分如果返回值為NO的時候,會尋找備援接收者,調用- (id)forwardingTargetForSelector:(SEL)aSelector,如果我們在這里為返回可以處理length的接收者。那么也能處理消息。

相關代碼如下:

static NSArray *respondClasses;

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    printf("%s:%s \n", __func__ ,NSStringFromSelector(aSelector).UTF8String);

    id forwardTarget = [super forwardingTargetForSelector:aSelector];
    if (forwardTarget) {
        return forwardTarget;
    }
    
    Class someClass = [self qiResponedClassForSelector:aSelector];
    if (someClass) {
        forwardTarget = [someClass new];
    }
    
    return forwardTarget;
}


- (Class)qiResponedClassForSelector:(SEL)selector {
    
    respondClasses = @[
                       [NSMutableArray class],
                       [NSMutableDictionary class],
                       [NSMutableString class],
                       [NSNumber class],
                       [NSDate class],
                       [NSData class]
                       ];
    for (Class someClass in respondClasses) {
        if ([someClass instancesRespondToSelector:selector]) {
            return someClass;
        }
    }
    return nil;
}


這里有一個不常用的API:+ (BOOL)instancesRespondToSelector:(SEL)aSelector;,這個API用于返回Class對應的實例能否相應aSelector。

  • (3)qiMessage在調用length方法后,動態方法解析部分如果返回值為NO的時候,尋找備援接收者的返回值為nil的時候,會進行完整的消息轉發流程。調用- (void)forwardInvocation:(NSInvocation *)anInvocation,這個過程會有一個插曲,- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector,只有我們在- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector中返回了相應地NSMethodSignature實例的時候,完整地消息轉發流程才能得以順利完成。

先聊下插曲- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector。

摘抄自文檔:This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.

加粗部分就是適用我們當前場景的部分。

這個方法也會用于消息轉發的時候,當NSInvocation對象必須創建的時候,如果我們的對象能夠處理沒有直接實現的方法,我們應該重寫這個方法,返回一個合適的方法簽名。

  • 相關代碼
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    printf("%s:%s \n\n\n\n", __func__ ,NSStringFromSelector(anInvocation.selector).UTF8String);
    
    anInvocation.target = nil;
    [anInvocation invoke];
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        Class responededClass = [self qiResponedClassForSelector:selector];
        if (responededClass) {
            @try {
                signature = [responededClass instanceMethodSignatureForSelector:selector];
            } @catch (NSException *exception) {
                
            }@finally {
                
            }
        }
    }
    return signature;
}

- (Class)qiResponedClassForSelector:(SEL)selector {
    
    respondClasses = @[
                       [NSMutableArray class],
                       [NSMutableDictionary class],
                       [NSMutableString class],
                       [NSNumber class],
                       [NSDate class],
                       [NSData class]
                       ];
    for (Class someClass in respondClasses) {
        if ([someClass instancesRespondToSelector:selector]) {
            return someClass;
        }
    }
    return nil;
}

這里有一個不常用的API:+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;,這個API通過Class及給定的aSelector返回一個包含實例方法標識描述的方法簽名實例。

> 此外對于NSInvocation的筆者發現一個很好玩的點。
仍然以`qiMessage`調用`length`方法為例。
- (void)forwardInvocation:(NSInvocation *)anInvocation中的 anInvocation的信息如下:

<NSInvocation: 0x6000025b8140>
return value: {Q} 0
target: {@} 0x60000322c360
selector: {:} length

> return value指返回值,“Q”表示返回值類型為long long類型;
> target 指的是消息的接收者,“@“標識對象類型;
> selector指的是方法,“:” 表示是方法,后邊的length為方法名。

更多內容可見下圖NSInvocation的types:

NSInvocation的types

尚存疑點

細心的讀者可能會發現在首次消息轉發的時候流程并不是

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
-[QiMessage forwardInvocation:]:length 

而是

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
+[QiMessage resolveInstanceMethod:]:length 
+[QiMessage resolveInstanceMethod:]:_forwardStackInvocation: 
-[QiMessage forwardInvocation:]:length 

這里的第三行+[QiMessage resolveInstanceMethod:]:length
第四行+[QiMessage resolveInstanceMethod:]:_forwardStackInvocation:
筆者查看了開源源碼:NSObject.mm
相關源碼如下:

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

筆者尚未搞清楚原因。讀者有知道的敬請指教。

QiSafeType之消息轉發相關代碼

  • QiSafeType消息轉發相關的代碼在QiMessage

NSNull+QiNullSafe.m

筆者結合NullSafehttps://github.com/nicklockwood/NullSafe仿寫了一個NSNull+QiNullSafe.m

  • NSNull+QiNullSafe.m能夠避免的問題有:
    NSNull *null = [NSNull null];
    [null performSelector:@selector(addObject:) withObject:@"QiShare"];
    [null performSelector:@selector(setValue:forKey:) withObject:@"QiShare"];
    [null performSelector:@selector(valueForKey:) withObject:@"QiShare"];
    [null performSelector:@selector(length) withObject:nil];
    [null performSelector:@selector(integerValue) withObject:nil];
    [null performSelector:@selector(timeIntervalSinceNow) withObject:nil];
    [null performSelector:@selector(bytes) withObject:nil];

NullSafe是怎么處理null問題

其實NullSafe處理null問題用的是消息轉發的第三部分,走的是完整地消息轉發流程。

不過我們開發過程中,如果可以的話,還是盡可能早地處理消息轉發這部分,比如在動態方法解析的時候,動態添加方法(畢竟這一步系統可以為我們做方法的緩存處理)。
或者是在尋找備援接收對象的時候,返回能夠響應未實現的方法的對象。

注意:相關的使用場景在測試的時候不要用,測試的時候盡可能還是要暴露出問題的。
并且使用的時候,最好結合著異常日志上報。

參考學習資料


推薦文章:
iOS 自定義拖拽式控件:QiDragView
iOS 自定義卡片式控件:QiCardView
iOS Wireshark抓包
iOS Charles抓包
初探TCP
IP、UDP初探

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言我們在開發過程中,可能遇到服務端返回數據中有null的情況,當取到null值,并且對null發送消息的時候,就...
    Lucky_Man閱讀 820評論 0 2
  • ios在類中,沒有定義的函數,要走消息轉發流程。如果不走消息轉發流程,程序會奔潰。消息轉發流程分四步調用。 第一步...
    zl520k閱讀 645評論 0 0
  • 消息轉發機制 假設說我們聲明一個類, 初始化對象, 并且在此類聲明一個方法, 調用方法的時候底層是怎么處理的呢? ...
    軟件iOS開發閱讀 271評論 0 0
  • ??最近看了『神奇的 BlocksKit』系列,里面說到動態代理是BlocksKit的精華部分,對于使用block...
    foreverSun_122閱讀 1,184評論 1 7
  • OC消息轉發 oc中的調用對象或者類不存在的方法,會執行一遍消息轉發流程.消息轉發主要包括4步 首先調用+ (BO...
    nullyy閱讀 419評論 0 0