Runtime 从NullSafe源码看消息转发 机制

开篇

马上就要年底了再码一波,自己总结一下Runtime,打算总结一下Runtime的各种用法,结合一些常见的源码来分析一下,有错误在所难免,希望尽量少一点。。。

NullSafe与消息转发

在处理后台返回的数据时会碰到返回的空的情况,大家有自己的处理方式来增加代码的稳健性,这里就借常见的NullSafe的源码来举例。
NullSafe源码内容并不算很多,主要的实现代码如下

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    //look up method signature
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature)
    {
        //check implementation cache first
        NSString *selectorString = NSStringFromSelector(selector);
        signature = signatureCache[selectorString];
        if (!signature)
        {
            @synchronized([NSNull class])
            {
                //check again, in case it was resolved while we were waitimg
                signature = signatureCache[selectorString];
                if (!signature)
                {
                    //创建一个缓存 获取到所有的类名
                    //not supported by NSNull, search other classes
                    if (signatureCache == nil)
                    {
                        if ([NSThread isMainThread])
                        {
                            cacheSignatures();
                        }
                        else
                        {
                            dispatch_sync(dispatch_get_main_queue(), ^{
                                cacheSignatures();
                            });
                        }
                    }
                    //遍历缓存,寻找是否已经有可以执行此方法的类
                    //find implementation
                    for (Class someClass in classList)
                    {
                        if ([someClass instancesRespondToSelector:selector])
                        {
                            //其次如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;
                            signature = [someClass instanceMethodSignatureForSelector:selector];
                            break;
                        }
                    }

                    //cache for next time
                    signatureCache[selectorString] = signature ?: [NSNull null];
                }
                else if ([signature isKindOfClass:[NSNull class]])
                {
                    signature = nil;
                }
            }
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
    invocation.target = nil;
    [invocation invoke];
}

主要方法
int objc_getClassList(Class *buffer, int bufferLen) //获取class列表
获取到,项目中类的所有类名。

static void cacheSignatures()
{
    classList = [[NSMutableSet alloc] init];
    signatureCache = [[NSMutableDictionary alloc] init];

    //get class list
    int numClasses = objc_getClassList(NULL, 0);
    Class *classes = (Class *)malloc(sizeof(Class) * (unsigned long)numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    //add to list for checking
    for (int i = 0; i < numClasses; i++)
    {
        //determine if class has a superclass
        Class someClass = classes[i];
        Class superclass = class_getSuperclass(someClass);
        while (superclass)
        {
            if (superclass == [NSObject class])
            {
                [classList addObject:someClass];
                [classList removeObject:[someClass superclass]];
                break;
            }
            superclass = class_getSuperclass(superclass);
        }
    }

    //free class list
    free(classes);
}
NullSafe的实现原理

把发送给NSNull的而NSNull又无法处理的消息经过如下几步处理:

  • 创建一个方法缓存,这个缓存会缓存项目中类的所有类名( cacheSignatures()方法),并且对缓存查找,看是否有可以执行的类方法。
  • 其次如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数。(想要走 forwardInvocation方法, 必须 先实现 methodSignatureForSelector 而且 返回 NSMethodSignature 必须不能为空。)
  • 如果没有的话,返回nil,接下来会走forwardInvocation:方法。
  • [invocation invokeWithTarget:nil];将消息转发给nil。

下面我们看一下Runtime的消息转发原理以及实现。

Runtime中的消息转发

原理我就不一个个敲了。如下:
消息的转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。第二阶段涉及“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接受者看看有没有其他对象处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一起如常。若没有“备援的接收者”,则启动完整的消息转发机制,运行期系统会把于消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。(深入理解Objective-C消息转发机制)

举个栗子

我们新建一个项目,定义一个goHome方法,但是不去实现,直接调用,可以见到如下错误
goHome

常见的错误

unrecognized selector sent to instance

因为我们的方法并没有实现,去调用,在消息的发送过程中并没有找到接受的对象去处理这个消息,导致了项目抛错。在Runtime中我们可以通过其他的方式去进行项目Crash的补救。

消息转发的三种处理方式
  • 动态方法解析 接受者1
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)name 

在调用方法时,对象在收到无法解读的消息后,首先将调用其所属类的上述类方法,然后在方法中判断方法名,利用class_addMethod动态添加新的方法名,IMP的功能是实现的新方法 必须有两个参数。这时我们再运行项目,调用 [self goHome];虽然没有实现,但是会走我们动态添加的方法
newGoHOme并打印数据。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}

//1 防止crash
void newGoHOme(id self, SEL _cmd){
    // implementation
    NSLog(@"这里执行goHOme的打印");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
    if (name == @selector(goHome)) {
        class_addMethod([self class], name, (IMP)newGoHOme, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
    NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
    return [super resolveClassMethod:name];
}
/****
 class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
 const char * _Nullable types)
 IMP的功能是实现的新方法  必须有两个参数
 ****/
  • 备援接收者2
- (id)forwardingTargetForSelector:(SEL)aSelector

如果接受者1并未执行,则消息传递到被接受者2处理。我们新建一个CompanyVC,设置并实现goHome方法,


CompanyVC.png
- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}
//执行其他已实现的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return [[CompanyVC alloc]init];
}

我们在ViewController中引入CompanyVC头文件,然后在forwardingTargetForSelector返回CompanyVC,然后运行项目,我们可以看到虽然我们仍然没有实现ViewController中的goHome方法,但是没有crash并且打印出了CompanyVC中方法的数据。
其实他实现的原理就是接收到转发信息时看当前是否能找到援助对象,如果有则将其返回,若找不到就返回nil。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回。这里找到了CompanyVC,所以不会崩溃。

  • 备援接收者3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
- (void)forwardInvocation:(NSInvocation *)anInvocation

如果消息转发到这一步基本就是一个完整的消息转发了。
完整的消息转发过程的图片,如下(手绘,可能有点丑😆)


消息转发机制.png

如上图由1开始事件的调取,然后由2转发至3再转发至4进入如下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    [self goHome];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 为方法生成方法签名,这个签名就是给下面的anInvocation调用的   invocation中有一个方法就是通过sig生成的
    NSString *sel = NSStringFromSelector(aSelector);
    if ([sel isEqualToString:@"goHome"]) {
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // 方法签名
        return sig;
    }
    return [super methodSignatureForSelector:aSelector];

}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    CompanyVC *newHome = [CompanyVC new];
    if ([newHome respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:newHome];
    }
}

查找goHome方法,然后生成签名,给anInvocation去调用,一样执行了CompanyVC中的方法,同时避免了程序的Crash。

消息转发机制总结

如转发机制图,在由1到5的过程中,不管是2,还是3对接受到的消息进行了处理,就结束了一次消息的转发不会再往下进行消息的转发处理,只有2和3都没有找到相关的对象可以处理这个方法,转发消息给4 5处理,进行一次完整的消息转发过程。
如果所有的都无法找到处理这个方法的对象执行下一个方法

- (void)doesNotRecognizeSelector:(SEL)aSelector

之后调用

- (void)forwardInvocation:(NSInvocation *)anInvocation

到这里一样结束一次消息的转发过程。
到这里我们再去看之前的NullSafe就会清楚这个分类的实现,在接收到Null无法处理时利用转发机制对消息进行了处理,防止了项目Crash。

参考资料及拓展阅读

http://www.cocoachina.com/ios/20160830/17424.html
http://blog.csdn.net/mangosnow/article/details/36183535
http://blog.csdn.net/hello_hwc/article/details/49687543
http://blog.csdn.net/app_ios/article/details/52411076
http://www.jianshu.com/p/151edae1d6ee

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

推荐阅读更多精彩内容