iOS底层之消息动态决议

前言

之前写到方法查找,当调用一个方法是,会先到类的cache里进行快速查找,找到了就直接执行对应的imp。如果没有找到,会到类的bits里去进行慢速查找,找到了就会到cache里进行插入。如果这一步也没有查找到,就会将imp赋值为forward_imp。那么赋值完了以后,接下来又会怎么操作呢,一起去探索一下。

resolveInstanceMethod

_objc_msgForward_impcache

先来看看这个forward是什么。

const IMP forward_imp = (IMP)_objc_msgForward_impcache;
复制代码

它就是_objc_msgForward_impcache,全局搜索找一找这个在哪里实现的。

image.png

在汇编代码里找到它的实现,里面就执行了__objc_msgForward,而__objc_msgForward的大概意思就是执行x17x17又是来自__objc_forward_handler,接着找下去。但是会发现通过__objc_forward_handler找不到,可能是这个方法不是汇编写的,所以把前面的下划线去掉,再次全局搜索。

image.png

可以看到_objc_forward_handler就是objc_defaultForwardStretHandler,而其中就只有一句,是不是很熟悉,没错这就是我们经常看到的找不到方法的错误信息打印。
但是通过lookUpImpOrForward()代码可以看到,其实还没有直接执行到这一步来,而是进行了一个判断。

image.png

先整理一下,如果没有找到imp,且cls的父类为nil的时候,就会给imp赋值为forward_imp,并且break出循环,然后就会走到这一句。画个图看看behavior是多少。

behavior.png

这一步主要是对behavior进行处理,后面还会再对其进行使用,也是一个比较关键的变量。下面来看resolveMethod_locked()

image.png

这里主要是做了一个判断,传进来的cls是否是元类,然后再根据情况调用resolveInstanceMethod()或者resolveClassMethod(),最后再调用lookUpImpOrForwardTryCache()并返回结果。
这些操作也叫做消息动态决议,这也是苹果在遇到没有实现的方法时给的一次机会。

resolveInstanceMethod()

image.png

这是它的实现,下面折叠部分是一些日志打印,不重要。
可以看到这里就对进行了一个消息发送,发送的方法是resolveInstanceMethod:,可以看到这个待发送的和当前方法名称相同,但是它们不是一个,因为待发送的方法有一个冒号。
然后尝试查找resolveInstanceMethod:有没有实现,这个判断基本是不会为真的,因为系统(也就是NSObject)有实现resolveInstanceMethod:,不过只返回了一个NO,还是需要我们自己去处理。
接下来对resolve_sel进行消息发送,并返回是否有处理,然后再对sel进行lookUpImpOrNilTryCache()查找,并得到查找的imp,下面就是根据imp是否存在做出一些日志打印。
先来看看lookUpImpOrNilTryCache()做了什么。

image.png

image.png

它就是调用了_lookUpImpTryCache(),传入的参加基本都没有变化,只有behavior做了处理,之前得到behavior = 1,现在传入behavior | LOOKUP_NIL,算出来传入的behavior = 5
_lookUpImpTryCache()中,先判断cls是否初始化,没有的话直接走lookUpImpOrForward()慢速查找(按照正常顺序走过来的话,cls基本都是初始化了的)。
然后再进行快速查找,快速查找有了就直接goto done,如果没找到再去共享缓存中查找,共享缓存没有再进行慢速查找,并且直接返回慢速查找的结果,这里behavior = 5了(此时再进入慢速查找时判断behavior & LOOKUP_RESOLVER就是5 & 2 = 0,就不会再进入resolveMethod_locked(),不然就会无限递归下去)。
done的地方判断behavior & LOOKUP_NIL(5 & 4 = 4),并且imp == (IMP)_objc_msgForward_impcache(也就是之前看过的没有找到就会给imp赋值一个错误打印日志的sel),最后再返回nil。

Tips: 可以看到在resolveInstanceMethod()里已经调用了一次_lookUpImpTryCache(),为什么还会在resolveMethod_locked()的结尾再调用一次呢。
其实主要是因为可能存在多线程的问题,如果有其他线程先缓存好了,那么就要确保能最快的获取到,也是一个为了提升速度的操作。

接下来看看要怎么实现resolveInstanceMethod:才不会报错。

实例方法动态决议例子

#import "DDAnimal.h"
#import <objc/message.h>

@interface DDAnimal : NSObject

- (void)run;

@end


@implementation DDAnimal

- (void)sayRun{
    NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(run)) {
        NSLog(@"resolveInstanceMethod: %@ - %@",self,NSStringFromSelector(sel));
        IMP sayRunImp = class_getMethodImplementation(self, @selector(sayRun));
        Method method = class_getInstanceMethod(self, @selector(sayRun));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayRunImp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

@end
复制代码

上面这个例子可以看到run只有声明,没有实现,如果直接调用run肯定是会报错的。

image.png

现在加上+ (BOOL)resolveInstanceMethod:(SEL)sel,在这个方法里先判断sel是否为run,然后拿到事先写好的sayRun实现,并且把这个实现添加到sel上,这样sel就有了实现,再次调用就不会报错了。

image.png

resolveClassMethod()

image.png

如果类是元类,那么就会调用resolveClassMethod(),也就是当前sel是一个类方法。
这里为什么执行了一次resolveClassMethod(),还要再执行一次resolveInstanceMethod()呢?
主要是因为类方法不仅会在类中以类方法的形式存在,还会在元类里以实例方法的形式存在。所以如果在类方法里找不到,就会到元类里去找实例方法的处理。这些都可以在isa和继承的走势图里看到。
接下来看看它的代码实现。

image.png

可以看到它和resolveInstanceMethod()基本一样,只是需要对元类进行一个处理,看看它是否实现。

类方法动态决议例子

#import "DDAnimal.h"
#import <objc/message.h>

@interface DDAnimal : NSObject

+ (void)jump;

@end


@implementation DDAnimal

+ (void)sayJump {
    NSLog(@"%@ - %s", self, __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));

    if (sel == @selector(jump)) {
        id meta = objc_getMetaClass("DDAnimal");
        IMP sayJumpImp     = class_getMethodImplementation(meta, @selector(sayJump));
        Method method    = class_getInstanceMethod(meta, @selector(sayJump));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(meta, sel, sayJumpImp, type);
    }

    return [super resolveClassMethod:sel];
}

@end
复制代码

注意一点: 因为是类方法,所以要去元类获取。

总结

当我们调用一个方法之后,系统底层实际是进行了消息发送,然后会快速查找cache里去找,如果没找到就会慢速查找methodList里去找,如果还没有找到,就会进行消息动态决议,看看是否实现了resolveClassMethod:或者resolveInstanceMethod:,如果还是没有就会进行快速消息转发慢速消息转发(不过目前还没有讲到消息转发),最后都没有找到就会报错unrecognized selector sent to class的错误,直接崩溃,整个流程结束。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享