美文网首页iOS 优化
iOS类加载流程(二):类的静态初始化

iOS类加载流程(二):类的静态初始化

作者: 康小曹 | 来源:发表于2022-06-16 16:29 被阅读0次

    iOS类加载流程(一):类加载流程的触发
    中已经知道两个关键函数 map_images()load_images() 的触发逻辑了。但是现在直接看 map_images() 会一脸懵逼。

    这里直接看 realizeClassWithoutSwift 函数,我们只需要知道,这个函数是 objc 进行类的懒加载和非懒加载必须调用的方法,也是类初始化中的关键函数。理解了这个函数再去看主流程会比较清晰;

    1. 引子

    realizeClassWithoutSwift 是执行类的初始化的关键方法,代码也比较多,这里拆分来看。

    第一步是获取静态数据,关键代码如下:

    static Class realizeClassWithoutSwift(Class cls) {
        // 变量声明
        const class_ro_t *ro;
        class_rw_t *rw;
        Class supercls;
        Class metacls;
        bool isMeta;
    
        // 是否已经被map
        if (!cls) return nil;
        if (cls->isRealized()) return cls;
        assert(cls == remapClass(cls));
    
        // 静态数据初始化
        // 因为cls是从mach-O获取到的,所以此时data()方法获取到的是静态数据,所以类型是class_ro_t而不是class_rw_t
        ro = (const class_ro_t *)cls->data();
        if (ro->flags & RO_FUTURE) {
          // Future Class相关,暂时省略
        } else {
            // Normal class. Allocate writeable class data.
            rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
            rw->ro = ro;
            rw->flags = RW_REALIZED|RW_REALIZING;
            //rw因为8字节对齐,所以其后三位必定是空闲的
            // 后三位有对应的set和get方法来进行标志位的获取和设置
            cls->setData(rw);
        }
        // 补充标志位
        isMeta = ro->flags & RO_META;
        rw->version = isMeta ? 7 : 0;  // old runtime went up to 6
    }
    

    上述代码做了几件事:

    1. 变量声明和一些条件判断,没什么好说的;
    2. 为 rw 分配内存并且将 ro 赋值给 rw;
    3. 设置了一些标志位;

    这里需要提一下这段代码:

    // classref_t is unremapped class_t*
    typedef struct classref * classref_t;
    

    根据注释可知道 struct classref 表示没有被 map 的 class_t*。而 class_t* 就是运行时的类结构体。但是,怎么证明或者体现呢?这里可以搜一下 objc 中 classref_t 的使用,基本上会有这样一段代码:

    classref

    上图可以看到 classref 被强制转化成了 Class,其他几处使用到 classref_t 的地方也都是这样,总结下来两个共同点:

    1. classref_t 指向的都是从 mach-O 文件中的数据(类的静态数据);
    2. 都被强制转化成了 Class,也就是 class_t *

    由此,可以知道 classref_t 就是一个只有声明没有实际结构的结构体(有兴趣可以使用 C 代码坐下实践),类似于 OC 中只有 @Interface 却没有 @implementation 的类。

    继续看 realizeClassWithoutSwift 中的代码,上述代码关键在于第二步,筛选出关键代码也就几行:

    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    

    这里有几个疑问需要解开:

    1. ro 和 rw 是什么?
    2. setData() 做了什么?

    关于第一点,先直接说结论:

    1. ro 就是 class_ro_t 结构体,表示编译时期生成的类的静态结构,直接记录在 mach-O 文件类,被 __objc_classlist__objc_nlclslist 这两个 section 记录并映射。而 ro 的生成过程是在静态编译链接时期,可以通过 OC 转换后的 C/C++ 代码来看到表示数据的静态时期的结构体;
    2. rw 就是 class_rw_t 结构体,表示运行时类的实际结构,是 objc 动态性的体现。rw 在被 ro 初始化之后还会根据 objc 的动态性,在运行时为类添加其他属性,包括但不限于方法、属性等;

    那么,这个结论是怎么来的呢?

    2. 第一次尝试

    OC 本质是对 C/C++ 的封装,而程序运行时,本质上都是机器指令 + 数据的存、取、处理,而数据在 C/C++ 中又是通过结构体/类来定义的。所以,这里可以尝试研究下 C/C++ 源码中类对应的结构体是怎样的。

    首先来看看我们常见的 .m 文件编译之后产生的 C++ 文件。

    测试代码如下,为了方便查看,全都写在了 .m 文件下:

    #import <Foundation/Foundation.h>
    
    // 父类XKPerson
    @interface XKPerson : NSObject
    @property (copy, nonatomic) NSString *name;
    @end
    
    @implementation XKPerson
    @end
    
    // 子类XKStudent
    @interface XKStudent : XKPerson
    @property (copy, nonatomic) NSString *studioName;
    - (void)learn;
    @end
    
    @implementation XKStudent
    - (void)learn {
        NSLog(@"%@ is learning at %@",self.name, self.studioName);
    }
    @end
    
    int main(int argc, char * argv[]) {
        XKStudent *student = [XKStudent new];
        student.name = @"Jack";
        student.studioName = @"Affiliated Middle School of Tsinghua";
        [student learn];
        return 0;
    }
    

    编译 .m 文件:

     clang -rewrite-objc main.m -o main.cpp          
    

    main 函数中代码很多,为了寻找 OC 中类的本质,思路是先来看看 XKStudent 到底是个啥。XKStudent 初始化代码简化之后如下:

    XKStudent *student = (objc_msgSend)(objc_getClass("XKStudent"), sel_registerName("new"));
    

    既然 OC 背后是 C/C++,那么 XKStudent 的定义又是什么呢?如下:

    typedef struct objc_object XKStudent;
    

    objc_object 就是我们最常见的类的定义:

    typedef struct objc_class *Class;
    struct objc_object {
        Class _Nonnull isa __attribute__((deprecated));
    };
    typedef struct objc_object *id;
    typedef struct objc_selector *SEL;
    

    这里自己的理解是:使用多态的形式来表示实例变量 student,类似于可以使用 NSObject * 或者 id 来表示 OC 中任何一个实例变量。

    但是,这里有两个疑问:

    1. 源码中有使用到 struct XKStudent,而 XKStudent 是别名,已经包含了 struct,所以不需要带 struct,那这个 struct XKStudent 必定存在,在哪被声明的呢?结构体又是怎样?
    2. 除了 isa 指针外,每个实例变量都有一份自己的成员变量,而 XKStudent 是别名,本质是 struct objc_object,而 struct objc_object 内部只有一个 isa 指针,如何表示类的结构呢?

    因此,必定还存在一个 struct XKStudent 的结构体定义。

    可以先不管 OC 中方法寻找流程,但是最少需要这么一个结构体来让编译器或者 ide 知道这个类有哪些成员属性,进而进行内存分配或者是代码提示。

    XKStudent 在源码中有三种用法:

    1. XKStudent;
    2. struct XKStudent;
    3. struct XKStudent_IMPL;

    XKStudent 就是上文的 objc_object 进行了一个 typedef 操作,是多态的利用。在使用 typedef struct XKStudent XKStudent; 之后,别名是 XKStudent,已经包含了 struct,不需要再用 struct 修饰了;

    而第二种用法中, struct 是 C 语言中的结构体,仍然使用 struct 修饰表明这是一个实际存在的结构体。所以 struct XKStudent 这种使用形式下,必定存在一个 struct XKStudent {xxx} 的定义的(否则第一种用法都不能使用 typedef 来定义别名)。

    最简单的例子,比如:

    结构体实例

    但是整个编译之后的源码找不到 struct XKStudent {} 这种形式的代码,所以,猜测可能是在其他地方定义的,只不过编译之后的源码没暴露出来。这里猜测的依据是两断代码,第一段:

    XKStudent_IMPL

    这里 XKStudent_IMPL 这个结构体只在这一个地方被使用到,而且只是用来计算 size,所以,大概率这个结构体实际上并没有被使用到,完成了 size 的计算使命之后就不再使用了~~~

    还有就是 struct XKStudent 相关的代码:

    XKStudent

    struct XKStudent 有三处被使用,且都是 __OFFSETOFIVAR__ 这种形式,目的是用来取得成员变量的偏移,这是个啥?代码如下:

    #define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
    

    简化之后本质如下:

    &((TYPE *)0)->MEMBER
    

    可以看到,__OFFSETOFIVAR__ 的本质就是使用 TYPE 对应的结构体去取到 MEMBER 的在这个结构体中的偏移。

    &((TYPE *)0)->MEMBER 是成员变量访问逻辑的本质,OFFSET 是静态时期就决定的的,所以这个逻辑会影响到多态,也会影响到 self、super 的某些使用场景,在 Java 中也是如此,不点点在于 Java 中访问成员变量时直接访问,而 OC 是点方法访问成员变量。直接访问 _xxx 是不行的,因为默认 _xxx 是 private 权限修饰符修饰。详情见:this和super、成员变量的访问

    这不就是相当于在内存中使用 TYPE 这个模子来找到成员变量相对于这个结构体起始位置的偏移吗!!这更加确信 struct XKStudent 这个结构体的存在,只是这个结构体在哪被定义的呢?是怎么定义的?

    这里有两种方法可以从侧面窥探一下这个结构体

    第一种,查看运行时结构:

    结构体测试

    如上图,struct XKStudent 虽然没找到源码,但是却实际存在。这里只是一种简单的猜测来方便理解,因为对编译器实现、Xcode 调试原理没有研究,所以这里可能不正确,自己辩证来看。

    第二种,自己定义一个结构体:

    struct XKStruct {
      int a;
      int b;
    }
    

    这个时候运行编译都是不会报错的,但是使用 -rewrite-objc 指令却报错了:

    ERROR

    这里显示结构体重复定义就直接证明了 struct XKStudent 的存在。而且编译不报错,也从侧面证明这个结构体只是帮助编译器完成代码的二进制化转换,即生成类的静态数据,完成任务后,结构体也就被删除了。

    其实到这里,就可以不需要纠结 struct XKStudent 这个结构体了,而是应该把重心放在 objc 如何在动态链接时将静态数据初始化到,静态数据又是如何生成的;

    感兴趣的可以使用 clang -rewrite-legacy-objc main.m -o main.cpp 来看看旧版本的 objc 转 c++ 的源码,这里是有 struct XKStudent的,但是实际意义并不大,所以就不展示了;

    3. 另一种尝试

    从上面的分析来看,struct XKStudent 更侧重于为编译器或者 ide 提供一些信息,目的是为了生成类的静态数据。那么我们为什么不来看看静态数据和动态数据的联系呢?

    objc 源码中,类的初始化逻辑主要在 realizeClassWithoutSwift 函数中。而该函数的数据是从 __objc_classlist 或者 __objc_nlclslist 中来的:

    __objc_classlist

    上面的类的名称是不是似曾相识?那现在我们换个角度,看看 C++ 源码中何时往这些 section 中添加数据,以此来研究类的本质;

    objc 的类加载的过程中,首先读取了 __classlist 中的类列表,然后根据对应的指针去获取静态数据,也就是 ro,最后组装到 rw 上,完成初始化操作。

    而静态源码中,插入到 mach-O 的关键代码是:

    static struct _class_t *L_OBJC_LABEL_CLASS_$ [2] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
        &OBJC_CLASS_$_XKPerson,
        &OBJC_CLASS_$_XKStudent,
    };
    

    如上代码,就是把 OBJC_CLASS_$_XKStudent 这个指针存入了 mach-O 的 __DATA 这个 segment 中的 _objc_classlist section。那这个 L_OBJC_LABEL_CLASS_ 结构体是什么?

    extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_data"))) = {
        0, // &OBJC_METACLASS_$_XKStudent,
        0, // &OBJC_CLASS_$_XKPerson,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_CLASS_RO_$_XKStudent,
    };
    

    而且这个结构体的元类和父类会被填充:

    static void OBJC_CLASS_SETUP_$_XKStudent(void ) {
        OBJC_METACLASS_$_XKStudent.isa = &OBJC_METACLASS_$_NSObject;
        OBJC_METACLASS_$_XKStudent.superclass = &OBJC_METACLASS_$_XKPerson;
        OBJC_METACLASS_$_XKStudent.cache = &_objc_empty_cache;
        OBJC_CLASS_$_XKStudent.isa = &OBJC_METACLASS_$_XKStudent;
        OBJC_CLASS_$_XKStudent.superclass = &OBJC_CLASS_$_XKPerson;
        OBJC_CLASS_$_XKStudent.cache = &_objc_empty_cache;
    }
    

    总结下来,这个 setup 函数做了几件事:

    1. XKStudent 、XKPerson 和 NSObject 一样,有类、元类;
    2. XKStudent 的元类的 isa 指向 NSObject 对应的元类;
    3. XKStudent 的元类的父类是 XKPerson 对应的元类;
    4. XKStudent 的类对象的 isa 指向 XKStudent 的元类;
    5. XKStudent 的类对象的父类是 XKPerson 的类对象;

    说白了,就是这张经典图指向图:

    isa/superclass指向

    此时我们可以总结一下:

    1. OBJC_CLASS_$_XKStudent 会被存储在 mach-O 中;
    2. OBJC_CLASS_$_XKStudent 会通过 setup 函数初始化;
    3. 初始化函数中设置了元类、类对象的 isa 和 superclass 的指向;

    继续看,_class_t 又是什么?这个结构体看上去正好和 objc 中的 objc_class 对应:

    objc_class

    首先,不需要知道 cache 和 vtable 在 objc 上为什么只用一个 cache 来实现,看注释可以知道,objc 中的 cache 就是表示 cache pointer 和 vtable。

    那么接下来就只剩下 ro 和 bits 是否对应了,如果对应,那么静态结构体就和 rw 中的 ro 对应,也就是静态结构体被存储在了 bit 中。

    这里需要重点关注两个函数:

    struct objc_class : objc_object {
        ....省略...
        class_rw_t *data() {
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
        ....省略...
    }
    

    在类的初始化函数 realizeClassWithoutSwift 中会直接使用 ro 对 rw 进行赋值并调用 setData() 传递给 class:

    // Normal class. Allocate writeable class data.
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    

    bits 没什么好说的,就是一个 unsigned long 类型,在 MacOS/iOS 中占 8 个字节,也就是 64 位,感觉就是为了和指针的 size 对应而制定的一个类型。重点是 bits 中这两个方法的具体实现:

    // struct class_data_bits_t
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    void setData(class_rw_t *newData) {
        // 对3-47位按位取反之后,这44位就都是0了,相当于清空3-47位而不清空0-2位
        // 按位运算,只要一个位1就为1,newData因为8字节对齐原则,0-2位必定为0
        // 整个流程最终的结果就是只修改3-47位,也就是修改rw
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }
    

    要理解上面的代码,需要知道 FAST_DATA_MASK 是什么。这个宏在 64 位系统下是这么定义的:

    #define FAST_DATA_MASK          0x00007ffffffffff8UL
    

    UL 表示 unsign long,可以直接去掉,因为和这个宏定义相关的都是位运算,来看看转换成二进制之后是多少:

    FAST_DATA_MASK二进制

    一共是 44 + 3 = 47 位,且后三位都是 0;

    上述 get 方法中,直接和 FAST_DATA_MASK 进行位运算,其意义就是取 3-47 位的值。

    而 set 方法中,做了这么几步:

    1. ~ 表示按位取反,所以 0-2 位变成了 1,相反的,3-47 位变成了 0;
    2. & 表示两个数按位且运算,如果都为 1 时,该位才为 1;
    3. | 表示两个数按位或,如果有一个为 1 ,该位就为 1;

    上述三个运算符对应的目的和结果是:

    1. 按位取反之后,和原始数据进行按位且运算,最终保留了原始数据中的 0-2 位,且清空了 3-47 位;
    2. 将上一步的结果和新数据按位或运算,结果就是获取到了新数据;

    因为内存时按 8 字节对齐的,所以新数据过来的时候是一个内存地址,这个地址后 3 位必定为 0,所以上述第二步中,因为是 或运算,最后三位的值在经过 set 之后保持原样。

    那为什么是 3-47 位?这个其实是和 isa 中的 shiftcls 是对应的:

    shiftcls

    上述 MACH_VM_MAX_ADDRESS 进行二进制转换之后分别是 47 位和 36 位:

    MACH_VM_MAX_ADDRESS二进制转换 MACH_VM_MAX_ADDRESS二进制转换

    因为内存地址最少是按 8 字节对齐(OC 中是 16字节),所以后三位可以必定为 0 ,在存储过程中可以直接抹掉,取出时加上就行,所以 shiftcls 的存取时会有对应的位移操作:

    // 存储时右移3位抹0
    newisa.shiftcls = (uintptr_t)newCls >> 3;
    // 读取时左移3位补0
    return (Class)((uintptr_t)oldisa.shiftcls << 3);
    

    所以,objc_class 中的 bit 中的 3-47 位存储的就是类的静态数据 ro,也就是 __objc_classlist 表中的指针所指向的数据。而节约出来的 0、1 、2 这三个比特位则可以用来存储三个标志位。三个标志位的存取同样是通过位运算完成,就不再赘述。

    至此,可以得出结论:

    1. 类的静态数据存储在 struct objc_class 的 bits 中;
    2. bit 类型是 unsign long,本质上是一个存储指针的容器;
    3. 64 位操作系统下指针的容量有冗余,系统的最大指针地址用 MAX_ADDRESS 表示,一般是 47/36 位;
    4. 内存对齐的本质是提高 CPU 寻址效率,而指针为 8 字节,所以大部分系统都是最少以 8 字节对齐,也就是可以多,但不能少,比如 objc 就是 16 字节对齐;
    5. 基于 8 字节内存对齐原则,最后 3 位必定为 0 ,所以 bit 的最后 3 位可以用来存储其他信息;
    6. 基于以上前提,objc 中才有了 3 位移运算,33/47 位 Mask 这些基本操作;

    通过代码分析已经得出结论:_class_t 和 objc 中的 object_class 对应:

    _class_t

    也就是说 object_class 中的 bit 对应的就是 _class_t 中的 ro,那么静态时期这个 ro 何时被赋值?代码如下:

    extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_data"))) = {
        0, // &OBJC_METACLASS_$_XKStudent,
        0, // &OBJC_CLASS_$_XKPerson,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_CLASS_RO_$_XKStudent,
    };
    

    也就是在 setup 函数被调用之前,这个 ro 就已经被赋值了,这个值就是 _OBJC_CLASS_RO_$_XKStudent,这个结构体定义如下:

    static struct _class_ro_t _OBJC_CLASS_RO_$_XKStudent __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        0, //flags
        __OFFSETOFIVAR__(struct XKStudent, _studioName), //instanceStart
        sizeof(struct XKStudent_IMPL), //instanceSize
        (unsigned int)0, //reserved
        0, //ivarlayout
        "XKStudent", //name
        (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_XKStudent, //Method
        0, // protocols
        (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_XKStudent, //ivars
        0, //weakIvarLayout
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_XKStudent,//properties
    };
    

    那么 _class_ro_t 是什么呢?而 objc 中又有一个 clsss_ro_t,这两是什么关系?猜测应该也是对应的。来对比一下看看:

    对比

    这两个完全就是一模一样啊!!!

    现在,回到最初的观点:类在初始化时,使用 ro 来初始化 rw。而 ro 就是 clss_ro_t 的类型,这个类型正好和静态编译时期生成的 _class_ro_t 完全重合。所以,这个时候需要来看下 rw 的初始化流程到底是怎么样的,静态数据是如何在初始化阶段被赋值的?

    这里就回到了最初的代码:

    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    

    上述代码直接将 ro 赋值给 rw 的 ro 属性,然后调用了 cls 的 setData 方法,将 rw 赋值给 cls,完成静态数据初始化。

    4. 验证

    因为 coding 的本质是机器指令 + 数据,看看运行时内存中的数据是否和静态数据 ro 对应即可验证我们的逻辑。

    这里,直接把 objc 中的结构体移植过来使用,看看 XKStudent 的类对象的 name 是否和 _OBJC_CLASS_RO_$_XKStudent 结构体一致即可。

    首先把 object_class 移植过来:

    struct xk_objc_class : xk_objc_object {
        // Class ISA;
        xkClass superclass;
        xk_cache_t cache;             // formerly cache pointer and vtable
        xk_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    };
    

    然后依次补足 cache_tclass_data_bits_t

    struct bucket_t {
    private:
    #if __arm64__
        uintptr_t _imp;
        SEL _sel;
    #else
        SEL _sel;
        uintptr_t _imp;
    #endif
    };
    
    #if __LP64__
    typedef uint32_t xk_mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    #else
    typedef uint16_t xk_mask_t;
    #endif
    
    struct xk_cache_t {
        struct bucket_t *_buckets;
        xk_mask_t _mask;
        xk_mask_t _occupied;
    };
    
    struct xk_class_data_bits_t {
        uintptr_t bits;
    };
    

    还要移植 isa_tobjc_object

    typedef struct xk_objc_class *xkClass;
    
    union xk_isa_t {
        xk_isa_t() { }
        xk_isa_t(uintptr_t value) : bits(value) { }
    
        xkClass cls;
        uintptr_t bits;
    #if defined(ISA_BITFIELD)
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    #endif
    };
    
    struct xk_objc_object {
        xk_isa_t isa;
    };
    

    最后补足宏定义:

    #if !__LP64__
    
    #elif 1
    
    #define XK_FAST_DATA_MASK          0x00007ffffffffff8UL
    
    #else
    
    #define FAST_DATA_MASK          0x00007ffffffffff8UL
    
    #endif
    
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    # else
    #   error unknown architecture for packed isa
    # endif
    

    上述结构体中,为了方便验证,删除了 method 等比较复杂的结构体。

    最后,我们再来写两个 OC 相关的结构体:

    struct xk_person : xk_objc_object {
        NSString *name;
    };
    
    struct xk_student : xk_person {
        NSString *studioName;
    };
    

    此时,就可以写验证的代码了:

    int main(int argc, char * argv[]) {
        
        XKStudent *student = [XKStudent new];
        student.name = @"Jack";
        student.studioName = @"Affiliated Middle School of Tsinghua";
        
        struct xk_student *obj = (__bridge struct xk_student *)student;
        xk_isa_t isa = (*obj).isa;
        void *p = (void *)((isa.bits) & ISA_MASK);
        
        NSLog(@"isa:%p",p);
        NSLog(@"name:%p",obj->name);
        NSLog(@"studioName:%p",obj->studioName);
    
        struct xk_objc_class *cls = (__bridge struct xk_objc_class *)[XKStudent class];
        uintptr_t cls_bits = (*cls).bits.bits;
        struct xk_class_rw_t *cls_rw = (struct xk_class_rw_t *)(cls_bits & XK_FAST_DATA_MASK);
        
        const xk_class_ro_t *cls_ro = (*cls_rw).ro;
        const char *cls_name = (*cls_ro).name;
        
        NSLog(@"class name:%s",cls_name);
    
        return 0;
    }
    

    结果:

    isa for instance:0x102fbe0d8
    name:Jack
    studioName:Affiliated Middle School of Tsinghua
    class pointer:0x102fbe0d8
    class name:XKStudent
    

    上述代码中:

    1. 创建了一个 XKStudent 的实例;
    2. 使用自定义的 struct xk_student 指针来指向实例对象;
    3. 获取实例对象的 isa,并使用 Mask 获取到真实的 shiftcls 的值;
    4. 打印class、name、studioName 属性;
    5. 获取类对象的地址;
    6. 打印之后可以发现,类对象地址和实例对象的 isa 中存储的 shiftcls 指向一致;
    7. 依次获取到 bits、rw、ro;
    8. 打印 ro 中的 name;

    isa 相关知识详见:iOS:isa指针

    其实这段代码可以玩一阵子,比如在 MacOS 应用上跑这段代码,如果不支持指针优化,那么 cls 就直接指向类对象。再比如可以继续验证属性、方法等静态结构体,还可以找一找元类、父类对象等,就不再赘述了。

    5. 总结

    1. OC 中类首先在静态阶段,被编译器生成相关的结构体。这些结构体包括:实例对象、类对象、元类对象、方法、属性等;
    2. 静态阶段还通过 setup 等方法先进行静态初始化,完成了对象的关联关系的建立,也就是那张经典的 isa、superclass 指向图;
    3. 静态时期初始化完成的结构体会被存储到 mach-O 文件中;
    4. 动态链接时,dyld 和 objc 通过回调的方式触发类的加载流程。
    5. 类的动态初始化中,读取静态时期的 ro 并且赋值给 rw,完成最基本的初始化逻辑;

    后续初始化逻辑还干了什么?下回分解......

    相关文章

      网友评论

        本文标题:iOS类加载流程(二):类的静态初始化

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