iOS 消息转发

前言
我们在开发过程中,可能遇到服务端返回数据中有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
      1. 其中在+ (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个有效方法)

  • 3. 结合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问题用的是消息转发的第三部分,走的是完整地消息转发流程。

不过我们开发过程中,如果可以的话,还是尽可能早地处理消息转发这部分,比如在动态方法解析的时候,动态添加方法(毕竟这一步系统可以为我们做方法的缓存处理)。
或者是在寻找备援接收对象的时候,返回能够响应未实现的方法的对象。

注意:相关的使用场景在测试的时候不要用,测试的时候尽可能还是要暴露出问题的。
并且使用的时候,最好结合着异常日志上报。

参考学习资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342