OC的动态决议和消息转发

196 阅读4分钟

我们上篇文章探索了OC中的objc_msgSend在class中查找方法的流程,当沿继承链找到最顶层,即父类是nil的时候,便会进行消息转发。接下来我们继续沿着流程走下去,看看消息转发的过程是怎样的。我们从forword_imp说起: 截屏2022-05-10 下午9.44.52.png 我们找到它的赋值处: 截屏2022-05-10 下午9.46.07.png 接着我们找这个_objc_msgForward_impcache的实现: 截屏2022-05-10 下午9.52.06.png 接着我们找到_objc_forward_handler截屏2022-05-10 下午9.55.22.png unrecognized selector sent to instance xx 这段描述我们再熟悉不过了,当我们调用方法的时候,如果找不到方法的实现的时候就会报这个错误。

接下来我们返回lookUpImpOrForward方法,imp = forward_imp这句代码中为imp赋值了,但是还没调用,我们继续往下看,找到如下代码: 截屏2022-05-10 下午10.07.42.png 从注释可以得知,这个方法是没有找到实现的时候,会尝试一次方法解析器,我们接着找到resolveMethod_locked的实现。

方法的动态决议

截屏2022-05-10 下午10.10.02.png 截屏2022-05-12 下午11.16.35.png resolveInstanceMethodresolveClassMethod这两个方法,我们可以在runtime找不到方法的实现的时候去重写这两个方法(实例方法重写resolveInstanceMethod,类方法重写resolveClassMethod),来给未实现方法实现的方法一次修正的机会。我们来试试: 截屏2022-05-10 下午10.22.15.png 截屏2022-05-10 下午10.23.00.png 截屏2022-05-10 下午10.23.47.png 截屏2022-05-10 下午10.24.31.png 这里有个问题就是为什么会调用两次resolveInstanceMethod,后面会说到。我们重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,然后调用未实现的test方法: 截屏2022-05-12 下午11.21.55.png 类方法同理重写resolveClassMethod截屏2022-05-13 下午3.08.46.png 那我们不禁思考,通过方法的动态决议调用的方法会不会缓存。我们通过之前学到的知识进行探索可以知道答案是肯定的:

(lldb) p $6.imp(nil, Person.class)
(IMP) $7 = 0x0000000100003da0 (KCObjcBuild`-[Person occurError])
(lldb) p $6.sel()
(SEL) $8 = "test"
(lldb) 

得到的结果是impoccurErrorseltest。也就是进了缓存,而且方法的实现是动态决议添加的实现。

我们分别把实例方法以及类方法动态决议中的判断注释掉,类方法的动态决议方法中转发给一个没有实现的方法,会出现什么? iShot2022-05-13_15.13.39.png 当调用类方法classMethod后发现其竟然调用了instanceMethodforwordToMe方法,是怎么回事呢? 截屏2022-05-13 下午3.15.16.png 调用类方法后,没有找到方法的实现,则会进行方法动态决议,我们重写了resolveClassMethod方法,则会进入该方法,class_getMethodImplementation这个方法的底层也是从类对象或者元类对象中去查找方法,我们这里是类对象,也就是在Person类中查找这个不存在的方法,方法实现仍然没有找到,则会进行动态决议,进入resolveInstanceMethod这个方法,这个方法中我们去掉了限制,则会去找instanceMethodforwordToMe的实现,故而会调用该方法。如果我们通过去元类中查找方法,则会陷入死循环。 截屏2022-05-13 下午3.24.30.png

我们把Person类中的所有动态决议的内容注释掉,在NSObject的分类中加上动态决议的方法。从我们对objc_msgSend的探索中可以知道当我们所有的类出现找不到方法的时候,就会进入NSObject的分类中进行动态决议,因为方法查找时会向上查找,知道NSObject。这种方式有点AOP的特征,也就是面向切面编程,通过某一个方法将所有的某个特征的消息进行拦截,进行统一处理。基于此,我们想到的是,我们可以通过这个做一些埋点的操作,比如我们要在页面的生命周期中做一些自己的统计,我们可以在+load方法中对父类UIViewController系统的方法进行替换,这些操作需要在分类中进行,然后在自己的方法中做一些自己的操作。原理是当控制器中的生命周期方法调用super方法时,会进行向上调用,直到UIViewController的分类的方法中,代码如下: 截屏2022-05-13 下午4.33.55.png

消息的快速转发

当我们没有重写消息的动态决议的时候,系统还会给我们一次快速转发消息的机会,我们在-/+(id)forwardingTargetForSelector:(SEL)aSelector中提供转发的对象,我们可以提供一个统一的类来处理方法未找到的情况,然后在此方法中转发给该类。

- (id)forwardingTargetForSelector:(SEL)aSelector {
   
    if (aSelector == @selector(instanceMethod)) {
        return [DealError new];
    }
    return nil;
}

DealError类中处理一些常见的找不到方法的情况。

消息的慢速转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    DealError *dealError = [DealError new];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self];
    } else if ([dealError respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:dealError];
    } else {
        NSLog(@"弹框提示");
    }
}

所以最后整个消息传递和转发的流程如下:

绘图1.png