美文网首页
runtime 探究

runtime 探究

作者: code_xu | 来源:发表于2018-10-23 17:26 被阅读0次

    Runtime消息传递

    一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的

    • 首先,通过obj的isa指针找到它的 class ;
    • 在 class 的 method listfoo ;
    • 如果 class 中没到 foo,继续往它的superclass 中找 ;
    • 一旦找到 foo 这个函数,就去执行它的实现IMP

    但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo之后,把foomethod_name 作为keymethod_imp作为value 给存起来。当再次收到foo消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。

    objec_msgSend的方法定义如下:

    OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    

    那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体

    //对象
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    //类
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;
    //方法列表
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    //方法
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    
    
    

    1.系统首先找到消息的接收对象,然后通过对象的isa找到它的类

    1. 在它的类中查找method_list,是否有selector方法。
      3.没有则查找父类的method_list
      4.找到对应的method,执行它的IMP
      5.转发IMP的return值。

    Runtime消息转发

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

    runtime流程图.png

    动态方法解析

    首先,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函数
    }
    
    
    

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

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

    备用接收者

    如果目标对象实现了 -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
    
    
    

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

    完整消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
    首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -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
    

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

    以上就是Runtime的三次转发流程。下面我们讲讲Runtime的实际应用。

    Runtime应用

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

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

    关联对象(Objective-C Associated Objects)给分类增加属性

    我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。

    关联对象Runtime提供了下面几个接口

    //关联对象
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    //获取关联的对象
    id objc_getAssociatedObject(id object, const void *key)
    //移除关联的对象
    void objc_removeAssociatedObjects(id object)
    
    
    

    参考解释

    id object:被关联的对象
    const void *key:关联的key,要求唯一
    id value:关联的对象
    objc_AssociationPolicy policy:内存管理的策略
    

    内存管理的策略

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    

    下面实现一个UIViewCategory自定义属性defaultColor

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface UIView (DefaultColor)
    
    @property (nonatomic, strong) UIColor *defaultColor;
    
    @end
    
    @implementation UIView (DefaultColor)
    
    @dynamic defaultColor;
    
    static char kDefaultColorKey;
    
    - (void)setDefaultColor:(UIColor *)defaultColor {
        objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)defaultColor {
        return objc_getAssociatedObject(self, &kDefaultColorKey);
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        UIView *test = [UIView new];
        test.defaultColor = [UIColor blackColor];
        NSLog(@"%@", test.defaultColor);
    }
    
    @end
    

    通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放

    方法魔法(Method Swizzling)方法添加和替换和KVO实现

    方法添加

    实际上添加方法刚才在讲消息转发的时候,动态方法解析的时候就提到了。

    相关文章

      网友评论

          本文标题:runtime 探究

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