F4670be52d4a6c55d7c66e2c39d2c9d1
iOS 高级之美(九)—— 类结构分析下篇

一、文章前言

我们通过 LLDB 分析得出:

  • cls 的类结构信息
  • 0x0000000100000f9d 是我们相应 isa的指向
  • 0x0000000800000003 就是我们的父类
  • 0x0000000100000010 就是我们常说的cache_t 信息
  • 0x0000000100000f54 就是我们 data() 数据的落地之处!这个篇章我们就是针对这个家伙展开分析

二、class_data_bits_t 分析

struct class_data_bits_t {} 结构体类型,我们通过-

uintptr_t bits 是一个 uintptr_t 类型的指针,在编译阶段会指向 class_ro_t ,在运行时会封装成 class_rw_t

2.1、class_ro_t 分析

class_ro_t 存储了很多在编译时期就确定的类的信息。其内部包含了类名、ivar、方法、属性等。是只读类型的结构。

struct class_ro_t {
    uint32_t flags;               //配合mask 可以用来判断,元类,根类等          
    uint32_t instanceStart;       //non-fragile判断依据
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;           
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

其中 instanceStartinstanceSize 是为了实现 Non Fragile特性。

In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.

下面我们来读取下 class_ro_t 的信息,为了证明 class_ro_t 是在编译期期确定,我们要在 runtime 的入口下个符号断点 objc_init (这也可以先运行一次,先得到类对象的地址,第二次符号断点直接使用)。由于 objc_initruntime 入口(runtime还没有处理类的元数据)。

2.2、class_rw_t

从上面的章节看出 class_ro_t 存储的大多是类在编译时就已经确定的信息,但是 Objective-C 又是一门动态语言,因此需要另一个可以运行时 读写 数据,也就是 class_rw_t

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;           //同样结合mask 使用
    uint32_t version;
    const class_ro_t *ro;     //ro的指针
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;     

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    ...
};

class_rw_t 提供了运行时对类拓展的能力,存有类的方法、属性(成员变量)、协议等信息。class_rw_t 的内容是可以在运行时被动态修改的,可以说运行时对类的拓展大都是存储在这里的。

demangledName 是计算机语言用于解决实体名称唯一性的一种方法,做法是向名称中添加一些类型信息,用于从编译器中向链接器传递更多语义信息

值得注意的是 method_array_t 的结构会有三种变化。

  • 1️⃣:空。
  • 2️⃣:一个一维数组的指针。(默认为一维数组,就算一个类有category,如果没有实现 +load,就也是一维数组,没有实现 +load,就意味这个类可以懒加载)
  • 3️⃣:指向列表的指针数组。(当一个类存在 category 且这个类无法懒加载,或者动态修改方法列表如 addMethod。就会生成二维列表,)

如果从二进制的角度讲,只要这个类存在于 __objc_catlist 或者 __objc_nlcatlist ,那么他的 method_array_t 就为二维数组。
那么 class_rw_t 是什么时候被赋值的呢。

三、类的初始化

3.1、realizeClass

首先简单看一个runtime初始化的流程。

  • ①.objc_init

  • ②. read_images 读取所有镜像文件信息

    • 1. 初始化全局 class map,读取 macho__objc_classlist 初始化全局 class map__objc_classrefs
    • 2. 读取 __objc_selrefs等段读取相关。进行 sel fix up
    • 3. 读取 protocol
    • 4. 读取所有 非懒加载 类,并调用 realizeClass 进行初始化,进一步调用 methodizeClass(Attach categories)
    • 5. 读取 categories,把读取出来的 categories 放到全局的 map 里,如果 cat 关联的 cls 已经被 realize 则进行remethodizeClass
  • ③. 对所有非懒加载的类 和 category 执行 +load

类的可读写元数据的初始话主要发生在 realizeClassmethodizeClass

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();
    ... 
    // fixme verify class is not in an un-dlopened part of the shared cache?
    //RO_FUTURE 标志位来记录是否可以直接强转。
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } 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;
        cls->setData(rw);
    }

    isMeta = ro->flags & RO_META;

    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6
    ...
    优先执行supercls 和metacls 的realize
    检测isa 优化
    更新实例大小,关联子类父类,元类根类。
    ...
    // Attach categories
    methodizeClass(cls);
    return cls;
}
  • realizeClass 方法中会根据 ro 中的 flagsRO_FUTURE 来判断是否可以直接进行类型转换,如不可以则需要另外给 rw 申请内存。

    • 同时也注意到,在生成 rw 后,并没有进行方法属性等列表的赋值,而方法等列表的拼接是在 methodizeClass
top Created with Sketch.