美文网首页
iOS-Runtime从认识到深入

iOS-Runtime从认识到深入

作者: 树下敲代码的超人 | 来源:发表于2017-11-14 18:31 被阅读0次

    技 术 文 章 / 超 人


    Runtime(运行时机制)概念

    Object-C 是面向对象的语言,C是面向结构也就是面向过程的语言。Object-C是基于C基础上的语言。而Runtime为Object-C添加了面向对象的特性,将静态语言编译期和链接期做的事情放到了运行时来处理。所以Runtime是每一个IOS开发人员都应该了解的。Runtime是一套纯C的API。

    • Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。

    • Runtime库主要做下面几件事:
      封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
      找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。

    • 对于C语言而言函数的调用在编译时就会决定调用那一个函数。而OC在编译时并不能决定调用那一个函数,只有在运行时才能确定调用那一个函数,举个简单的例子:在日常开发时,在.h中声明了某个方法,而.m中却没有实现该方法,但编译项目却不会报错,因为OC编译时并没有确定.h中声明但方法具体调用.m的那一个函数,当然也就不知道该函数有没有实现了,而在运行时调用该函数时就会报错,因为运行时才会确定具体调用那一个函数,函数没有实现那么就会报错。OC是动态调用,正因为动态性,所以在运行时可以修改调用方法的指针。

    • Runtime其实有两个版本: “modern” 和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

    • Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。

    基础知识补充

    • Class:定义Objective-C类
    • Ivar:定义对象的实例变量,包括类型和名字。
    • Protocol:定义正式协议。
    • objc_property_t:定义属性。叫这个名字可能是为了防止和Objective-C 1.0中的用户类型冲突,那时候还没有属性。
    • Method:定义对象方法或类方法。这个类型提供了方法的名字(就是选择器)、参数数量和类型,以及返回值(这些信息合起来称为方法的签名),还有一个指向代码的函数指针(也就是方法的实现)。
    • SEL:定义选择器。选择器是方法名的唯一标识符。
    • IMP:定义方法实现。这只是一个指向某个函数的指针,该函数接受一个对象、一个选择器和一个可变长参数列表(varargs),返回一个对象
    • unsigned signed和unsigned不会改变类型长度,仅表示最高位是否为符号位,其中unsigned表示大于等于0的正数

    受限于内存分配的机制,一个NSObject对象会分配16byte的内存空间,但在64位系统下只使用了8byte,在32位系统下只使用了4byte.

    获取Obj-C指针所指向的内存的大小

    malloc_size((__brige const void *)obj);
    

    一个NSObject实例对象成员变量所占的大小是8byte。
    获取Object对象大小方法

     Class_getInstanceSize([NSObject class])
    //其本质是
    size_t class_getInstanceSize(Class cls)
    {
      if(!cls)return 0;
      return cls->alignedInstanceSize();
    }
    

    看到这里。你应该对Runtime有一个初步的了解了。建议先看看Runtime的方法,在看下面的内容。这样才会容易明白实例代码的用意。请查看Runtime方法说明


    使用Runtime
    1.导入头文件 #import <objc/message.h>
    2.OC 解决消息机制方法提示步骤[查找build setting-> 搜索msg->objc_msgSend(YES --> NO]

    Runtime最主要是消息机制,OC在编译时会转化为C,转化为: objc_msgSend(receiver, selector),何以证明?新建一个类 MyClass,其.m文件如下:

    #import "MyClass.h"
    @implementation MyClass
    -(instancetype)init{
        if (self = [super init]) {
            [self showUserName];
        }
        return self;
    }
    -(void)showUserName{
        NSLog(@"Dave Ping");
    }
    

    然后使用 clang 重写命令:

    $ clang -rewrite-objc MyClass.m
    

    然后在同一目录下会多出一个 MyClass.cpp 文件,双击打开,可以看到 init 方法已经被编译器转化为下面这样:

    static instancetype _I_MyClass_init(MyClass * self, SEL _cmd) {
        if (self = ((MyClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("init"))) {
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"));
        }
        return self;
    }
    

    我们要找的就是它:

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"))
    

    objc_msgSend 函数被定义在 objc/message.h 目录下,其函数原型是酱紫滴:

    OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    

    该函数有两个参数,一个 id 类型,一个 SEL 类型。

    SEL

    SEL 被定义在 objc/objc.h 目录下:

    typedef struct objc_selector *SEL;
    

    其实它就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。简单来说就是方法名称

    id

    与 SEL 一样,id 也被定义在 objc/objc.h 目录下:

    typedef struct objc_object *id;
    

    id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。objc_object 结构体定义如下:

    struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};
    

    我们通常所说的对象,就长这个样子,这个结构体只有一个成员变量 isa,对象可以通过 isa 指针找到其所属的类。isa 是一个 Class 类型的成员变量,那么 Class 又是什么呢?

    isa 等价于 isKindOf方法

    • 实例对象 isa 指向类对象
    • 类对象指 isa 向元类对象
    • 元类对象的 isa 指向元类的基类

    isa 有两种类型

    • 纯指针:指向内存地址
    • NON_POINTER_ISA:除了内存地址,还存有一些其他信息

    Class

    Class 也是一个结构体指针类型:

    typedef struct objc_class *Class;
    

    objc_class 结构体是酱紫滴:

    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;
    

    我们通常说的类就长这样子:

    • Class : Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针
    • super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
    • name:是类名。
    • version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可以让我们识别出不同类定义版本中实例变量布局的改变。
    • info:是类的详情。
    • instance_size:是该类的实例对象的大小。
    • ivars:指向该类的成员变量列表。
    • methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。
    • cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
    • protocols:指向该类的协议列表。

    objc_object与id

    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
     
    typedef struct objc_object *id;
    

    可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

    当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。

    另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

    ** objc_cache**
    上面提到了objc_class结构体中的cache字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:

    struct objc_cache {
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        Method buckets[1]                                        OBJC2_UNAVAILABLE;
    };
    
    • mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。

    • occupied:一个整数,指定实际占用的缓存bucket的总数。

    • buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长

    元类(Meta Class)

    例:

    NSArray *array = [NSArray array];
    
    • 这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念:

    • meta-class是一个类对象的类。

    • 当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

    • meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

    • 再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。

    IMP
    IMP就是Implementation的缩写,顾名思义,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。

    IMP的定义如下:

    if !OBJC_OLD_DISPATCH_PROTOTYPES
    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    else
    typedef id (*IMP)(id, SEL, ...); 
    endif
    

    实际上直接调用一个方法的IMP指针的效率是高于调用方法本身的,所以,如果你有一个合适的时机获取到方法的IMP的话,你可以试着调用它。

    objc_msgSend说明
    OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    

    首先,Runtime 系统会把方法调用转化为消息发送,即 objc_msgSend,并且把方法的调用者,和方法选择器,当做参数传递过去.

    此时,方法的调用者会通过 isa 指针来找到其所属的类,然后在 cache 或者 methodLists 中查找该方法,找得到就跳到对应的方法去执行。

    如果在类中没有找到该方法,则通过 super_class 往上一级超类查找(如果一直找到 NSObject 都没有找到该方法的话,这种情况,我们放到后面消息转发的时候再说)。

    前面我们说 methodLists 指向该类的实例方法列表,实例方法即-方法,那么类方法(+方法)存储在哪儿呢?类方法被存储在元类中,Class 通过 isa 指针即可找到其所属的元类。


    指针

    上图实线是 super_class 指针,虚线是 isa 指针。根元类的超类是NSObject,而 isa 指向了自己。NSObject 的超类为 nil,也就是它没有超类。

    前面我们使用 clang 重写命令,看到 Runtime 是如何将方法调用转化为消息发送的。我们也可以依样画葫芦,来学习使用一下 objc_msgSend。新建一个类 TestClass,添加如下方法:

    -(void)showAge{
        NSLog(@"24");
    }
    -(void)showName:(NSString *)aName{
        NSLog(@"name is %@",aName);
    }
    -(void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
        NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
    }
    -(float)getHeight{
        return 187.5f;
    }
    -(NSString *)getInfo{
        return @"Hi, my name is Dave Ping, I'm twenty-four years old in the year, I like apple, nice to meet you.";
    }
    

    我们可以像下面这样,使用 objc_msgSend 依次调用这些方法:

    Class *objct = NSClassFromStrin[@"TestClass"];
    ((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("showAge"));
    ((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("showName:"), @"Dave Ping");
    ((void (*) (id, SEL, float, float)) objc_msgSend) (objct, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
    float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
    NSLog(@"height is %.2f",f);
    NSString *info = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getInfo"));
    NSLog(@"%@",info);
    

    也许你已经注意到,objc_msgSend 在使用时都被强制转换了一下,这是因为 objc_msgSend 函数可以hold住各种不同的返回值以及多个参数,但默认情况下是没有参数和返回值的。如果我们把调用 showAge 方法改成这样:

    objc_msgSend(objct, sel_registerName("showAge"));
    

    Xcode 就会报错:

    Too many arguments to function call, expected 0, have 2.
    

    objc_msgSendSuper
    编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五个方法中选择一个来调用。如果消息是传递给超类,那么会调用 objc_msgSendSuper 方法,如果消息返回值是数据结构,就会调用 objc_msgSendSuper_stret 方法,如果返回值是浮点数,则调用 objc_msgSend_fpret 方法。

    这里我们重点说一下 objc_msgSendSuper,objc_msgSendSuper 函数原型如下:

    OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    

    当我们调用 [super selector] 时,Runtime 会调用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有两个参数,super 和 op,Runtime 会把 selector 方法选择器赋值给 op。而 super 是一个 objc_super 结构体指针,objc_super 结构体定义如下:

    struct objc_super {
        /// Specifies an instance of a class.
        __unsafe_unretained id receiver;
        /// Specifies the particular superclass of the instance to message.
    #if !defined(__cplusplus)  &&  !__OBJC2__
        /* For compatibility with old objc-runtime.h header */
        __unsafe_unretained Class class;
    #else
        __unsafe_unretained Class super_class;
    #endif
        /* super_class is the first class to search */
    };
    

    Runtime 会创建一个 objc_spuer 结构体变量,将其地址作为参数(super)传递给 objc_msgSendSuper,并且将 self 赋值给 receiver:super—>receiver=self.

    举个栗子,问下面的代码输出什么:

    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    

    答案是全部输出 Son。

    使用 clang 重写命令,发现上述代码被转化为:

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));
    

    当调用 [super class] 时,会转换成 objc_msgSendSuper 函数:

    • 第一步先构造 objc_super 结构体,结构体第一个成员就是 self。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)).

    第二步是去 Father 这个类里去找 - (Class)class,没有,然后去 NSObject 类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和 [self class] 调用相同了,所以两个输出结果都是 Son。

    Associated对象关联
    对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性:

    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
    
    • object 是源对象

    • value 是被关联的对象

    • key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象

    • policy 是一个枚举值,表示关联对象的行为,从命名就能看出各个枚举值的含义:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
       OBJC_ASSOCIATION_ASSIGN = 0,           /** 指定对关联对象的弱引用. */
       OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定对关联对象的强引.不是atomically */
       OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /** 指定复制的关联对象.*   不是 atomically. */
       OBJC_ASSOCIATION_RETAIN = 01401,       /** 指定对关联对象的强引用.*   是 atomically. */
       OBJC_ASSOCIATION_COPY = 01403          /** 指定复制的关联对象. *   是 atomically. */
    };
    

    要取出被关联的对象使用 objc_getAssociatedObject 方法即可,要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可:

    objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
    

    objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象.

    举个栗子,假如我们要给 UIButton 添加一个监听单击事件的 block 属性,新建 UIButton 的 Category,其.m文件如下:

    #import "UIButton+ClickBlock.h"
    #import static const void *associatedKey = "associatedKey";
    @implementation UIButton (ClickBlock)
    //Category中的属性,只会生成setter和getter方法,不会生成成员变量
    -(void)setClick:(clickBlock)click{
        /* 建立click的关联,暗号为associatedKey */
        objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
        /* 先移除监听,防止重复监听 */
        [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        /* 判断是否实现了click,没有实现就不用注册 */
        if (click) {
        /* 注册监听 */
            [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        }
    }
    -(clickBlock)click{
        /* 当使用click属性时,直接获取关联对象当click,相当于button.click使用时获取当对象就关联对象 */
        return objc_getAssociatedObject(self, associatedKey);
    }
    -(void)buttonClick{
        /* 收到监听消息后回调到click对象中 */
        if (self.click) {
            self.click();
        }
    }
    @end
    

    然后在代码中,就可以使用 UIButton 的属性来监听单击事件了:

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = self.view.bounds;
    [self.view addSubview:button];
    button.click = ^{
        NSLog(@"buttonClicked");
    };
    

    Runtime案例使用

    自动归档

    一般到归档写法时这样的:

    - (void)encodeWithCoder:(NSCoder *)aCoder{
        [aCoder encodeObject:self.name forKey:@"name"];
        [aCoder encodeObject:self.ID forKey:@"ID"];
    }
    - (id)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super init]) {
            self.ID = [aDecoder decodeObjectForKey:@"ID"];
            self.name = [aDecoder decodeObjectForKey:@"name"];
        }
        return self;
    }
    

    那么问题来了,如果当前 Model 有100个属性的话,就需要写100行这种代码:

    [aCoder encodeObject:self.name forKey:@"name"];
    

    想想都头疼,通过 Runtime 我们就可以轻松解决这个问题:

    1.使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.

    2.使用 ivar_getName 方法获取成员变量的名称.

    3.通过 KVC 来读取 Model 的属性值(encodeWithCoder:),以及给 Model 的属性赋值(initWithCoder:).

    举个栗子,新建一个 Model 类,其.m文件如下

    #import "TestModel.h"
    #import #import @implementation TestModel
    - (void)encodeWithCoder:(NSCoder *)aCoder{
       unsigned int outCount = 0;
       Ivar *vars = class_copyIvarList([self class], &outCount);
       for (int i = 0; i < outCount; i ++) {
           Ivar var = vars[i];
           const char *name = ivar_getName(var);
           NSString *key = [NSString stringWithUTF8String:name];
           // 注意kvc的特性是,如果能找到key这个属性的setter方法,则调用setter方法
           // 如果找不到setter方法,则查找成员变量key或者成员变量_key,并且为其赋值
           // 所以这里不需要再另外处理成员变量名称的“_”前缀
           id value = [self valueForKey:key];
           [aCoder encodeObject:value forKey:key];
       }
    }
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
       if (self = [super init]) {
           unsigned int outCount = 0;
           Ivar *vars = class_copyIvarList([self class], &outCount);
           for (int i = 0; i < outCount; i ++) {
               Ivar var = vars[i];
               const char *name = ivar_getName(var);
               NSString *key = [NSString stringWithUTF8String:name];
               id value = [aDecoder decodeObjectForKey:key];
               [self setValue:value forKey:key];
           }
       }
       return self;
    }
    @end
    

    Ivar 定义对象的实例变量,包括类型和名字
    是objc_ivar的指针,包含变量名,变量类型等成员。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针

    • Ivar定义:
    typedef objc_ivar * Ivar;
    
    • Ivar结构体:
    struct objc_ivar {
         char *ivar_name;
         char *ivar_type;
         int ivar_offset;
      #ifdef __LP64__
         int space;
      #endif
      }
    
    • Ivar的相关操作
    //获取Ivar的名称
      const char *ivar_getName(Ivar v);
      //获取Ivar的类型编码,
      const char *ivar_getTypeEncoding(Ivar v)
      //通过变量名称获取类中的实例成员变量
      Ivar class_getInstanceVariable(Class cls, const char *name)
      //通过变量名称获取类中的类成员变量
      Ivar class_getClassVariable(Class cls, const char *name)
      //获取指定类的Ivar列表及Ivar个数
      Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
      //获取实例对象中Ivar的值
      id object_getIvar(id obj, Ivar ivar) 
      //设置实例对象中Ivar的值
      void object_setIvar(id obj, Ivar ivar, id value)
    
    字典转模型

    一般的字典转模型是这样滴:

    -(instancetype)initWithDictionary:(NSDictionary *)dict{
        if (self = [super init]) {
            self.age = dict[@"age"];
            self.name = dict[@"name"];
        }
        return self;
    }
    

    可想而知,遇到的问题跟归档时候一样(后来使用MJExtension),这里我们稍微来学习一下其中原理,字典转模型的时候:

    1.根据字典的 key 生成 setter 方法

    2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)

    模型转字典的时候:

    1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性

    2.调用 property_getName 获取属性名称

    3.根据属性名称生成 getter 方法

    4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)

    代码如下:

    #import "NSObject+KeyValues.h"
    #import #import @implementation NSObject (KeyValues)
    
    //字典转模型
    +(id)objectWithKeyValues:(NSDictionary *)aDictionary{
        /* 创建model */
        id objc = [[self alloc] init];
        /* 遍历字典里所有的key */
        for (NSString *key in aDictionary.allKeys) {
            /* 取出key里的值 */
            id value = aDictionary[key];
            /*根据key在self中获取该key的属性指针property,判断当前属性是不是Model的*/
            objc_property_t property = class_getProperty(self, key.UTF8String);
            /* outCount存放你定义属性的个数 */
            unsigned int outCount = 0;
            /* 根据指针动态取出对象所有属性 */
            objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
            /* 取出对象首个属性 */
            objc_property_attribute_t attribute = attributeList[0];
            /* 取出属性值 */
            NSString *typeString = [NSString stringWithUTF8String:attribute.value];
            /* 判断取出的属性类型是否是当前这个model类 */
            if ([typeString isEqualToString:[NSString stringWithFormat:@"@\"%@\"",self.class]]) {
              /* 如果model中有一个属性就是也是model,即嵌套model。就遍历一次属性model  */
                value = [self objectWithKeyValues:value];
            }
            /**********************/
            //生成setter方法,并用objc_msgSend调用
            NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
            /* 获取方法名 */
            SEL setter = sel_registerName(methodName.UTF8String);
            /* 判断是否有该方法名 */
            if ([objc respondsToSelector:setter]) {
                /* 如果有就发送消息到该方法 */
                ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
            }
        }
        return objc;
    }
    //模型转字典
    -(NSDictionary *)keyValuesWithObject{
        /* outCount存放你定义属性的个数 */
        unsigned int outCount = 0;
        /* 取出模型中所有到属性指针,并获取属性个数 */
        objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
        /* 创建一个字典 */
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        /* 根据属性数量遍历 */
        for (int i = 0; i < outCount; i ++) {
            /* 取出每个属性指针 */
            objc_property_t property = propertyList[i];
            //生成getter方法,并用objc_msgSend调用
            const char *propertyName = property_getName(property);
            /* 获取方法名 */
            SEL getter = sel_registerName(propertyName);
            /* 判断是否存在该方法 */
            if ([self respondsToSelector:getter]) {
                /* 如果存在就从该方法获取值 */
                id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
                /*判断当前属性是不是Model*/
                if ([value isKindOfClass:[self class]] && value) {
                    /* 如果是就在遍历一次属性model */
                    value = [value keyValuesWithObject];
                }
                /**********************/
                /* 判断是否有值 */
                if (value) {
                    /* 获取属性名称到字符串 */
                    NSString *key = [NSString stringWithUTF8String:propertyName];
                  /* 存值 */
                    [dict setObject:value forKey:key];
                }
            }
        }
        return dict;
    }
    @end
    

    字典转模型中的知识点补充

    • objc_property_t :属性指针
      类型:
    typedef struct property_t *objc_property_t;
    

    @property 标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property 结构体的指针.可以通过 class_copyPropertyList 和 protocol_copyPropertyList 方法来获取类和协议中的属性

    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
    

    返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个 objc_property_t 指针,而这两个函数返回的值是指向这个数组的指针。
    可以用 property_getName 函数来查找属性名称:

    const char *property_getName(objc_property_t property)
    

    可以用class_getProperty 和 protocol_getProperty通过给出的名称来在类和协议中获取属性的引用:

    objc_property_t class_getProperty(Class cls, const char *name)
    objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
    

    可以用property_getAttributes函数来发掘属性的名称和@encode类型字符串:

    const char *property_getAttributes(objc_property_t property)
    

    对比下 class_copyIvarList 函数,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。

    • objc_property_attribute_t:属性指针对象的列表
      objc_property_attribute_t的value和name
    常用attribute name value
    nonatomic "N" ""
    strong/retain "&" ""
    weak "W" ""
    属性的类型type "T" "@TypeName", eg"@"NSString""
    属性对应的实例变量Ivar "V" "Ivar_name", eg "_name"
    readonly "R" ""
    getter "G" "GetterName", eg"isRight"
    setter "S" "SetterName", eg"setName"
    assign/atomic 默认即为assign和retain
    动态方法解析

    如果某个对象调用了不存在的方法时会怎么样,一般情况下程序会crash,错误信息类似下面这样:

    unrecognized selector sent to instance 0x7fd0a141afd0

    但是在程序crash之前,Runtime 会给我们动态方法解析的机会,消息发送的步骤大致如下:

    • 1.检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了

    • 2.检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉

    • 3.如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行,如果 cache 找不到就找一下方法分发表

    • 4.如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止
      如果还找不到就要开始进入消息转发了,消息转发的大致过程如图:


      消息转发
    • 1.进入 resolveInstanceMethod:方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕。
    • 2.resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector:方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步,返回某个对象,则会调用该对象的方法。
    • 3.若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector:来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步。
    • 4.当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等。如果到最后,消息还是没有得到响应,程序就会crash

    未完待续...

    相关文章

      网友评论

          本文标题:iOS-Runtime从认识到深入

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