探秘Runtime-剖析Runtime结构体

25次阅读

共计 10146 个字符,预计需要花费 26 分钟才能阅读完成。

该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:

< 简书 — 刘小壮 > https://www.jianshu.com/p/5b7e7c8075ef



NSObject

之前的定义

OC1.0 中,Runtime很多定义都写在 NSObject.h 文件中,如果之前研究过 Runtime 的同学可以应该见过下面的定义,定义了一些基础的信息。

// 声明 Class 和 id
typedef struct objc_class *Class;
typedef struct objc_object *id;

// 声明常用变量
typedef struct objc_method *Method;
typedef struct objc_ivar *Ivar;
typedef struct objc_category *Category;
typedef struct objc_property *objc_property_t;

// objc_object 和 objc_class
struct objc_object {Class _Nonnull 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;

之前的 Runtime 结构也比较简单,都是一些很直接的结构体定义,现在新版的 Runtime 在操作的时候,各种地址偏移操作和位运算。

之后的定义

后来可能苹果也不太想让开发者知道 Runtime 内部的实现,所以就把源码定义从 NSObject 中搬到 Runtime 中了。而且之前的定义也不用了,通过 OBJC_TYPES_DEFINED 预编译指令,将之前的代码废弃调了。

现在 NSObject 中的定义非常简单,直接就是一个 Class 类型的 isa 变量,其他信息都隐藏起来了。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

这是最新的一些常用 Runtime 定义,和之前的定义也不太一样了,用了最新的结构体对象,之前的结构体也都废弃了。

typedef struct objc_class *Class;
typedef struct objc_object *id;

typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;

对象结构体

objc_object 定义

在 OC 中每个对象都是一个结构体,结构体中都包含一个 isa 的成员变量,其位于成员变量的第一位。isa的成员变量之前都是 Class 类型的,后来苹果将其改为isa_t

struct objc_object {
private:
    isa_t isa;
};

OC 中的类和元类也是一样,都是结构体构成的。由于类的结构体定义继承自 objc_object,所以其也是一个对象,并且具有对象的isa 特征。

所以可以通过 isa_t 来查找对应的类或元类,查找方法应该是通过 uintptr_t 类型的 bits,通过按位操作来查找isa_t 指向的类的地址。

实例对象或类对象的方法,并不会定义在各个对象中,而是都定义在 isa_t 指向的类中。查找到对应的类后,通过类的 class_data_bits_t 类型的 bits 结构体查找方法,对象、类、元类都是同样的查找原理。

isa_t 定义

isa_t是一个 union 的结构对象,union类似于 C++ 结构体,其内部可以定义成员变量和函数。在 isa_t 中定义了 clsbitsisa_t 三部分,下面的 struct 结构体就是 isa_t 的结构体构成。

下面对 isa_t 中的结构体进行了位域声明,地址从 nonpointer 起到 extra_rc 结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占 8 字节,所有的位域加在一起正好是 64 位。

小提示:unionbits 可以操作整个内存区,而位域只能操作对应的位。

下面的代码是不完整代码,只保留了 arm64 部分,其他部分被忽略掉了。

union isa_t 
{isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; // 是 32 位还是 64 位
        uintptr_t has_assoc         : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象
        uintptr_t has_cxx_dtor      : 1; // 表示是否有 C ++ 析构函数或 OC 的析构函数
        uintptr_t shiftcls          : 33; // 对象指向类的内存地址,也就是 isa 指向的地址
        uintptr_t magic             : 6; // 对象是否初始化完成
        uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用
        uintptr_t deallocating      : 1; // 对象是否被释放中
        uintptr_t has_sidetable_rc  : 1; // 对象引用计数太大,是否超出存储区域
        uintptr_t extra_rc          : 19; // 对象引用计数
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__
// ····
# else
// ····
# endif
};

ARM64 架构下,isa_t以以下结构进行布局。在不同的 CPU 架构下,布局方式会有所不同,但参数都是一样的。

类结构体

objc_class 结构体

Runtime 中类也是一个对象,类的结构体 objc_class 是继承自 objc_object 的,具备对象所有的特征。在 objc_class 中定义了三个成员变量,superclass是一个 objc_class 类型的指针,指向其父类的 objc_class 结构体。cache用来处理已调用方法的缓存。

bitsobjc_class 的主角,其内部只定义了一个 uintptr_t 类型的 bits 成员变量,存储了 class_rw_t 的地址。bits中还定义了一些基本操作,例如获取 class_rw_traw isa 状态、是否 swift 等函数。objc_class结构体中定义的一些函数,其内部都是通过 bits 实现的。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits;    

    class_rw_t *data() {return bits.data();
    }
    void setData(class_rw_t *newData) {bits.setData(newData);
    }
    // .....
}

objc_class 的源码可以看出,可以通过 bits 结构体的 data() 函数,获取 class_rw_t 指针。我们进入源代码中看一下,可以看出是通过对 uintptr_t 类型的 bits 变量,做位运算查找对应的值。

class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);
}

uintptr_t本质上是一个 unsigned longtypedefunsigned long在 64 位处理器中占 8 字节,正好是 64 位二进制。通过 FAST_DATA_MASK 转换为二进制后,是取 bits 中的 47-3 的位置,正好是取出 class_rw_t 指针。

在 OC 中一个指针的长度是 47,例如打印一个 UIViewController 的地址是0x7faf1b580450,转换为二进制是11111111010111100011011010110000000010001010000,最后面三位是占位的,所以在取地址的时候会忽略最后三位。

// 查找第 0 位,表示是否 swift
#define FAST_IS_SWIFT           (1UL<<0)
// 当前类或父类是否定义了 retain、release 等方法
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// 类或父类需要初始化 isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// 数据段的指针
#define FAST_DATA_MASK          0x00007ffffffffff8UL
// 11111111111111111111111111111111111111111111000 总共 47 位

因为在 bits 中最后三位是没用的,所以可以用来存储一些其他信息。在 class_data_bits_t 还定义了三个宏,用来对后三位做位运算。

class_ro_t 和 class_rw_t

class_data_bits_t 相关的有两个很重要结构体,class_rw_tclass_ro_t,其中都定义着method listprotocol listproperty list 等关键信息。

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};

在编译后 class_data_bits_t 指向的是一个 class_ro_t 的地址,这个结构体是不可变的 (只读)。在运行时,才会通过realizeClass 函数将 bits 指向class_rw_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;

    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;
};

在程序开始运行后会初始化 Class,在这个过程中,会把编译器存储在bits 中的 class_ro_t 取出,然后创建 class_rw_t,并把ro 赋值给 rw,成为rw 的一个成员变量,最后把 rw 设置给 bits,替代之前bits 中存储的 ro。除了这些操作外,还会有一些其他赋值的操作,下面是初始化Class 的精简版代码。

static Class realizeClass(Class cls) 
{
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;

    ro = (const class_ro_t *)cls->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;

    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()))

    cls->superclass = supercls;
    cls->initClassIsa(metacls);
    cls->setInstanceSize(ro->instanceSize);

    if (supercls) {addSubclass(supercls, cls);
    } else {addRootClass(cls);
    }

    methodizeClass(cls);
    return cls;
}

在上面的代码中我们还发现了两个函数,addRootClassaddSubclass 函数,这两个函数的职责是将某个类的子类串成一个列表,大致是下面的链接顺序。由此可知,我们是可以通过class_rw_t,获取到当前类的所有子类。

superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...

初始化 rwro之后,rwmethod listprotocol listproperty list 都是空的,需要在下面 methodizeClass 函数中进行赋值。函数中会把 rolist都取出来,然后赋值给 rw,如果在运行时动态修改,也是对rw 做的操作。所以 ro 中存储的是编译时就已经决定的原数据,rw才是运行时动态修改的数据。

static void methodizeClass(Class cls)
{bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    method_list_t *list = ro->baseMethods();
    if (list) {prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {rw->protocols.attachLists(&protolist, 1);
    }

    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}

假设创建一个类 LXZObject,继承自NSObject,并为其加入一个testMethod 方法,不做其他操作。因为在编译后 objc_classbits对应的是 class_ro_t 结构体,所以我们打印一下结构体的成员变量,看一下编译后的 class_ro_t 是什么样的。

struct class_ro_t {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x0000000100000f7a "LXZObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000000000000
}

经过打印可以看出,一个类的 class_ro_t 中只会包含当前类的信息,不会包含其父类的信息,在 LXZObject 类中只会包含 namebaseMethodList两个字段,而 baseMethodList 中只有一个 testMethod 方法。由此可知,class_rw_t结构体也是一样的。

初始化过程

下面是已经初始化后的 isa_t 结构体的布局,以及各个结构体成员在结构体中的位置。

union经常配合结构体使用,第一次使用 union 就是对结构体区域做初始化。在对象初始化时,会对 isa_tbits字段赋值为 ISA_MAGIC_VALUE,这就是对union 联合体初始化的过程。

// 在 objc-723 中已经没有了
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 
{if (!indexed) {isa.cls = cls;} else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

在对象通过 initIsa() 函数初始化时,会通过 ISA_MAGIC_VALUEisa进行初始化。ISA_MAGIC_VALUE是一个 16 进制的值,将其转换为二进制后,会发现 ISA_MAGIC_VALUE 是对 nonpointermagic做初始化。

nonpointer是对之前 32 位处理器的兼容。在访问对象所属的类时,如果是 32 位则返回之前的 isa 指针地址,否则表示是 64 位处理器,则返回 isa_t 结构体。

# define ISA_MAGIC_VALUE 0x000001a000000001ULL
二进制:11010000000000000000000000000000000000001
补全二进制:23 个零 +11010000000000000000000000000000000000001

随后会通过位域,对 has_cxx_dtorshiftcls做初始化,这时候就已经有四个字段被初始化了。has_cxx_dtor表示是否有 C++ 或 OC 的析构方法,在打印方法列表时,经常能看到一个名为 .cxx_destruct 的方法,就和这个字段有关系。

在计算机中为了对存储区 (Memory or Disk) 读取方便,所以在写入和读取时,会对内存有对其操作。一般是以字节为单位进行对其,这样也是对读写速度的优化。在对 shiftcls 进行赋值时,对 Class 的指针进行了位移操作,向右位移三位。这是因为类指针为了内存对其,将最后三位用 0 填充,所以这三位是没有意义的。

isa 结构体
0000000001011101100000000000000100000000001110101110000011111001
0x5d8001003ae0f8

类对象地址
100000000001110101110000011111000
0x1003ae0f8

将类对象地址右移三位为 100000000001110101110000011111,正好符合 isa_t 地址中 shiftcls 的部分,前面不足补零。

外界获取 Class 时,应该通过 ISA() 函数,而不是像之前一样直接访问 isa 指针。在 ISA() 函数中,是对 isa_t 的结构体做与运算,是通过 ISA_MASK 宏进行的,转换为二进制的话,正好是把 shiftcls 的地址取出来。

inline Class 
objc_object::ISA() 
{return (Class)(isa.bits & ISA_MASK);
}

#define ISA_MASK 0x0000000ffffffff8ULL
111111111111111111111111111111111000

Tagged Pointer

iPhone5s 开始,iOS设备开始引入了 64 位处理器,之前的处理器一直都是 32 位的。

但是在 64 位处理器中,指针长度以及一些变量所占内存都发生了改变,32 位一个指针占用 4 字节,但 64 位一个指针占用 8 字节;32 位一个 long 占用 4 字节,64 位一个 long 占用 8 字节等,所以在 64 位上内存占用会多出很多。

苹果为了优化这个问题,推出了 Tagged Pointer 新特性。之前一个指针指向一个地址,而 Tagged Pointer 中一个指针就代表一个值, 以 NSNumber 为例。

NSNumber *number1 = @1;
NSNumber *number2 = @3;
NSNumber *number3 = @54;

// 输出
(lldb) p number1
(__NSCFNumber *) $3 = 0xb000000000000012 (int)1
(lldb) p number2
(__NSCFNumber *) $4 = 0xb000000000000032 (int)3
(lldb) p number3
(__NSCFNumber *) $5 = 0xb000000000000362 (int)54

通过上面代码可以看出,使用了 Tagged Pointer 新特性后,指针中就存储着对象的值。例如一个值为 1 的 NSNumber,指针就是0xb000000000000012,如果抛去前面的0xb 和后面的 2,中间正好就是 16 进制的值。

苹果通过 Tagged Pointer 的特性,明显的提升了执行效率并节省了很多内存。在 64 位处理器下,内存占用减少了将近一半,执行效率也大大提升。由于通过指针来直接表示数值,所以没有了 mallocfree的过程,对象的创建和销毁速度提升几十倍。

isa_t

对于对象指针也是一样,在 OC1.0 时代 isa 是一个真的指针,指向一个堆区的地址。而 OC2.0 时代,一个指针长度是八字节也就是 64 位,在 64 位中直接存储着对象的信息。当查找对象所属的类时,直接在 isa 指针中进行位运算即可,而且由于是在栈区进行操作,查找速度是非常快的。

struct {
    uintptr_t nonpointer        : 1;
    uintptr_t has_assoc         : 1;
    uintptr_t has_cxx_dtor      : 1;
    uintptr_t shiftcls          : 33;
    uintptr_t magic             : 6;
    uintptr_t weakly_referenced : 1;
    uintptr_t deallocating      : 1;
    uintptr_t has_sidetable_rc  : 1;
    uintptr_t extra_rc          : 19;
};

例如 isa_t 本质上是一个结构体,如果创建结构体再用指针指向这个结构体,内存占用是很大的。但是 Tagged Pointer 特性中,直接把结构体的值都存储到指针中,这就相当节省内存了。

苹果不允许直接访问 isa 指针,和 Tagged Pointer 也是有关系的。因为在 Tagged Pointer 的情况下,isa并不是一个指针指向另一块内存区,而是直接表示对象的值,所以通过直接访问 isa 获取到的信息是错误的。

Tagged Pointer


简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!????

正文完
 0