美文网首页iOS 开发之 runtime
runtime方法查找流程及消息转发

runtime方法查找流程及消息转发

作者: 一个半吊子工程师 | 来源:发表于2020-09-15 15:35 被阅读0次

    方法查找

    方法查找的流程:缓存查找-->当前类查找-->父类逐级查找

    1.缓存 看看缓存中是否有对应的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)
    2.方法列表 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)
    3.父类方法列表 如果当前类对象方法列表没有,就会逐级 父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)
    4.如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递

    Runtime消息转发

    进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

    • 动态方法解析
    • 快速转发(找备用接收者去实现 forwardingTargetForSelector
    • 慢速消息转发
      • 1.方法签名 -methodSignatureForSelector:
      • 2.事务的转发-forwardInvacation:
    动态方法解析

    首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

    实现一个动态方法解析的例子如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函数
    }
    复制代码
    

    打印结果: 2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo

    可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。

    如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector

    动态方法决议方法 判断的并不是 返回 YES NO 而是 看当前类及整个继承链向上查找,有没有动态的添加上 sel, 如果整个继承链没有找到的话 那么会进行下面的 快速消息转发

    快速转发(备用接收者)

    如果目标对象实现了-forwardingTargetForSelector:Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

    实现一个备用接收者的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函数
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES;//返回YES,进入下一步转发
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(foo)) {
            return [Person new];//返回Person对象,让Person对象接收这个消息
        }
    
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    

    打印结果: 2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo

    可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证明我们成功实现了转发。

    • 以上实现 -forwardingTargetForSelector: 方法直接返回一个处理消息的对象,我们称为消息的快速转发

    forwardingTargetForSelector快速消息转发 指的是 将上面未处理的消息 ,重定向一个 消息接受者,并将未实现的消息 实现(需要一模模一样样)。返回值就是重定向消息者。(如返回了一个消息接受者,但是只声明了 并未实现 ,则同样会进入重定向的·方法决议-消息转发(快 慢)-崩溃) 如果返回nil 或者 返回的还是当前 接受者 直接进入下面方法 判断 及慢速转发流程

    慢速转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nilRuntime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。

    实现一个完整转发的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函数
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES;//返回YES,进入下一步转发
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil;//返回nil,进入下一步转发
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
        }
    
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
    
        Person *p = [Person new];
        if([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        }
        else {
            [self doesNotRecognizeSelector:sel];
        }
    
    }
    
    @end
    

    打印结果: 2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo

    从打印结果来看,我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档Type Encodings有详细的解释。

    以上就是Runtime的三次转发流程。

    三、总结

    当方法查找流程结束后仍没有找到 IMPruntime 首先进行 动态方法解析,之后再进入快速的消息转发,最后慢速消息转发。

    1. 动态方法解析:调用 + resolveInstanceMethod+ resolveClassMethod 尝试获取 IMP
    2. 没有 IMP,进入快速消息转发,调用 - forwardingTargetForSelector: 尝试获取一个可以处理的对象
    3. 仍没有处理,进入慢速转发,调用 - methodSignatureForSelector: 获取到方法签名后,将消息封装为一个invocation 再调用 - forwardInvocation: 进行处理。

    可见,当一个方法没有实现时,runtime 给了3次机会让我们进行处理。

    下面是动态方法解析和消息转发的流程:

    Runtime应用
    Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

    • 关联对象(Objective-C Associated Objects)给分类增加属性
    • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
    • 消息转发(热更新)解决Bug(JSPatch)
    • 实现NSCoding的自动归档和自动解档
    • 实现字典和模型的自动转换(MJExtension)
    相关

    1.[obj foo]和objc_msgSend()函数之间有什么关系?

    objc_msgSend()是[obj foo]的具体实现。
    在runtime中,objc_msgSend()是一个c函数,
    [obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。
    
    首先,通过obj的isa指针找到它的 class ;
    在 class 的 method list 找 foo ;
    如果 class 中没到 foo,继续往它的 superclass 中找 ;
    一旦找到 foo 这个函数,就去执行它的实现IMP 。
    

    2.runtime是如何通过selector找到对应的IMP地址的?

    缓存查找-->当前类查找-->父类逐级查找
    

    3.能否向编译后的类中增加实例变量?

    不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。
    但可以向动态添加的类中增加实例变量。
    

    参考链接:https://juejin.im/post/5ac0a6116fb9a028de44d717

    4.runtime 中,SEL和IMP的区别?

    每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,
    SEL是方法名(编号),IMP指向方法实现的首地址
    

    5.Method Swizzling
    换方法的几种实现方式(其实就是把方法实现进行调换):

    • 利用method_exchangeImplementations交换两个方法的实现
    • 利用class_replaceMethod替换方法的实现
    • 利用method_setImplementation来直接设置某个方法的IMP

    Method Swizzling 是一把双刃剑,使用不当也会给项目带来隐患,这里提一些注意事项:

    尽量在+load方法中(load可以保证被调用,其他方法都不靠谱),并且用 dispatch_once 来确保交换顺序和线程安全
    对于Hook的方法,需要调用原有实现,避免覆盖原有的方法,导致原有的功能缺失
    方法命名推荐使用前缀,避免与原方法名字冲突
    

    相关文章

      网友评论

        本文标题:runtime方法查找流程及消息转发

        本文链接:https://www.haomeiwen.com/subject/kurjlktx.html