前言:
咱们在开发过程当中,可能遇到服务端返回数据中有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初探
奇舞周刊