Runtime的原理探索-类的结构

/ 0

概述

runtime 简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。基于 C/C++ 语言和汇编语言编写,苹果和 GNU 各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。

对于 C 语言而言,函数的调用在编译的时候就会决定调用哪个函数,无任何二义性。

OC 的方法调用则称为消息发送,在编译的时候并不能决定真正调用哪个方法,只有在运行的时候才会根据方法的名称找到对应的方法来调用。

Objective-C 程序在三个不同的层次上与运行时系统交互:

  1. 通过 Objective-C 源代码进行交互;
  2. 通过 NSObject 类中定义的方法交互;
  3. 通过直接调用运行时函数;

支撑OC动态性

OC 是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。

OC 的动态性就是由 runtime 来支撑和实现的,runtime 封装了很多动态性相关的函数。

平时编写的 OC 代码,底层都是转换成了 runtime API 进行调用。

具体应用

isa详解

arm64 架构之前,isa 就是一个普通的指针,存储着 ClassMeta-Class 对象的内存地址。

arm64 架构开始,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

#include "isa.h"

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

    uintptr_t bits;

private:
    // 访问该类需要自定义 ptrauth 操作,因此通过将其设为私有来强制客户端通过 setClass/getClass。
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 结构体的成员,以宏定义方式定义在 isa.h 中,并且区分多个CPU架构
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

下面是 arm64 架构的 isa 位域宏定义,所有信息加起来刚好是 64 位,也就是 8 字节:

#define ISA_BITFIELD                                                      \
    uintptr_t nonpointer        : 1;                                       \
    uintptr_t has_assoc         : 1;                                       \
    uintptr_t has_cxx_dtor      : 1;                                       \
    uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
    uintptr_t magic             : 6;                                       \
    uintptr_t weakly_referenced : 1;                                       \
    uintptr_t unused            : 1;                                       \
    uintptr_t has_sidetable_rc  : 1;                                       \
    uintptr_t extra_rc          : 19

nonpointer

0 代表普通的指针,存储着 ClassMeta-Class 对象的内存地址。

1 代表优化过,使用位域存储更多的信息。

has_assoc

是否有设置过关联对象,如果没有,释放时会更快。

has_cxx_dtor

是否有 C++ 的析构函数(.cxx_destruct),如果没有,释放时会更快。

shiftcls

存储着 ClassMeta-Class 对象的内存地址信息。地址值最后一位永远是08,因为后3个二进制位是0

magic

用于在调试时分辨对象是否未完成初始化。

weakly_referenced

是否有被弱引用指向过,如果没有,释放时会更快。

deallocating

对象是否正在释放。

extra_rc

里面存储的值是引用计数器减1

has_sidetable_rc

引用计数器是否过大无法存储在 isa 中。

如果为1,那么引用计数会存储在一个叫 SideTable 的类的属性中。

如何分析isa

创建一个对象,然后断点后,使用 lldb 指令打印 isa

NSObject *obj = [[NSObject alloc] init];

// 打印isa
//(lldb) p/x obj->isa
//(Class) $1 = 0x01000001e8b52331 NSObject

然后根据位域中每个信息的位数和含义去分析即可,我这是用 M1 运行的,应该按照 arm64e 架构的位域信息结构体来分析:

0x01000001e8b52331 转二进制
0b 0000 0001 0000 0000 0000 0000 0000 0001
   1110 1000 1011 0101 0010 0011 0011 0001

Class的结构

struct objc_object {
    isa_t isa; // isa共用体
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;            // 方法缓存
    class_data_bits_t bits;    // 用于获取具体的类信息
};

struct class_rw_t {
    // ...
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    // ...
private:
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
};

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro; // 指向 class_ro_t
    method_array_t methods; // 方法列表,二维数组,存放method_list_t,method_list_t存放method_t
    property_array_t properties; // 属性列表,二维数组
    protocol_array_t protocols; // 协议列表,二维数组
    char *demangledName;
    uint32_t version;
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; // instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name; // 类名
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList; // 方法列表,一维数组,直接存放method_t
    protocol_list_t * baseProtocols; // 协议列表,一维数组
    const ivar_list_t * ivars; // 成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 属性列表,一维数组
};

method_t

method_t 是对方法/函数的封装,底层结构:

struct method_t {
    struct big {
        SEL name; // 方法名
        const char *types; // 类型编码(返回值类型、参数类型)
        MethodListIMP imp; // 指向方法实现的函数的指针(函数地址)
    };
};

SEL

可以简单理解为方法名的字符串,通过 @selector() 或者 sel_registerName() 获得。

不同类中相同名字的方法,所对应的方法选择器是相同的。

SEL 是一个类型别名,不过 objc_selector 结构体的源码可能没开源:

typedef struct objc_selector *SEL;

Type Encoding

类型编码,iOS 中提供了一个叫做 @encode 的指令,可以将具体的类型表示成字符串编码。

符号和类型对照表:苹果官方文档

NSLog(@"%s %s %s %s %s", @encode(Person), @encode(int), @encode(void), @encode(SEL), @encode(id));
//  {Person=#} i v : @

方法类型编码示例:

// types: i24@0:8i16f20
// i24: 返回值类型int,所有参数占的24字节数
// @0: 第1个参数类型id(8字节),从第0字节开始
// :8: 第2个参数类型SEL(8字节),从第8字节开始
// i16: 第3个参数类型int(4字节),从第16字节开始
// f20: 第4个参数类型float(4字节),从第20字节开始
- (int)testWithArg1:(int)arg1 arg2:(float)arg2;

IMP

指向方法实现的函数的指针(函数地址)。

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

using MethodListIMP = IMP;

方法缓存

Class 内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。

cache_t 的结构:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
};

如果不使用缓存,则每次调用方法都是要根据类的父子关系,逐层从 类对象元类对象 中的 class_ro_tclass_rw_ext_t 里的方法数组中遍历查找。