【Runtime】

作者: 大基本功 | 来源:发表于2018-06-13 11:41 被阅读10次

    Runtime简介

    Runtime是什么呢?比较官方的解释是

    Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一

    那Runtime的作用又是什么呢?

    OC是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。

    Runtime中的基本概念

    对象
    typedef struct objc_object {
        Class isa;
    } *id;
    

    文档描述

    A pointer to an instance of a class.
    一个指向实例对象的指针
    When you create an instance of a particular class, the allocated memory contains an objc_object data structure, which is directly followed by the data for the instance variables of the class.
    当你创建一个一个特定类的实例时,将会被分配一块包含objc_object的数据结构;这个数据结构是这个类的变量
    The alloc and alloc<wbr>With<wbr>Zone: methods of the Foundation framework class NSObject use the function class_createInstance to create objc_object data structures.

    id(任意对象)是一个 objc_object 结构类型的指针, 其第一个成员是一个 objc_class 结构类型的指针。

    Objective-C类是由Class类型来表示的,Class 是一个 objc_class 结构类型的指针;

    typedef struct objc_class *Class; 
    

    查看objc/runtime.h中objc_class结构体的定义如下:

    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    

    struct objc_class结构体定义了很多变量,通过命名不难发现,
    结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量链表、方法链表、缓存、遵守的协议链表等。简单说明下作用

    • isa:是一个 objc_class 类型的指针,看到这里,想起我前面的引申解读了没?内存布局以一个
    • objc_class 指 针为开始的所有东东都可以当做一个 object 来对待! 这就是说 objc_class 或者说类其实也可以当做一个
    • name:一个 C 字符串,指示类的名称。我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));
    • version:类的版本信息,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获 取(class_getVersion)。
    • info:供运行期使用的一些位标识。有如下一些位掩码:
      CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
      CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
      CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置; CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在 ObjC 2.0 中被废弃了); CLS_MAPPED (0x10L) 为 ObjC 运行期所使用
      CLS_FLUSH_CACHE (0x20L) 为 ObjC 运行期所使用
      CLS_GROW_CACHE (0x40L) 为 ObjC 运行期所使用
      CLS_NEED_BIND (0x80L) 为 ObjC 运行期所使用
      CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是 一个包含 objc_method_list 指针的数组;
    • instance_size:该类的实例变量大小(包括从父类继承下来的实例变量);
    • ivars:指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为NULL;
    • methodLists:与 info 的一些标志位有关,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指 向单个 objc_method_list 还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法;
    • cache:指向 objc_cache 的指针,用来缓存最近使用的方法,以 高效率;
    • protocols:指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。

    一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),
    该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

    类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
    就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),
    元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:

    如图所示
    每一个实例包含一个isa对象
    isa指向类,类是一个objc_class结构体,包含实例的方法列表,参数列表,category等,除此之外,objc_class中还有一个super_class,指向其类的父类,isa指针,这里的isa指针指向元类,即metaClass,元类存储类方法等信息
    元类里也包含isa指针,元类里的isa指针指向 根元类,根元类的isa指针指向自己
    obj_msgSend发送实例消息的时候,先找到实例,然后通过实例的isa指针找到类的方法列表及参数列表等,如果找到,返回,如果没有找到,则通过super_class在其父类中重复此过程
    obj_msgSend发送类消息的时候,通过类的isa,找到元类,然后流程与步骤4相同

    Method(objc_method)

    先看下定义

    runtime.h
    /// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
    typedef struct objc_method *Method;
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    

    Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:

    - (void)logName
    {
        NSLog(@"name");
    }
    

    这段代码,就是一个函数。
    我们来看下objc_method这个结构体的内容:

    SEL method_name 方法名
    char *method_types 方法类型
    IMP method_imp 方法实现
    在这个结构体重,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。

    SEL(objc_selector)

    先看下定义

    Objc.h
    /// An opaque type that represents a method selector.代表一个方法的不透明类型
    typedef struct objc_selector *SEL;
    

    objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

    @property SEL selector;
    可以看到selector是SEL的一个实例。

    A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
    其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。

    selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

    同一个类,selector不能重复
    不同的类,selector可以重复
    这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。

    SEL的作用
    int main(int argc, const char * argv[]) {
        
        // 1.SEL类型的第一个作用, 配合对象/类来检查对象/类中有没有实现某一个方法
        /*
        SEL sel = @selector(setAge:);
        Person *p = [Person new];
        // 判断p对象中有没有实现-号开头的setAge:方法
        // 如果P对象实现了setAge:方法那么就会返回YES
        // 如果P对象没有实现setAge:方法那么就会返回NO
        BOOL flag = [p respondsToSelector:sel];
        NSLog(@"flag = %i", flag);
        
        // respondsToSelector注意点: 如果是通过一个对象来调用该方法那么会判断该对象有没有实现-号开头的方法
        // 如果是通过类来调用该方法, 那么会判断该类有没有实现+号开头的方法
        SEL sel1 = @selector(test);
        flag = [p respondsToSelector:sel1];
        NSLog(@"flag = %i", flag);
        
        flag = [Person respondsToSelector:sel1];
        NSLog(@"flag = %i", flag);
         */
        
        
        // 2.SEL类型的第二个作用, 配合对象/类来调用某一个SEL方法
        /*
        SEL sel = @selector(demo);
        Person *p = [Person new];
        // 调用p对象中sel类型对应的方法
        [p performSelector:sel];
        
        SEL sel1 = @selector(signalWithNumber:);
        // withObject: 需要传递的参数
        // 注意: 如果通过performSelector调用有参数的方法, 那么参数必须是对象类型,
        // 也就是说方法的形参必须接受的是一个对象, 因为withObject只能传递一个对象
        [p performSelector:sel1 withObject:@"13838383438"];
        
        SEL sel2 = @selector(setAge:);
        [p performSelector:sel2 withObject:@(5)];
        NSLog(@"age = %i", p.age);
        
        // 注意:performSelector最多只能传递2个参数
        SEL sel3 = @selector(sendMessageWithNumber:andContent:);
        [p performSelector:sel3 withObject:@"138383438" withObject:@"abcdefg"];
         */
        
        // 3.配合对象将SEL类型作为方法的形参
        Car *c = [Car new];
        SEL sel = @selector(run);
        
        Person *p = [Person new];
        [p makeObject:c andSel:sel];
        return 0;
    }
    
    @interface Car : NSObject
    - (void)run;
    @end
    
    @interface Person : NSObject
    
    @property int age;
    
    + (void)test;
    - (void)demo;
    
    - (void)signalWithNumber:(NSString *)number;
    
    - (void)sendMessageWithNumber:(NSString *)number andContent:(NSString *)content;
    
    // 调用传入对象的指定方法
    - (void)makeObject:(id)obj andSel:(SEL)sel;
    @end
    
    #import "Person.h"
    
    @implementation Person
    
    + (void)test
    {
        NSLog(@"test");
    }
    
    - (void)demo
    {
        NSLog(@"demo");
    }
    
    - (void)signalWithNumber:(NSString *)number
    {
        NSLog(@"number = %@", number);
    }
    
    - (void)sendMessageWithNumber:(NSString *)number andContent:(NSString *)content
    {
        NSLog(@"number = %@, content = %@", number, content);
    }
    
    - (void)makeObject:(id)obj andSel:(SEL)sel
    {
        [obj performSelector:sel];
    }
    @end
    
    
    IMP

    看下IMP的定义

    /// A pointer to the function of a method implementation.  指向一个方法实现的指针
    typedef id (*IMP)(id, SEL, ...); 
    #endif
    

    就是指向最终实现程序的内存地址的指针。
    在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

    类缓存(objc_cache)

    当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。

    为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

    Category(objc_category)

    Category是表示一个指向分类的结构体的指针,其定义如下:

    struct category_t { 
        const char *name; 
        classref_t cls; 
        struct method_list_t *instanceMethods; 
        struct method_list_t *classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
    };
    

    name:是指 class_name 而不是 category_name。
    cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
    instanceMethods:category中所有给类添加的实例方法的列表。
    classMethods:category中所有添加的类方法的列表。
    protocols:category实现的所有协议的列表。
    instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
    从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

    Runtime的消息传递

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

    1️⃣首先,通过obj的isa指针找到它的 class ;
    2️⃣在 class 的 method list 找 foo ;
    3️⃣如果 class 中没到 foo,继续往它的 superclass 中找 ;
    4️⃣一旦找到 foo 这个函数,就去执行它的实现IMP 。

    但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_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找到它的类。
    2️⃣在它的类中查找method_list,是否有selector方法。
    3️⃣没有则查找父类的method_list。
    4️⃣找到对应的method,执行它的IMP。
    5️⃣转发IMP的return值。

    Runtime的消息转发

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

    • 动态方法解析
    • 备用接收者
    • 完整消息转发
    动态方法解析

    首先,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。

    备用接收者

    如果目标对象实现了-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去执行了。打印结果也证明我们成功实现了转发。

    完整消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
    首先它会发送-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
    
    

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

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

    Runtime的应用

    1️⃣关联对象(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. */
    };
    

    下面实现一个UIView的Category添加自定义属性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
    

    打印结果:
    2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1
    打印结果来看,我们成功在分类上添加了一个属性,实现了它的setter和getter方法。
    通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。

    我们看看内存测量对于的属性修饰。

    内存策略 属性修饰 描述
    OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一个关联对象的弱引用。
    OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) @property (nonatomic, strong) 指定一个关联对象的强引用,不能被原子化使用。
    OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一个关联对象的copy引用,不能被原子化使用。
    OBJC_ASSOCIATION_RETAIN @property (atomic, stron) 指定一个关联对象的强引用,能被原子化使用。
    OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一个关联对象的copy引用,能被原子化使用。

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

    方法添加

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

    //class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    
    
    • cls 被添加方法的类
    • name 添加的方法的名称的SEL
    • imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
    • 类型编码
    方法替换

    下面实现一个替换ViewControllerviewDidLoad方法的例子。

    @implementation ViewController
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            SEL originalSelector = @selector(viewDidLoad);
            SEL swizzledSelector = @selector(jkviewDidLoad);
    
            Method originalMethod = class_getInstanceMethod(class,originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
    
            //judge the method named  swizzledMethod is already existed.
            BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            // if swizzledMethod is already existed.
            if (didAddMethod) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }
            else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    - (void)jkviewDidLoad {
        NSLog(@"替换的方法");
    
        [self jkviewDidLoad];
    }
    
    - (void)viewDidLoad {
        NSLog(@"自带的方法");
    
        [super viewDidLoad];
    }
    
    @end
    
    

    swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

    swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

    实现图解如下图。

    从图中可以看出,我们通过swizzling特性,将selectorC的方法实现IMPc与selectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。

    KVO实现

    全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。
    KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

    Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

    NSKVONotifying_A 类剖析
    NSLog(@"self->isa:%@",self->isa);  
    NSLog(@"self class:%@",[self class]);  
    

    在建立KVO监听前,打印结果为:

    self->isa:A
    self class:A
    

    在建立KVO监听之后,打印结果为:

    self->isa:NSKVONotifying_A
    self class:A
    

    在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被KVO 机制修改为指向系统新创建的子类NSKVONotifying_A 类,来实现当前类属性值改变的监听;
    所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。

    子类setter方法剖析
    KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangeValueForKey: ,在存取数值的前后分别调用 2 个方法:
    被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;
    当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

    KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

    - (void)setName:(NSString *)newName { 
          [self willChangeValueForKey:@"name"];    //KVO 在调用存取方法之前总调用 
          [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
          [self didChangeValueForKey:@"name"];     //KVO 在调用存取方法之后总调用
    }
    

    消息转发(热更新)解决Bug(JSPatch)

    JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。

    关于消息转发,前面已经讲到过了,消息转发分为三级,我们可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。JSPatch不仅能够实现消息转发,还可以实现方法添加、替换能一系列功能。

    实现NSCoding的自动归档和自动解档

    原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
    核心方法:在Model的基类中重写方法:

    - (id)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            unsigned int outCount;
            Ivar * ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                Ivar ivar = ivars[I];
                NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
            }
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[I];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
    }
    
    

    实现字典和模型的自动转换(MJExtension)

    原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
    核心方法:在NSObject的分类中添加方法

    - (instancetype)initWithDict:(NSDictionary *)dict {
    
        if (self = [self init]) {
            //(1)获取类的属性及属性对应的类型
            NSMutableArray * keys = [NSMutableArray array];
            NSMutableArray * attributes = [NSMutableArray array];
            /*
             * 例子
             * name = value3 attribute = T@"NSString",C,N,V_value3
             * name = value4 attribute = T^i,N,V_value4
             */
            unsigned int outCount;
            objc_property_t * properties = class_copyPropertyList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                objc_property_t property = properties[I];
                //通过property_getName函数获得属性的名字
                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
                [keys addObject:propertyName];
                //通过property_getAttributes函数可以获得属性的名字和@encode编码
                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
                [attributes addObject:propertyAttribute];
            }
            //立即释放properties指向的内存
            free(properties);
    
            //(2)根据类型给属性赋值
            for (NSString * key in keys) {
                if ([dict valueForKey:key] == nil) continue;
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
        return self;
    
    }
    
    

    参考:
    Runtime详解
    Runtime通篇详解

    相关文章

      网友评论

        本文标题:【Runtime】

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