前言:
咱們在開發過程當中,可能遇到服務端返回數據中有null
的狀況,當取到null
值,而且對null發送消息的時候,就可能出現,unrecognized selector sent to instance
,應用crash的狀況。
針對這種狀況,在每次取值的時候去作判斷處理又不大合適,之前筆者在GitHub上發現了一個神奇的文件NullSafe:github.com/nicklockwoo…。把這個文件拖到項目中,即便出現null
的狀況,也不會報出unrecognized selector sent to instance
的問題。
筆者近期分析了一下NullSafe文件,而且經過作了一個Demo:QiSafeType,筆者將經過介紹消息轉發流程
的方式,揭開NullSafe神祕的面紗。html
QiMessage
的實例qiMessage
沒有實現的length
方法,演示消息轉發過程。QiSafeType消息轉發效果說明:git
qiMessage
消息轉發的整個過程主要涉及的3個方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
- (id)forwardingTargetForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
+ (BOOL)resolveInstanceMethod:(SEL)sel
的時候,會有相應的方法緩存操做,這個操做是系統幫咱們作的。首先貼一張消息轉發的圖,筆者聊到的內容會圍繞着這張圖展開。github
下邊筆者依次分析消息轉發的過程緩存
下文仍是以
qiMessage
調用length
方法爲例,分析消息轉發的過程。bash
qiMessage
在調用length
方法後,會先進行動態方法解析,調用+ (BOOL)resolveInstanceMethod:(SEL)sel
,咱們能夠在這裏動態添加方法,並且若是在這裏動態添加方法成功後,系統會把動態添加的length
方法進行緩存,當qiMessage
再次調用length
方法的時候,將不會調用+ (BOOL)resolveInstanceMethod:(SEL)sel
。會直接調用動態添加成功的length
方法。尋找備援接收者
的過程- (id)forwardingTargetForSelector:(SEL)aSelector
,這個過程用於尋找一個接收者,能夠響應未知的方法aSelector
。完整的消息轉發流程:首先建立NSInvocation對象,把與還沒有處理的那條消息有關的所有細節都封於其中,此對象包含選擇子、目標(target)及參數。在出發NSInvocation對象時,「消息派發系統」(message-dispatch system)將親自出馬,把消息指派給目標對象。(摘抄自Effective Objective-C 2.0編寫高質量iOS與OS X的52個有效方法)微信
QiMessage
中的代碼對消息轉發流程進一步分析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@:"分別表明:app
」q「:返回值long long ;
」@「:調用方法的的實例爲對象類型
「:」:表示方法
複製代碼
若有其它須要,看下圖應該會更直觀一些
ide
(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。
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:
細心的讀者可能會發如今首次消息轉發的時候流程並非
+[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);
}
複製代碼
筆者還沒有搞清楚緣由。讀者有知道的敬請指教。
筆者結合NullSafe:github.com/nicklockwoo…仿寫了一個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問題用的是消息轉發的第三部分,走的是完整地消息轉發流程。
不過咱們開發過程當中,若是能夠的話,仍是儘量早地處理消息轉發這部分,好比在動態方法解析的時候,動態添加方法(畢竟這一步系統能夠爲咱們作方法的緩存處理)。 或者是在尋找備援接收對象的時候,返回可以響應未實現的方法的對象。
注意:相關的使用場景在測試的時候不要用,測試的時候儘量仍是要暴露出問題的。 而且使用的時候,最好結合着異常日誌上報。
小編微信:可加並拉入《QiShare技術交流羣》。
關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)
推薦文章:
iOS 自定義拖拽式控件:QiDragView
iOS 自定義卡片式控件:QiCardView
iOS Wireshark抓包
iOS Charles抓包
初探TCP
IP、UDP初探
奇舞週刊