Files
knowledge-kit/Chapter1 - iOS/1.82.md
2026-01-02 10:28:57 +08:00

154 KiB
Raw Blame History

Runtime

做很多技术项目或者业务项目、再或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就结合一些场景和源码分析来系统化学习。

2个问题

  • OC 的消息机制是什么样的?
  • OC 中 super 底层是什么?[super superclass]、[self superclass] 打印结果是什么

带着问题学习本文

动态语言

静态语言:在编译阶段确定了变量数据类型、函数地址等,无法动态修改。

动态语言:只有在运行的时候才可以决定变量属于什么类型、方法真正的地址。

OC 是一门动态性语言Runtime 是实现 OC 语言动态的 API。

@interface Person : NSObject
{
    NSString *_name; 
}
@property (nonatomic, strong) NSString *hobby;
@end

malloc_size((__bridge const void *)(p))    // 24 isa占8字节 + _name 指针占8字节 + hobby 指针占8字节 = 24 
class_getInstanceSize(p.class)             // 32 ,系统内存对齐 

QA为什么系统是由16字节对齐的

成员变量占用 8 字节对齐,每个对象的第一个都是 isa 指针必须要占用8字节。举例一个极端 case假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐会加快访问速度参考链表和数组的设计

接下去深入探索下 runtime。

使用较少内存存储属性值

位运算的设计

假设给 Person 对象设置高、富、帅3个属性如果要用最少的内存该怎么设计呢

先看几组位运算的特点:

// 如何把最右边的0变为1其他位不变00000001 按位或即可
0010 1000
0000 0001
-----------
0010 1001


// 如何把最右边的1变位0其他位不变11111110 按位与即可。也就是将(0000 0001)取反,再按位与即可
0010 0001
1111 1110
-----------
0010 1110

也就是说我们可以根据位运算的特点可以更改某个位的数据可以随意更改为0或者1

Person 类存在3个 BOOL 属性:

  • 用一个 char 来存储 tall、rich、handsome 3个属性的值用3个位的0、1表示 BOOL
  • 从最右到左的3位代表高富帅3个布尔值。只有高则表示为0000 0001,只有富则表示为 0000 0010 ,只有帅则表示为 0000 0100
  • 对高的 getter 问题转换为对一个字节数据的特定位取值问题。该 case判断高的 BOOL 值变为,对最后一位的取值。可以与 0000 0001 按位与,然后转换为 BOOL 即可
  • 对高的 setter ,则演变为对一个字节数据的特点位存值的问题。需要区分真假,如果为真,则可以与 0000 0001 按位或运算,或运算,一真为真。如果是假,则与 1111 1110 按位与即可,1111 1110 也就是 0000 0001 取反,也就是 ~0000 0001

上 Demo

结构体位域

位运算方案虽然实现了使用较少内存存储了 Person 的3个 BOOL 属性值。但是后续增加属性不够灵活,需要关心位运算,不具备较好拓展性

新方案采用:结构体的位域能力,限定单个成员变量所占用的内存。代码如下:

共用体

虽然上述方式都可以实现存储 Person 类3个属性的目的但是还有第三种方案参考 iOS 系统设计,采用 Union 实现。代码如下

分析:

union {
    char bits;
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    };
} _infoUnit;

Union 里面的内容等价于下面的内容,因为 struct 使用了位域限制了成员变量的大小所以占用3个空间的结构体还是小于等于 char 所占用的空间。

union {
    char bits;
} _infoUnit;

但是增加下面的结构体,是为了增加代码的可读性,内存无负向影响。

说明:

  • 联合体的所有成员 共享同一块内存空间,其大小由最大的成员决定。

  • char 类型的 bits 和匿名结构体 共享同一块内存1个字节大小的空间

  • struct 里面的 tall 第 0 位rich 第 1 位handsome 第 2 位。这样3 个成员总共只占用 3 位,而 1 字节有 8 位,因此可以完全容纳

  • 本质是共享内存:联合体的 bits 和结构体是同一块内存的不同“解释方式”。

    • 通过 bits,你可以直接读写整个字节。
    • 通过结构体位域,你可以单独操作某一位。

    位域的紧凑存储3 个 char :1 成员仅占用 3 位,而 1 字节有 8 位,因此足够存储。

位运算设计 API

系统很多 API 都有位或运算。比如 KVO 中的 options可以传递 NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ,那么系统是如何知道到底传递了哪几个值?搞清楚这个问题,我们就也可以设计位运算这样的 API。

先看看:按位或运算

0b0000 0001 // 1
0b0000 0010 // 2 
0b0000 0100 // 4
------------
0b0000 0111 // 7

可以看到上面3个数按位或之后的结果为 0b0000 0111

按位与运算。

0b0000 0111
0b0000 0001
-----------
0b0000 0001

0b0000 0111
0b0000 0010
-----------
0b0000 0010

0b0000 0111
0b0000 0100
-----------
0b0000 0100

0b0000 0111
0b0000 1000
-----------
0b0000 0000

我们发现上面3个数按位或之后的结果,再分别与每个数按位与,得到的结果就可以还原数据本身

也就是:

  • 方法参数可以传递各个枚举值按位或的结果
  • 方法内部再拿这个参数,分别与各个枚举值按位与,得到的结果如果是 YES则说明参数中传递了该枚举值

与一个不是3个数之一的数按位与得到的结果为0b0000 0000。利用这个特性我们可以判断传递来的参数是不是包含了某个值

有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。

Runtime 源码阅读

id 的本质

id 在 Objective-C 的运行时头文件(<objc/objc.h>)中被定义为指向 struct objc_object 的指针:

typedef struct objc_object *id;

objc_object 的核心结构是什么?

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
 		// ... 
}

isa 指针:所有 Objective-C 对象的内存起始位置都是一个 isa 指针,指向该对象的类(Class)。

注意Arm64 之后isa 变成了 union isa_t里面存储了更多信息

isa 本质

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

  • 在 arm64 之后,对 isa 进行了优化变成了一个共用体union结构还使用了位域技术来存储更多的信息位运算

union isa_t {
  	// ...
    uintptr_t bits;
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
	  // ...
};

跳转到 isa.h 中查看 isa 里结构体的定义arm64 系统为例,源码是 objc4-838.1 版本笔者将无用的代码删除了isa 的 union 内容如下:

union isa_t {
    uintptr_t bits;
  	struct {
        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
    };
};

struct 内部的成员变量可以指定占用内存位数, uintptr_t nonpointer : 1 代表占用1个字节是结构体里面的 位域

  • nonpointer0代表普通的指针存储着 class、meta-class 对象的内存地址1代表优化过使用位域存储更多的信息

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

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

    • has_cxx_dtor = 1 时,表示该 Objective-C 对象 持有 C++ 成员变量或继承自 C++ 类,需要在对象释放时调用 C++ 析构函数

    • Objective-C 的 dealloc 方法在释放对象内存前,会检查 has_cxx_dtor 标志位。若为 1,则调用 object_cxxDestruct 函数执行 C++ 析构逻辑

  • shiftcls存储着 class、meta-class 对象的内存地址信息

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

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

  • unuseddeallocating对象是否正在释放

  • extra_rc里面存储的值是引用计数器减 1刚创建出的对象查看这个信息位 0因为存储着 -1 之后的引用计数)

  • has_sidetable_rc引用计数器是否过大无法存储在 isa 中如果为1那么引用计数会存储在一个叫 SideTable 的类的属性中

上面说的「如果没有,释放时会更快」,是如何得出结论的?

查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
      	// 存在析构函数则执行析构函数
        if (cxx) object_cxxDestruct(obj);
      	// 存在关联对象,则移除关联对象
        if (assoc) _object_remove_assocations(obj);
      	// 执行销毁逻辑
        obj->clearDeallocating();
    }

    return obj;
}

isa 在 arm64 之后必须通过 ISA_MASK 去查询 class类对象、元类对象 真正的地址

0x0000000ffffffff8ULL 用程序员模式打开计算器

其中,结构体中的数据存放大体是下面的结构:

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

知道结构体可以指定存储大小这个功能后,可以看到 isa_t 联合体与 ISA_MASK 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)

如果要找出下面中间的 1010 如何实现按位与即可且要找的位置补充位1其他位置为0

0b0010 1000

0b0011 1100
-----------
0b0010 1000

结论:根据按位与的效果。ISA_MASK 的后3位都是0所以经过 isa union 位运算后得到的类对象地址或者元类对象地址用二进制表示时最后3位一定为0

我们可以验证下

Person *p = [[Person alloc] init];
NSLog(@"%p", [p class]);    // 0x1000081d8
NSLog(@"%p", object_getClass([Person class])); // 0x100008200
NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28

为什么有的结尾是8因为 0x 是16进制。16进制的8转为二进制0x1000

关于这部分的调试,需要在真机上运行,真机上 arm64拷贝对象地址到系统自带的运算器程序员模式查看64位地址。按照下面的顺序一一查看

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

所以可以根据 isa 信息查看对象是否创建过关联对象、有没有设置弱引用、

QA如何理解 isa 指针?

isa 指针,在 arm64 之前isa 指针就是普通的对象,存储着类对象或元类对象的地址值。

从 arm64 开始,采用共用体 union 的形式共64位存储了很多信息其中33位用来存储 class、meta-class 对象的内存地址信息其他的31位用来存储其他信息比如 has_assoc 占1位其代表是否有设置过关联对象如果没有释放时会更快。

有类对象、为什么设计元类对象?

复用消息机制。比如 [Person new]。元类对象isa、元类方法。

objc_msgSend 设计初衷就是为了消息发送很快。假如没有元类,则类方法也存储在类对象的方法信息中,则可能需要加额外的字段来标记某个方法是类方法还是对象方法。遍历或者寻找会比较慢。所以引入元类(单一职责),设计元类的目的就是为了提高 objc_msgSend 的效率。

类对象 Class 的结构

查看 objc4 源代码看看

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
 		// ... 
}
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

结构体继承于 objc_object 等同于下面代码

struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用于获取具体的类信息
};
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    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;
};

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:

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

可以看到 objc_class 获取 bits 里的真实数据需要经过按位与 FAST_DATA_MASK

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; // instance 对象占用的内存空间
#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;
    }
};

具体关系整理如下图

源码解读:

  • 元类对象可以看成是特殊的类对象,数据类型都是 Class,所以大部分数据结构都一样,区别在于某些值是否有值

  • class_rw_t:里面的 methods、properties、protocols 是二维数组(其实不能叫二维数组,因为每个子列表 method_list_t 的长度可以不同,应该叫 方法列表的列表)其结构类似:method_array_t -> method_list_t -> method_t,是可读可写的,包含了类的初始内容、分类的内容。

    比如访问 method 的过程

    // 新版
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
        }
    }
    
  • class_ro_t 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容

  • 当系统运行 Runtime 会把类自身的信息class_ro_t 中的信息和 Category 中的信息合并起来,放到 class_rw_t 中的信息去

    源码 objc-runtime-new.mm 如下

    static Class realizeClassWithoutSwift(Class cls, Class previously)
    {
    		// ...
        class_rw_t *rw;
        Class supercls;
        Class metacls;
       // ...
    
        // fixme verify class is not in an un-dlopened part of the shared cache?
    
        auto ro = cls->safe_ro(); // 获取编译期确定的只读元数据class_ro_t
        auto isMeta = ro->flags & RO_META; // 判断是否为元类
        if (ro->flags & RO_FUTURE) {
            // This was a future class. rw data is already allocated.
    			// Future Class 已预分配 rw 数据。Future Class共享缓存shared cache中预生成但未完全实现的类。
            rw = cls->data();	// 直接获取已分配的 rw
            ro = cls->data()->ro();	// 更新 ro 指针
            ASSERT(!isMeta);
            cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);	// 更新标志位:从 RW_FUTURE 转换为 RW_REALIZED|RW_REALIZING。
        } else {
            // Normal class. Allocate writeable class data.
          	// 为普通类分配可读写数据
            rw = objc::zalloc<class_rw_t>();	// 分配归零内存
            rw->set_ro(ro);	// 绑定只读数据到 rw
            rw->flags = RW_REALIZED|RW_REALIZING|isMeta;	// 设置状态标志
            cls->setData(rw);	 // 将 rw 关联到类
        }
     		// ...   
        return cls;
    }
    

    总结:

    • 系统刚运行起来的时候 objc_class 结构体的 class_data_bits_t bits 的 data 指向的是 class_ro_t 的
    • 经过 Runtime 会把类的原始信息和 Category 信息Property、method、protocol进行整合创建 class_rw_t 结构体,把 class_ro_t 复制给 class_rw_t 的 ro 成员变量。
    • 最后把 objc_class 结构体的 class_data_bits_t bits 的 data 指向的是 class_rw_t
    // 编译期
    objc_class.bits.data()  class_ro_t
    
    // 运行时初始化后
    objc_class.bits.data()  class_rw_t
    class_rw_t.ro  class_ro_t
    

    阐述下类的数据存储演进流程

    阶段 1编译期class_ro_t

    • class_ro_tRead Only
      • 内容:由编译器生成,包含类名、父类指针、实例大小、基础方法列表(baseMethodList)、属性列表(baseProperties)、协议列表(baseProtocols)等。
      • 内存位置:存储在 Mach-O 文件的 __DATA __objc_const 段中。
      • 访问方式:通过 objc_class->bits.data() 直接访问。

    阶段 2运行时初始化class_rw_t

    • 触发时机

      • 首次调用 [MyClass alloc]objc_getClass("MyClass") 时触发类的 realize
      • 运行时通过 realizeClassWithoutSwift() 函数完成初始化。
    • 核心操作

      1. 分配 class_rw_t:动态创建可读写结构体。
      2. 绑定 class_ro_t:将 rw->ro 指向编译期的 class_ro_t
      3. 更新指针:将 objc_class->bits.data() 指向 class_rw_t
      4. 合并 Category附加所有分类Category的方法、属性、协议到 class_rw_t
    • 内存变化

      // 编译期
      objc_class.bits.data() → class_ro_t
      
      // 运行时初始化后
      objc_class.bits.data() → class_rw_t
      class_rw_t.ro → class_ro_t
      

    class_rw_tclass_ro_t 的核心区别

    特性 class_ro_t(只读) class_rw_t(读写)
    生成时机 编译期生成 运行时动态分配
    内存位置 Mach-O 文件数据段 堆内存
    内容可变性 不可修改 可动态添加方法、属性、协议
    包含数据 基础方法、属性、协议 基础数据 + 分类数据 + 动态扩展数据
    性能优化 无锁访问 需要线程安全措施(如锁)

    Category 的合并逻辑

    • 附加到 class_rw_t

      • 方法:分类的方法会被插入到 class_rw_t.methods 数组的头部,因此分类方法优先于原类方法被调用。
      • 属性分类的属性通过关联对象Associated Object实现不会真正添加到 class_rw_t.properties
      • 协议:分类的协议会被合并到 class_rw_t.protocols 列表中。
    • 源码关键路径

      // objc-runtime-new.mm
      static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count) {
          // 遍历所有分类,合并方法、属性、协议到 class_rw_t
          for (uint32_t i = 0; i < cats_count; i++) {
              auto& entry = cats_list[i];
              method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
              if (mlist) {
                  rw->methods.attachLists(&mlist, 1); // 方法合并
              }
              // 类似处理属性和协议
          }
      }
      

    设计意义

    • 内存优化
      • 未初始化的类保持 class_ro_t 轻量级存储,减少内存占用。
      • 按需分配 class_rw_t,仅在类首次使用时初始化。
    • 动态性支持
      • 运行时可通过 class_addMethod() 或分类动态扩展方法。
      • 支持方法交换Method Swizzling等高级特性。
    • 性能权衡
      • 方法查找需遍历 class_rw_t.methods 的多个方法列表(原类 + 所有分类)。
      • 引入 class_rw_ext_t 进一步优化(将常用数据如方法缓存分离)

QA

  1. method_array_t 中存储了 method_list_tmethod_list_t 的元素为 method_list_t 为什么不直接称它为二维数组?

    严格来说,不是二维数组,只不过是 array 里添加的对象也是 array且各个数组不等长也不会补空。

  2. 为什么需要设计为这样的结构?

    调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的,普通方法走的是消息机制。首先判断是对象方法还是类方法,然后根据 isa 找类对象(对象方法)和元类对象(对象方法)信息中先从方法缓存中查找方法是否有缓存(方法缓存查找的过程是:先根据方法的 SELSEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t然后和方法缓存哈希表的 MASK 进行按位与MASK 值初始为 1<<INIT_CACHE_SIZE - 1;然后根据获得的方法缓存 key 去哈希表中查找,如果找到则返回方法缓存哈希表中的方法信息。没找到则继续执行 cache_next 方法)。没有找到再通过方法二维数组查找(二维是因为类存在分类,分类可能也有方法)没找到则通过 superclass 继续找父类对象或者父元类对象继续找,找到则执行并给当前类的 cache 方法散列表缓存下来,如果一直找到 NSObject 还是没找到,则开始走消息转发机制,起死回生几个阶段

    另外 load 调用会根据编译顺序决定如果遇到某个类存在父类则先调用父类的load、再执行子类的 load。category 会按照编译顺序runtime 会给方法进行重新组合顺序,源码显示最后 category 的方法会排到最前面。

    本类的 class method List 和 category methodList 都是 arraycategory List 会插在本类 method List 前面,匹配到 category method List 同名方法,本类就不调用了,看着有点像被"覆盖"了。

    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    };
    
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
    
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;
    
        cache_t *cache = getCache(cls);
        cache_key_t key = getKey(sel);
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {
            // Cache is read-only. Replace it.
          	// 此处传入的第二个参数为  INIT_CACHE_SIZE
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            // Cache is too full. Expand it.
            cache->expand();
        }
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        bucket_t *bucket = cache->find(key, receiver);
        if (bucket->key() == 0) cache->incrementOccupied();
        bucket->set(key, imp);
    }
    
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
    {
        bool freeOld = canBeFreed();
    
        bucket_t *oldBuckets = buckets();
        bucket_t *newBuckets = allocateBuckets(newCapacity);
    
        // Cache's old contents are not propagated. 
        // This is thought to save cache memory at the cost of extra cache fills.
        // fixme re-measure this
    
        assert(newCapacity > 0);
        assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    		// 设置默认的 Mask,为 1<<INIT_CACHE_SIZE - 11<<2 - 1 = 3
        setBucketsAndMask(newBuckets, newCapacity - 1);
    
        if (freeOld) {
            cache_collect_free(oldBuckets, oldCapacity);
            cache_collect(false);
        }
    }
    

    方法通过 superclass 从父类类对象、父类元类对象中查找方法,找到后则将方法实现缓存到当前类的方法缓存中:

    • 根据方法的 SELSEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t然后和方法缓存哈希表的 MASK 进行按位与MASK 值初始为 1<<INIT_CACHE_SIZE - 1

class_rw_t、class_ro_t、class_rw_ext_t 的区别

探索系统源码:

// objc-os.mm
// 系统入口
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    static bool firstTime = YES;
    header_info *hList[mhCount];
    uint32_t hCount;
    size_t selrefCount = 0;

    // Perform first-time initialization if necessary.
    // This function is called before ordinary library initializers. 
    // fixme defer initialization until an objc-using image is found?
    if (firstTime) {
        preopt_init();
    }

    if (PrintImages) {
        _objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount);
    }


    // Find all images with Objective-C metadata.
    hCount = 0;

    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    {
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[i];

            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                // no objc data in this entry
                continue;
            }
            
            if (mhdr->filetype == MH_EXECUTE) {
                // Size some data structures based on main executable's size
#if __OBJC2__
                // If dyld3 optimized the main executable, then there shouldn't
                // be any selrefs needed in the dynamic map so we can just init
                // to a 0 sized map
                if ( !hi->hasPreoptimizedSelectors() ) {
                  size_t count;
                  _getObjc2SelectorRefs(hi, &count);
                  selrefCount += count;
                  _getObjc2MessageRefs(hi, &count);
                  selrefCount += count;
                }
#else
                _getObjcSelectorRefs(hi, &selrefCount);
#endif
                
#if SUPPORT_GC_COMPAT
                // Halt if this is a GC app.
                if (shouldRejectGCApp(hi)) {
                    _objc_fatal_with_reason
                        (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                         OS_REASON_FLAG_CONSISTENT_FAILURE, 
                         "Objective-C garbage collection " 
                         "is no longer supported.");
                }
#endif
            }
            
            hList[hCount++] = hi;
            
            if (PrintImages) {
                _objc_inform("IMAGES: loading image for %s%s%s%s%s\n", 
                             hi->fname(),
                             mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
                             hi->info()->isReplacement() ? " (replacement)" : "",
                             hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
                             hi->info()->optimizedByDyld()?" (preoptimized)":"");
            }
        }
    }

    // Perform one-time runtime initialization that must be deferred until 
    // the executable itself is found. This needs to be done before 
    // further initialization.
    // (The executable may not be present in this infoList if the 
    // executable does not contain Objective-C code but Objective-C 
    // is dynamically loaded later.
    if (firstTime) {
        sel_init(selrefCount);
        arr_init();

#if SUPPORT_GC_COMPAT
        // Reject any GC images linked to the main executable.
        // We already rejected the app itself above.
        // Images loaded after launch will be rejected by dyld.

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE  &&  shouldRejectGCImage(mh)) {
                _objc_fatal_with_reason
                    (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                     OS_REASON_FLAG_CONSISTENT_FAILURE, 
                     "%s requires Objective-C garbage collection "
                     "which is no longer supported.", hi->fname());
            }
        }
#endif

#if TARGET_OS_OSX
        // Disable +initialize fork safety if the app is too old (< 10.13).
        // Disable +initialize fork safety if the app has a
        //   __DATA,__objc_fork_ok section.

//        if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) {
//            DisableInitializeForkSafety = true;
//            if (PrintInitializing) {
//                _objc_inform("INITIALIZE: disabling +initialize fork "
//                             "safety enforcement because the app is "
//                             "too old.)");
//            }
//        }

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE) continue;
            unsigned long size;
            if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
                DisableInitializeForkSafety = true;
                if (PrintInitializing) {
                    _objc_inform("INITIALIZE: disabling +initialize fork "
                                 "safety enforcement because the app has "
                                 "a __DATA,__objc_fork_ok section");
                }
            }
            break;  // assume only one MH_EXECUTE image
        }
#endif

    }

    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
    
    // Call image load funcs after everything is set up.
    for (auto func : loadImageFuncs) {
        for (uint32_t i = 0; i < mhCount; i++) {
            func(mhdrs[i]);
        }
    }
}

最后会调用

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    bool launchTime = NO;
    TimeLogger ts(PrintImageTimes);

    runtimeLock.assertLocked();

#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

    if (!doneOnce) {
        doneOnce = YES;
        launchTime = YES;

#if SUPPORT_NONPOINTER_ISA
        // Disable non-pointer isa under some conditions.

# if SUPPORT_INDEXED_ISA
        // Disable nonpointer isa if any image contains old Swift code
        for (EACH_HEADER) {
            if (hi->info()->containsSwift()  &&
                hi->info()->swiftUnstableVersion() < objc_image_info::SwiftVersion3)
            {
                DisableNonpointerIsa = true;
                if (PrintRawIsa) {
                    _objc_inform("RAW ISA: disabling non-pointer isa because "
                                 "the app or a framework contains Swift code "
                                 "older than Swift 3.0");
                }
                break;
            }
        }
# endif

# if TARGET_OS_OSX
        // Disable non-pointer isa if the app is too old
        // (linked before OS X 10.11)
        // Note: we must check for macOS, because Catalyst and Almond apps
        // return false for a Mac SDK check! rdar://78225780
//        if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
//            DisableNonpointerIsa = true;
//            if (PrintRawIsa) {
//                _objc_inform("RAW ISA: disabling non-pointer isa because "
//                             "the app is too old.");
//            }
//        }

        // Disable non-pointer isa if the app has a __DATA,__objc_rawisa section
        // New apps that load old extensions may need this.
        for (EACH_HEADER) {
            if (hi->mhdr()->filetype != MH_EXECUTE) continue;
            unsigned long size;
            if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
                DisableNonpointerIsa = true;
                if (PrintRawIsa) {
                    _objc_inform("RAW ISA: disabling non-pointer isa because "
                                 "the app has a __DATA,__objc_rawisa section");
                }
            }
            break;  // assume only one MH_EXECUTE image
        }
# endif

#endif

        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        
        initializeTaggedPointerObfuscator();

        if (PrintConnecting) {
            _objc_inform("CLASS: found %d classes during launch", totalClasses);
        }

        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

        ts.log("IMAGE TIMES: first time tasks");
    }

    // Fix up @selector references
    // Note this has to be before anyone uses a method list, as relative method
    // lists point to selRefs, and assume they are already fixed up (uniqued).
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;

            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }

    ts.log("IMAGE TIMES: fix up selector references");

    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        classref_t const *classlist = _getObjc2ClassList(hi, &count);

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs 
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }

    ts.log("IMAGE TIMES: discover classes");

    // Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    
    if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
            // fixme why doesn't test future1 catch the absence of this?
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
        }
    }

    ts.log("IMAGE TIMES: remap classes");

#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
            fixupMessageRef(refs+i);
        }
    }

    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif


    // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        ASSERT(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->hasPreoptimizedProtocols();

        // Skip reading protocols if this is an image from the shared cache
        // and we support roots
        // Note, after launch we do need to walk the protocol as the protocol
        // in the shared cache is marked with isCanonical() and that may not
        // be true if some non-shared cache binary was chosen as the canonical
        // definition
        if (launchTime && isPreoptimized) {
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
                             hi->fname());
            }
            continue;
        }

        bool isBundle = hi->isBundle();

        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

    ts.log("IMAGE TIMES: discover protocols");

    // Fix up @protocol references
    // Preoptimized images may have the right 
    // answer already but we don't know for sure.
    for (EACH_HEADER) {
        // At launch time, we know preoptimized image refs are pointing at the
        // shared cache definition of a protocol.  We can skip the check on
        // launch, but have to visit @protocol refs for shared cache images
        // loaded later.
        if (launchTime && hi->isPreoptimized())
            continue;
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
        }
    }

    ts.log("IMAGE TIMES: fix up @protocol references");

    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

    ts.log("IMAGE TIMES: discover categories");

    // Category discovery MUST BE Late to avoid potential races
    // when other threads call the new category code before
    // this thread finishes its fixups.

    // +load handled by prepare_load_methods()

    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t const *classlist = hi->nlclslist(&count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            addClassTableEntry(cls);

            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
                // fixme also disallow relocatable classes
                // We can't disallow all Swift classes because of
                // classes like Swift.__EmptyArrayStorage
            }
            realizeClassWithoutSwift(cls, nil);
        }
    }

    ts.log("IMAGE TIMES: realize non-lazy classes");

    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            Class cls = resolvedFutureClasses[i];
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class is not allowed to be future");
            }
            realizeClassWithoutSwift(cls, nil);
            cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }

    ts.log("IMAGE TIMES: realize future classes");

    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }


    // Print preoptimization statistics
    if (PrintPreopt) {
        static unsigned int PreoptTotalMethodLists;
        static unsigned int PreoptOptimizedMethodLists;
        static unsigned int PreoptTotalClasses;
        static unsigned int PreoptOptimizedClasses;

        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) {
                _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors "
                             "in %s", hi->fname());
            }
            else if (hi->info()->optimizedByDyld()) {
                _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors "
                             "in %s", hi->fname());
            }

            classref_t const *classlist = _getObjc2ClassList(hi, &count);
            for (i = 0; i < count; i++) {
                Class cls = remapClass(classlist[i]);
                if (!cls) continue;

                PreoptTotalClasses++;
                if (hi->hasPreoptimizedClasses()) {
                    PreoptOptimizedClasses++;
                }
                
                const method_list_t *mlist;
                if ((mlist = cls->bits.safe_ro()->baseMethods)) {
                    PreoptTotalMethodLists++;
                    if (mlist->isFixedUp()) {
                        PreoptOptimizedMethodLists++;
                    }
                }
                if ((mlist = cls->ISA()->bits.safe_ro()->baseMethods)) {
                    PreoptTotalMethodLists++;
                    if (mlist->isFixedUp()) {
                        PreoptOptimizedMethodLists++;
                    }
                }
            }
        }

        _objc_inform("PREOPTIMIZATION: %zu selector references not "
                     "pre-optimized", UnfixedSelectors);
        _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted",
                     PreoptOptimizedMethodLists, PreoptTotalMethodLists, 
                     PreoptTotalMethodLists
                     ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists 
                     : 0.0);
        _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered",
                     PreoptOptimizedClasses, PreoptTotalClasses, 
                     PreoptTotalClasses 
                     ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses
                     : 0.0);
        _objc_inform("PREOPTIMIZATION: %zu protocol references not "
                     "pre-optimized", UnfixedProtocolReferences);
    }

#undef EACH_HEADER
}

核心相关代码

/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
  	// ...
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
    cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
		// ...
}

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

查看 objc4 源码runtime 启动步骤可以知道:

  • struct objc_class 结构体中的 bits 一开始指向的是 class_ro_t 结构体,然后经过 runtime 的 realizeClassWithoutSwift 方法,将 bits 指向一个新创建的 class_rw_t 结构体,新创建的结构体中的成员变量 ro 指向类原本的 class_ro_t。也就是说 runtime 会对类自身信息和 Category 信息进行组合。
  • class_ro_t 在编译时期生成的,class_rw_t 是在运行时期生成的。

那么什么是 class_rw_ext_t首先明确 2。个概念

  • clean memory加载后不会被修改。当系统内存紧张时可以从内存中移除需要时可以再次加载

  • dirty memory加载后会被修改一直处于内存中

runtime 初始化的时候,遇到一个类,则会利用类的 class_ro_t 中的基础信息methods、properties、protocols来创建 class_rw_t 对象。class_rw_t 设计的目的就是为了 runtime 所需Category 增加属性、协议、动态增加方法等),但是实际上写了很多的类,只有少部分类才需要 runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 class_rw_t 拆分出 class_rw_ext_t,用来存储 Methods、Protocols、Properties 信息,只有在使用的时候才创建,节省更多内存。

Runtime - 方法

Method_t

method_t 是对方法、函数的封装

struct method_t {
  	// ...
    struct big {
        SEL name;	// 函数名、方法名    
        const char *types; // 编码(返回值类型、参数类型)
        MethodListIMP imp; // 指向函数的指针(函数地址,给方法下断点的话,汇编模式的第一条指令的地址就是函数地址)
    };
    struct small {
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP, /*isNullable*/false> imp;
    };
	  // ...
}

IMP 代表函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

SEL 代表方法、函数名,一般叫做选择器,底层结构跟 char * 类似。本质上是一个指向方法名 C 字符串的指针。

typedef struct objc_selector *SEL;
  • 可以通过 @selector()sel_registerName() 获得

  • 可以通过 sel_getName()NSStringFromSelector() 转成字符串

  • 不同类中相同名字的方法,所对应的方法选择器是相同的。也就是 SEL 不具备唯一性,方法命名需要规范,否则 runtime 调用起来就会发生不符合预期的行为。

types 包含了函数返回值、参数编码的字符串。返回值|参数1|参数2| ... | 参数n

Type Encoding

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

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

可以对照下面的表格进行查看:

- (int)calcuate:(int)baseHeight heigith:(float)height;

比如这个方法的 type encodingi24@0:8i16f20

解读下上面的方法其实携带了2个基础参数(id)self _cmd:(SEL)_cmd :

  • i 代表方法返回值为 int

  • 24 代表参数共占24个字节大小。4个参数分别为 id 类型的 selfSEL 类型的 _cmd int 类型的 age、float 类型的 height。8+8+4+4 共24个字节id、SEL 都为指针长度为8

  • @ 代表第一个参数为 id 类型从第0个字节开始即 self

  • :代表第二个参数为 SEL从第8个字节开始

  • i 代表第三个参数为 int从第16个字节开始占4个字节

  • f 代表第四个参数为 float从第20个字节开始

方法缓存

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

  • 用于快速查找方法执行函数
  • 是可增量拓展的哈希表结构
  • 局部性原理的最佳应用一般调用方法的时候并不会每个方法都调用一般来说可能会调用某个类的某几个方法这几个方法每调用过1次就缓存起来下次再去调用的时候就省去了 runtime 中方法查找的流程,提高效率)
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    // Class ISA;
  	// ...
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
}

调用方法的本质:

  • 比如说对象方法,先根据对象的 isa 找到类对象,在 arm64 下isa 是非指针的 union利用64位中的不同位存储不同信息比如用33位来存储类对象地址、元类对象地址信。用 ISA_MASK 来提取类对象的真实地址
  • 在类对象的 method_list_t 类型的 methods 方法数组Array 中的元素是方法 Array类的Category1、类的 Category2... 类自身的方法)查找方法
  • 找不到则调用 superclass 查找父类的 methods 方法数组Array 中的元素是方法 Array看是否存在方法。找不到则继续在当前类的 superclass 继续向上查找...
  • 假设某个类的方法调用 [Worker live],需要通过 superclass 查找8次每次都是在方法的“二维数组”里遍历。某个逻辑就需要调用 [Worker live]6次每次需要8次二维数组的遍历6*8= 48次可想而知效率低下。

所以为了方便,给类设置了方法缓存。比如调用 Worker 对象的 live 方法,通过 isa 找到元类对象,元类对象中不存在,则通过 superclass 找父类的元类对象, 不断找假设经历了8次 superclass最后在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Worker 的 cache_t 类型的 cache 中。

Cache 完整流程

  1. 通过实例的 isa 指针找到类对象

    实例方法存储在类对象中,类方法存储在元类中。方法调用时,首先通过实例的 isa 指针定位到类对象。

  2. 找类对象的 cache

    • 检查类对象的方法缓存 cache哈希表结构若找到方法实现IMP直接调用并结束流程。

    • 缓存目的:哈希表查找时间复杂度为 O(1),避免频繁方法查找的性能损耗。

  3. 查找类对象的 method_list_t 方法列表

    • cache 未命中,遍历类对象的 method_list_t(方法列表)。

    • 方法列表结构

      • 由多个方法数组构成,顺序为:后加载的 Category 方法在前,类自身方法在后例如Category2 → Category1 → 类原始方法)。
      • 这是 Runtime 在启动时合并 class_ro_t(只读数据)到 class_rw_t(可读写数据)时确定的,后编译的 Category 方法会插入到列表前端,覆盖同名方法。
    • 命中缓存:若在方法列表中找到方法,将其缓存到当前类对象的 cache 中,随后调用方法。

  4. 沿继承链向父类逐级查找

    若当前类未找到方法,通过 superclass 指针查找父类,重复以下步骤:

    • 查找父类 cache:若父类 cache 命中,将方法缓存到最初类对象的 cache(即子类的 cache),调用方法
    • 如果 cache 中 没有找到,则查找父类 method_list_t:若父类方法列表命中,同样缓存到子类的 cache,调用方法
    • 递归查找:若未找到,继续向更上层父类查找...
    • 直到根类(NSObject),如果 NSObject 的 cache 中找到方法缓存则执行方法,且把方法在当前的子类的 cache 中缓存一份。如果没找到则继续在 method_list_t 中查找,找到也继续在当前的子类 cache 中缓存一份
  5. 若当前类未找到方法,通过 superclass 指针查找父类,重复上述步骤:,未找到方法时触发消息转发若继承链全部查找未果,进入动态消息解析流程:

    • 动态方法解析(+resolveInstanceMethod:+resolveClassMethod:
    • 快速消息转发(-forwardingTargetForSelector:):将消息转发给其他对象处理。
    • 慢速消息转发(-methodSignatureForSelector:-forwardInvocation:):创建 NSInvocation 对象,可修改参数、目标、方法等
    • 如果还没处理,则报标准的 “unrecognized selector sent to instance” 错误,导致程序崩溃。

源码剖析

为什么说空间换时间?

假设 Person 类存在 test1、test2 方法。经过运算后 test1 需要存储在 bucket_t 数组的第8个位置test2 存储在第5个位置其他的位置都是 NULL。也就是预留了一些闲置的空间。存储的时候都会经过 hash 运算。效率会很高。空间换时间的实现。

struct cache_t {
    struct bucket_t *_buckets; // 散列表 -> | bucket_t |bucket_t |bucket_t |bucket_t |...
    mask_t _mask;              // 散列表的长度 -1
    mask_t _occupied;          // 已经缓存的方法数量
}
struct bucket_t {
    cache_key_t _key;    // SEL 作为 key
    IMP _imp;            // 函数的内存地址
}

方法缓存查找原理,就是利用散列表(哈希表)查找。涉及:空间换时间,哈希表拓容策略,哈希碰撞算法

objc4 源码 objc-cache.mm

typedef uintptr_t cache_key_t;

// 根据 SEL 计算方法缓存 key 就是根据 SEL方法名 C 字符串的指针转换为一个用于存储指针的整数值这个类型是一个无符号整数类型uintptr_t 提供了一种方式来将指针转换为一个整数,以及将整数转换回指针)
cache_key_t getKey(SEL sel) 
{
    assert(sel);
    return (cache_key_t)sel;
}

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();
		
  	// 避免在类完成初始化之前,修改方法缓存
    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
          // 找到合适的位置,插入新的 method_t
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
	          // SEL 已经存在,被其他线程加入
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
      // while 条件满足一次,则尝试插入一次。没有插入成功,则继续尝试 while意味着发生了哈希冲突开放定址法线性尝试 index
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

void cache_t::copyCacheNolock(objc_imp_cache_entry *buffer, int len)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif
    int wpos = 0;

#if CONFIG_USE_PREOPT_CACHES
    if (isConstantOptimizedCache()) {
        auto cache = preopt_cache();
        auto mask = cache->mask;
        uintptr_t sel_base = objc_opt_offsets[OBJC_OPT_METHODNAME_START];
        uintptr_t imp_base = (uintptr_t)&cache->entries;

        for (uintptr_t index = 0; index <= mask && wpos < len; index++) {
            auto &ent = cache->entries[index];
            if (~ent.sel_offs) {
                buffer[wpos].sel = (SEL)(sel_base + ent.sel_offs);
                buffer[wpos].imp = (IMP)(imp_base - ent.imp_offset());
                wpos++;
            }
        }
        return;
    }
#endif
    {
        bucket_t *buckets = this->buckets();
        uintptr_t count = capacity();

        for (uintptr_t index = 0; index < count && wpos < len; index++) {
            if (buckets[index].sel()) {
                buffer[wpos].imp = buckets[index].imp(buckets, cls());
                buffer[wpos].sel = buckets[index].sel();
                wpos++;
            }
        }
    }
}

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);	// 该方法实现就是 `key & mask`,按位与来计算哈希索引
    mask_t i = begin;
    do {
      	// 从 buckets 里的 beign 位置开始寻找方法缓存,如果找到则返回 buckets[i]
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
      // while 的一个为true则代表初始化找到的哈希索引位置没有找到意味着发生了哈希冲突则用开放定址法去查找下一个可能的位置。调用的方法是 cache_next
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}


// 哈希冲突的时候,调用 cache_next 方法来寻找合适的 index。是标准的开放定址法
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
#else

可以看到在查找方法缓存的时候:

  • 首先根据缓存 key利用 cache_hash 方法计算出一个 begin赋值给 i

  • 然后利用 i 在方法缓存 cache_tbuckets 中查找,假设 key 相等,则直接返回对应的方法

  • 如果没有找到则执行 cache_next 方法,该方法则会判断 i 是不是等于0不等于0则自减1等于0则设置为 mask 的值

  • mask 值,在设计上等于 buckets 散列表的长度减1。

    1. 任何一个值,与一个 mask 按位与之后的结果,一定小于等于 mask 的值。比如:

      1. 按位与之后,最大的等于自己的值本身
      	0x 1000 1001
      & 0x 1111 1111
      -----------------
      	0x 1000 10001
      
      
      2. 按位与之后,结果小于自己的值本身
      	0x 1111 0001
      & 0x 0000 0001	
      -----------------
      	0x 0000 0001
      
      	0x 1111 0001
      & 0x 0000 1111	
      -----------------
      	0x 0000 1111	
      
    2. 所以哈希函数设计位:方法选择器 SEL 的指针地址(uintptr_t 类型)作为 key_mask 按位与的结果。其中 _mask 为当前散列表的长度减1

散列表长度不够了,则会哈希拓容(),此时之前存储的方法缓存则会被释放,执行 cache_collect_free

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
  	// 扩容容量变为原来的2倍大小
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    setBucketsAndMask(newBuckets, newCapacity - 1);
    if (freeOld) {
      	// 释放之前的方法缓存
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

哈希查找元素核心是一个求 key 的过程Java 中是求余iOS 中是按位与 key & mask

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

空间换时间的一个实现。且按位与的特点,(key & mask) 的结果一定比 mask 值小。

模拟方法缓存工作流程

举个例子,来直观了解下 cache 的工作流程:

假设初始缓存容量为 4_mask = 3),逐步插入 test1test2test3test4 方法后触发扩容,最终容量变为 8_mask = 7)。

  1. 初始状态散列表容量为4_mask =3_occupied = 0目前没有方法写入缓存。散列桶数组为空

    buckets[0]: null
    buckets[1]: null
    buckets[2]: null
    buckets[3]: null
    
  2. 不断插入方法:

    • 假设 test1 的 sel 的 uintptr_t 为5计算哈希索引 5 & 3 = 1

      	0b 0000 0101
      & 0b 0000 0011
      ---------------
        0b 0000 0001
      

      所以插入索引为1的位置。buckets[1] = test1 _occupied= 1

      buckets[0]: null
      buckets[1]: test1
      buckets[2]: null
      buckets[3]: null
      
    • 假设 test2 的 sel 的 uintptr_t 为7计算哈希索引 7 & 3 = 3。所以插入索引为3的位置。buckets[3] = test2 _occupied= 2

      buckets[0]: null
      buckets[1]: test1
      buckets[2]: null
      buckets[3]: test2
      
    • 假设 test3 的 sel 的 uintptr_t 为9计算哈希索引 9 & 3 = 1发现位置1被占用了此时采用开放定址法寻找合适的位置也就是哈希冲突的解法。比如先从1开始尝试index = 2发现没有被占用则开放定址的过程终止则将 test3 放到位置2上

      buckets[0]: null
      buckets[1]: test1
      buckets[2]: test3
      buckets[3]: test2
      

      假设哈希函数为: index = Hash(key) mod _mask

      补充说明:针对哈希冲突的开放定址法一般有:线性探测法、二次探测法、伪随机探测法

      • 线性探测法就是:index = (Hash(key) + di) mod _mask,其中 di 为1, 2, 3, ... _mask - 1 构成的线性序列
      • 二次探测法就是:index = (Hash(key) + di) mod _mask,其中 di 为1^2, 2^2, 3^2, ...直到平方数小于等于 _mask - 1 的数,构成的二次序列
      • 伪随机数测法就是:index = (Hash(key) + di) mod _mask,其中 di 为伪随机数,构成的二次序列

      所以插入索引为3的位置。buckets[3] = test2 _occupied= 2

    • 假设 test4 的 sel uintptr_t 为11计算哈希索引 11 & 3 = 3发现位置3被占用了此时采用开放定址法寻找合适的位置也就是哈希冲突的解法。比如先从1开始尝试index = 0发现没有被占用则将 test4 放到位置0上

      buckets[0]: test4
      buckets[1]: test1
      buckets[2]: test3
      buckets[3]: test2
      

      此时,_occupied = 4,此时 _occupied > 3/4 * capacity3/4 * 4 = 3触发扩容

  3. 扩容与哈希重建

    新容量capacity = 8, _mask = 7

    创建新桶

    buckets[0]: null
    buckets[1]: null
    buckets[2]: null
    buckets[3]: null
    buckets[4]: null
    buckets[5]: null
    buckets[6]: null
    buckets[7]: null
    

    安装上述哈希方法,重新计算位置并保存到桶中

    方法 旧索引 新索引计算(哈希值 & 7
    test1 1 5&7 = 5
    test2 3 7&7 =7
    test3 2 9 & 7 = 1
    Test4 0 11 & 7 = 3

    桶的最新状态

    buckets[0]: null
    buckets[1]: test3
    buckets[2]: null
    buckets[3]: test4
    buckets[4]: null
    buckets[5]: test1
    buckets[6]: null
    buckets[7]: test2
    

    更新缓存结构:

    • _buckets 指向新的桶数组
    • _mask = 7
    • _occupied = 4
  4. 读取方法流程

    • 从缓存中查找 test1 方法,假设 test1 的 sel 的 uintptr_t 为5计算哈希索引 5 & 7 = 5,则返回桶中 buckets[5] 的 method_t
    • 从缓存中查找 test2 方法,假设 test2 的 sel 的 uintptr_t 为7计算哈希索引 7 & 7 = 7,则返回桶中 buckets[7] 的 method_t
    • 从缓存中查找 test3 方法。假设 test3 的 sel 的 uintptr_t 为9计算哈希索引 9 & 7 = 1,则返回桶中 buckets[1] 的 method_t
    • 从缓存中查找 test4 方法,假设 test4 的 sel 的 uintptr_t 为11计算哈希索引 11 & 7 = 3,则返回桶中 buckets[3] 的 method_t

结论:

  1. 存储位置动态变化:扩容后方法的存储位置会发生变化(如 test3 从旧索引 2 → 新索引 1但通过 重新哈希,所有方法被重新分配到新容量的正确位置。
  2. 查找逻辑一致性:无论缓存是否扩容,查找时始终使用 当前的 _mask 计算索引,确保能正确命中方法。
  3. 性能与空间的平衡:空间换时间
    • 扩容代价:重新哈希需要遍历旧桶,时间复杂度为 O(n),但扩容频率随容量指数级降低。
    • 查找效率:平均时间复杂度接近 O(1),即使位置变化也不影响性能。

设计哲学:

  • 以空间换时间:通过扩容减少哈希冲突,牺牲内存换取更快的查找速度。
  • 惰性扩容:仅在负载因子超过阈值时扩容,避免频繁内存分配(标准做法,没有哪个成熟方案是随便扩容的)
  • 幂等性保证:无论扩容多少次,方法的最终存储位置始终由 SEL & _mask 决定,确保逻辑正确。

通过这种设计Objective-C 的方法缓存在高频调用场景下依然能保持极速响应,同时动态适应方法数量的增长

实验:查找类的方法缓存 Demo

GoodStudent *goodStudent = [[GoodStudent alloc] init];
mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class];
[goodStudent goodStudentSay];	// 断点1	
[goodStudent studentSay];			// 断点2
[goodStudent personSay];			// 断点3
NSLog(@"");

流程:

断点1的地方可以看到 mock_objc_class 结构体 cache_occupied 为1_mask 为3初始化哈希表长度为4

在断点1的地方_occupied 为1则代表只有 init 方法被缓存,本行代码执行完,_occupied 为2.

在断点2的地方_occupied 为2则代表只有 init、goodStudentSay 方法被缓存。本行代码执行完,_occupied 为3

在断点3的地方_occupied 为3则代表只有 init 、goodStudentSay 、studentSay方法被缓存。本行代码执行完_occupied 为1_mask 为7。

奇了怪了,为什么 _occupied为1_mask 为7

因为哈希表长度为4缓存3个方法后到第4个方法需要缓存的时候会执行哈希表拓容缓存会失效。拓容策略为乘以2 即 uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 所以长度为8mask 为长度-1 则为7第4个方法刚好被缓存下来_occupied 为1。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

继续运行

在断点4的地方_occupied 为1则代表只有 personSay方法被缓存。本行代码执行完_occupied 为2_mask 为7。

在断点5的地方_occupied 为2则代表只有 personSay、goodStudentSay 方法被缓存。本行代码执行完,_occupied 为3_mask 为7。

在断点6的地方_occupied 为3则代表只有 personSay、goodStudentSay、studentSay 方法被缓存, _mask 为7。

如何根据方法散列表查找某个方法

所有的方法都在 buckets 数组里。所以问题转换为:如何根据方法计算出在 bucktes 中的索引?

本质上就是考察对于核心原理和源码的理解程度: bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache._mask]

GoodStudent *goodStudent = [[GoodStudent alloc] init];
mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class];
[goodStudent goodStudentSay];
[goodStudent studentSay];
[goodStudent personSay];

cache_t cache = personClass->cache;
bucket_t *buckets = cache._buckets;
//    for (int i = 0; i <= cache._mask; i++) {
//        bucket_t bucket = buckets[i];
//        NSLog(@"%s -- %p", (SEL)bucket._key, bucket._imp);
//    }
bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache._mask];
NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);

原理就是根据类对象结构体找到 cache 结构体cache 结构体内部的 _buckets 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 (key & mask) 找到哈希索引,然后找到方法对象 bucket其中寻找方法索引的 key 就是方法 selector。

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

注意:上述的模拟只是实现了系统哈希计算后查找 cache 的一半流程。还剩一半流程是:当哈希冲突的时候,计算出的 index 里存储的是另一个方法信息。此时就需要利用开放定植法去尝试合适的索引。

系统代码如下:

mask_t begin = cache_hash(k, m);	// 该方法实现就是 `key & mask`,按位与来计算哈希索引
mask_t i = begin;	// 初始先从该位置获取 cache 里的方法,做比较

// 如果不满足,则利用 do...while 实现的开放定址法寻找方法缓存
do {
    // 从 buckets 里的 beign 位置开始寻找方法缓存,如果找到则返回 buckets[i]
    if (b[i].key() == 0  ||  b[i].key() == k) {
        return &b[i];
    }
  // while 的一个为true则代表初始化找到的哈希索引位置没有找到意味着发生了哈希冲突则用开放定址法去查找下一个可能的位置。调用的方法是 cache_next
} while ((i = cache_next(i, m)) != begin);

[student live]当调用对象方法的时候。会根据对象的 isa 指针,找到类对象,然后在类对象的方法缓存中查找有没有方法实现

  • 如果找到了则立马执行
  • 如果缓存中没有方法实现,则会在 class_rw_t 的 methods 里面查找有没有方法实现
    • 如果找到了则将方法更新到方法缓存中去,然后立马执行(查找的过程则是根据 SEL 的方法名 C 字符串指针地址值与 _mask 按位与运算,然后在方法缓存的哈希表中查找 比如 cache.buckets[@selector(methodName) & _mask]
    • 如果此时还是找不到。则根据类对象的 superclass 指针查找父类的类对象。到父类的类对象这一步,也先从方法缓存中查找有没有方法缓存:
      • 如果找到了,则立马执行,同时将方法缓存到子类自己的缓存中去。下次调用,则直接在子类自己的方法缓存中查找即可
      • 如果没找到,则继续在 class_rw_t 的 methods 中查找方法实现。
        1. 如果找到了,则更新到子类类对象的方法缓存中,然后执行方法
        2. 如果没找到,则继续沿着父类类对象的 superclass 指针,继续往上找,查找流程和上面的步骤类似
  • 如果一直找到根类,还是找不到则开始走消息机制...

散列表的设计哲学

阅读了方法缓存相关的源码,可以看到 Cache 的实现就是一个散列表。因为底层源码需要保证高性能,所以采用时间换空间的策略。

利用散列表的散列函数,来实现快速计算存储和访问所需的 index。但是带来的一个问题是散列表可能会有一些闲置空间且散列表计算出来的位置可能会冲突所以需要哈希碰撞策略比如开放定址法和拉链法另外散列表容量快满的时候则需要哈希扩容。

有个压力位的设计:例如 Apple OC 对于方法缓存的压力位就是 _capacity * 3 / 4

  • 当方法缓存的 _occupied(已占用的槽位数)大于等于 _capacity * 3 / 4 时,就会触发扩容操作。
  • 种设计是为了在缓存的查找效率和存储空间之间取得平衡。当缓存的占用率达到 3/4 时,意味着缓存的使用较为频繁,并且继续添加新缓存可能会导致较多的哈希冲突,从而降低查找效率。此时进行扩容可以有效地减少哈希冲突,提高后续方法查找的速度。

针对哈希冲突的开放定址法一般有:线性探测法、二次探测法、伪随机探测法

  • 线性探测法就是:index = (Hash(key) + di) mod _mask,其中 di 为1, 2, 3, ... _mask - 1 构成的线性序列
  • 二次探测法就是:index = (Hash(key) + di) mod _mask,其中 di 为1^2, 2^2, 3^2, ...直到平方数小于等于 _mask - 1 的数,构成的二次序列
  • 伪随机数测法就是:index = (Hash(key) + di) mod _mask,其中 di 为伪随机数,构成的二次序列

Runtime - objc_msgSen

Person *p = [[Person alloc] init];
[p eat];
objc_msgSend(p, sel_registerName("eat"));

[Person sayHi];
objc_msgSend(object_getClass("Person"), sel_registerName("sayHi"));

oc 方法(对象方法、类方法)调用本质就是 objc_msgSend 方法。objc_msgSend 可以分为3个阶段

  • 消息发送

  • 动态方法解析

  • 消息转发

这3个阶段还是没调用成功则抛出错误unrecognized selector sent to instance 0x600000ad0260

查看源码 objc-msg-arm64.s

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
    // x0 寄存器代表消息接受者receiver。objc_msgSend(person, sel_registerName("eat")) 的 person
    cmp    x0, #0            // nil check and tagged pointer check
    // b 代表指令跳转。le 代表 小于等于。<=0则跳转到 LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr    x13, [x0]        // x13 = isa // ldr 代表加载指令。这里的意思是将 x0 寄存器信息写入到 x13中
    and    x16, x13, #ISA_MASK    // x16 = class    // 这里就是将 x13 与  ISA_MASK 按位与,然后得到真实的 isa 信息,然后写入到 x16 中
LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached // 这里执行 objc_msgSend_uncached 逻辑CacheLookup 是一个汇编宏,看下面的说明

LNilOrTagged:
    // 判断为 nil 则跳转到  LReturnZero
    b.eq    LReturnZero        // nil check

    // tagged
    mov    x10, #0xf000000000000000
    cmp    x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov    x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    // 汇编中 ret 代表 return
    ret 

    END_ENTRY _objc_msgSend


.macro CacheLookup // 汇编宏,可以看到根据 (SEL & mask) 来寻找真正的方法地址
    // x1 = SEL, x16 = isa
    ldp    x10, x11, [x16, #CACHE]    // x10 = buckets, x11 = occupied|mask
    and    w12, w1, w11        // x12 = _cmd & mask
    add    x12, x10, x12, LSL #4    // x12 = buckets + ((_cmd & mask)<<4)
		// 可以看到 cache.buckets[_cmd & mask]  == SEL 就是在做方法缓存判断逻辑,看看有没有命中方法缓存
    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // wrap: x12 = first bucket, w11 = mask
    add    x12, x12, w11, UXTW #4    // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp
		// cachehit 就是命中缓存,则调用或者 return imp
2:    // not hit: x12 = not-hit bucket
    // checkmiss就是缓存没命中也就是方法查找失败则走 checkMiss 逻辑,具体看下面
    CheckMiss $0            // miss if bucket->sel == 0NORMAL
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop
3:    // double wrap
    JumpMiss $0

.endmacro

// CheckMiss 汇编宏,上面传入了参数 Normal内部走 __objc_msgSend_uncached 流程。不同参数,走不同的方法调用逻辑
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz    x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz    x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz    x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


// __objc_msgSend_uncached 内部其实走  MethodTableLookup 逻辑
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search

MethodTableLookup
br    x17

END_ENTRY __objc_msgSend_uncached

// MethodTableLookup 是一个汇编宏,内部指令跳转到 __class_lookupMethodAndLoadCache3。
.macro MethodTableLookup

    // push frame
    stp    fp, lr, [sp, #-16]!
    mov    fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub    sp, sp, #(10*8 + 8*16)
    stp    q0, q1, [sp, #(0*16)]
    stp    q2, q3, [sp, #(2*16)]
    stp    q4, q5, [sp, #(4*16)]
    stp    q6, q7, [sp, #(6*16)]
    stp    x0, x1, [sp, #(8*16+0*8)]
    stp    x2, x3, [sp, #(8*16+2*8)]
    stp    x4, x5, [sp, #(8*16+4*8)]
    stp    x6, x7, [sp, #(8*16+6*8)]
    str    x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov    x2, x16
    bl    __class_lookupMethodAndLoadCache3

    // imp in x0
    mov    x17, x0

    // restore registers and return
    ldp    q0, q1, [sp, #(0*16)]
    ldp    q2, q3, [sp, #(2*16)]
    ldp    q4, q5, [sp, #(4*16)]
    ldp    q6, q7, [sp, #(6*16)]
    ldp    x0, x1, [sp, #(8*16+0*8)]
    ldp    x2, x3, [sp, #(8*16+2*8)]
    ldp    x4, x5, [sp, #(8*16+4*8)]
    ldp    x6, x7, [sp, #(8*16+6*8)]
    ldr    x8,     [sp, #(8*16+8*8)]

    mov    sp, fp
    ldp    fp, lr, [sp], #16

.endmacro

Tipsc 方法在汇编中使用的时候,需要在方法名前加 _ 。所以在汇编中某个方法为 _xxx,则在其他地方查找实现,需要去掉 _。 此时 __class_lookupMethodAndLoadCache3 在汇编中没有实现,则去掉一个 _ 按照 _class_lookupMethodAndLoadCache3 在非汇编代码中查找

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
	  // 首先从方法缓存中查找,如果找到则 return imp也就是把方法地址返回给 bl 指令
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }


 retry:    
    runtimeLock.assertReading();
		// 这里也先从方法缓存中查找,目的是前面查找后,后续又通过 runtime 的方式增加了方法缓存
    // Try this class's cache.
    imp = cache_getImp(cls, sel);
    if (imp) goto done;	// 找到方法则执行 done 里面的逻辑done 里面也是将 IMP 的地址返回出去

    // Try this class's method lists.
    {
      // 通过 getMethodNoSuper_nolock 方法查找 cls 中有没有 SEL 方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }

            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }

            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

消息发送阶段

上面的代码走到 getMethodNoSuper_nolock 寻找类里的方法

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    // 这里根据类结构体找到 data(),然后找到 methods Array 数组,数组元素是方法 Array
    /*
    data() 其实就是 class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    */
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 排好序则调用 `findMethodInSortedMethodList`,其内部是二分查找实现。
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 没排序则线性查找,顺序依次遍历查找
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;

    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)probe->name;

        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }

        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}

cls->data()->methods.beginLists 这里根据类结构体调用到 data() 方法,获取到 class_rw_t

class_rw_t *data() { 
    return bits.data();
}

然后通过 class_rw_t 找到 methods Array 数组,数组元素是方法 Array。内部调用 search_method_list 方法。

search_method_list 方法内部判断方法数组是否排好序

  • 排好序则调用 findMethodInSortedMethodList,其内部是二分查找实现。

  • 没排序,则线性查找 (Linear search of unsorted method list)

getMethodNoSuper_nolock 执行完则会将方法写入到当前类对象的缓存中,调用 log_and_fill_cache 方法

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);	// 继续走 cache_fill 逻辑 
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);	// 继续走 cache_fill_nolock 逻辑
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;
		// 根据类对象/类的元类对象获得方法缓存 cache_t
    cache_t *cache = getCache(cls);
	  //根据方法 key 调用 getKey 方法,获取方法缓存哈希表的 key
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
  	// 根据方法 key 和 receiver 查找方法缓存哈希表 bucket
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
  	// 将方法缓存 key方法地址 IMP 保存到方法缓存哈希 bucket 中
    bucket->set(key, imp);
}

摘出 lookUpImpOrForward 方法中的一段代码

// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
    Method meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
}
// Try superclass caches and method lists.

如果代码没有找到,则不会 gotodone,开始走父类缓存查找逻辑

// Try superclass caches and method lists.
{
    unsigned attempts = unreasonableClassCount();
    // for 循环不断查找,找当前类的父类,父类的父类...直到当前类为 nil(一轮查找后当前类 curClass = curClass->superclass)。
    for (Class curClass = cls->superclass;
            curClass != nil;
            curClass = curClass->superclass)
    {
        // Halt if there is a cycle in the superclass chain.
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 先在父类的方法缓存中查找(根据 sel & mask得到方法缓存哈希中的 key`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
              	// 找到则将 IMP 写入到当前类 cls 的方法缓存中去,比如调用 [student personSay] 方法,从父类的方法列表找到 personSay则将 personSay 的 IMP 写入到 student 类的方法缓存中去
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        // 如果在父类的方法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 方法数组Array 元素为方法数组),按照排序好和没排序好分别走二分查找和线性查找。
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
           // 如果找到则继续填充到当前类的方法缓存中去
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
} 

上面的流程是整个 objc_msgSend 的消息发送阶段的整个流程。可以用下图表示

注意:

当找到方法后,缓存只保存到当前消息调用者,也就是子类的方法 Cache 中去。

方法缓存也叫快速映射表fast map即使是快速执行路径fast path还是不如“静态绑定的函数调用操作”statically bound function call那样迅速但大多数情况下这不是性能瓶颈。如果真的很在意可以用纯 c 函数去实现。

上述消息发送阶段结束,方法依旧没有找到并执行,则开始进入动态方法解析阶段。

动态方法解析阶段

接着查看源码

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
    // ...
}

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

判断当前类没有走过动态方法解析阶段,则走动态方法解析阶段,调用 _class_resolveMethod 方法。

内部会判断但前类是不是元类对象、还是类对象走不同逻辑。

类对象走 _class_resolveInstanceMethod 逻辑

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

核心是 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); ,也就是动态方法解析阶段,如果是类对象,则会调用类方法 + (BOOL)resolveInstanceMethod:(SEL)sel 方法。

元类对象走 _class_resolveClassMethod 逻辑

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

其实就是调用 bool resolved = msg(_class_getNonMetaClass(cls, inst), SEL_resolveClassMethod, sel);

最后还是走到了 goto retry; 继续走完整的消息发送流程(因为添加了方法,所以会按照方法查找再去执行的逻辑)

完整流程如下

QA

方法动态解析后,为什么还要 goto retry?

因为 API 设计阶段只是增加了口子,预留了能力,让开发者可以给类增加对象方法、类方法进行方法的动态解析。

如果我们实现了 + (BOOL)resolveInstanceMethod:(SEL)sel 方法或者实现了 + (BOOL)resolveClassMethod:(SEL)sel,

则根据函数返回值,拿到 YES 标记为已解析,则执行 goto retry。此时在一开始查找的时候就可以找到 IMP此时 goto done

里面返回函数地址到汇编代码中。

如果没有实现动态解析方法,则执行 goto retry 的时候,一开始找不到 IMP且在动态方法解析的 if 条件判断不成立 if(resolver & !triedResolver) 则不会继续动态方法解析,开始执行下面的逻辑,进入消息转发阶段 _objc_msgForward_impcache

上述动态消息解析后,会缓存起来吗?

第一次消息动态解析后,只是将方法增加到 class 或者 meta-class 的 class_rw_t 中。然后会继续执行 goto retry,在 gto retry 的流程中,会先在 class 的方法缓存中查找有没有缓存,如果没有缓存,则会在 class 的 class_rw_t 中查找方法,找到并执行,同时还会把方法保存到方法缓存中去。以便后续再次调用方法的时候更加高效。

所以这个“会”缓存起来,只不过缓存的时机不是同步的,而是再次调用 goto retry 的时候,发现没有缓存,则在 class_rw_t 找查找到后,再缓存起来

动态消息解析 Demo

实例方法
Person *person = [[Person alloc] init];
[person makeLiving];

调用不存在方法则报错 ***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person makeLiving]: unrecognized makeLiving sent to instance 0x101b2d900'**

因为调用对象不存在的方法,所以会 Crash

知道 objc_msgSend 的流程,我们尝试给它修正下

方法1增加一个兜底方法然后利用 class_addMethod 动态增加方法实现

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

- (void)mockMakeLiving {
    NSLog(@"方法动态解析拦截第一阶段");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(makeLiving)) {
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(mockMakeLiving));
        class_addMethod(self, sel, method->imp, method->types);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

方法2不用自定义的 struct method_t 结构体,直接用 Method 去承接,只不过在传递 IMP、encoding 的时候用 runtime api 获取即可

Method method = class_getInstanceMethod(self, @selector(mockMakeLiving));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));

方法3也可以添加 c 语言方法

c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 (IMP)cfuntionResolver,手动添加方法签名。

void cfuntionResolver(id self, SEL _cmd) {
    NSLog(@"cfuntionResolver %@ %@", self, NSStringFromSelector(_cmd));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        // 对象方法,存在于对象上。
        class_addMethod(self, sel, (IMP)cfuntionResolver, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
类方法动态解析

也可以给类方法做动态方法解析。需要注意的是类方法

  • 调用 -(BOOL)resolveClassMethod:(SEL)sel

  • class_addMethod 方法中的第一个参数,需要加到类的元类对象中,所以是 object_getClass

[Person drink];
void mockDrink (id self, SEL _cmd) {
    NSLog(@"假喝水");
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(drink)) {
        // 类方法,存在于元类对象上。
        class_addMethod(object_getClass(self), sel, (IMP)mockDrink, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

消息转发阶段

能走到消息转发,说明

  1. 类自身没有该方法(objc_msgSend 的消息发送)

  2. objc_msgSend 动态方法解析失败或者没有做

说明:类自身(自身方法缓存、自身没有方法实现、自身也没有动态增加方法)和父类没有可以处理该消息的能力,此时应该将该消息转发给其他对象

查看 objc4 的源码

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
    // ...
}

继续查找 _objc_msgForward_impcache

STATIC_ENTRY __objc_msgForward_impcache

MESSENGER_START
nop
MESSENGER_END_SLOW

// No stret specialization.
b    __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr    x17, [x17, __objc_forward_handler@PAGEOFF]
br    x1

END_ENTRY __objc_msgForward

查找 __objc_forward_handler 没有找到,可以猜想是一个 c 方法,去掉最前面的 _,按照 _objc_forward_handler 查找得到

__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

消息转发的代码是不开源的,查找资料找到一份靠谱的 __forwarding 方法实现

为什么是 __forwarding__ 方法。我们可以根据 Xcode 崩溃窥探一二

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

具体地址可以参考 __frowarding

完整流程如下

方法签名的获取

方法1: 自己根据方法的返回值类型方法2个基础参数参数id selfSEL _cdm,其他参数类型按照 Encoding 自己拼。 类似 v16@0:8

方法2 :根据某个类的对象,去调用 methodSignatureForSelector 方法获取。

[[[Person alloc] init] methodSignatureForSelector:@selector(makeLiving)];

方法1自己拼的缺点是不够灵活修改原始方法需要在方法签名处修改方法的参数、返回值信息还需手动计算。效率低。如果某个类实现了方法则可以通过 [class methodSignatureForSelector:@selector(***)] 的方式获取方法签名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return [[[PersonHelper alloc] init] methodSignatureForSelector:@selector(makeLiving:)];
    }
    return [super methodSignatureForSelector:aSelector];
}

消息转发 Demo

对象方法消息转发 Demo

Person 类不存在对象方法 makeliving PersonHelper 类存在。

调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,resolveInstanceMethod 没有处理对象方法,所以会报错

方法1因为动态消息解析没有处理则会开始走消息转发阶段。消息转发首先会调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。(如果是对象方法则调用 - (id)forwardingTargetForSelector:(SEL)aSelector,如果是类方法则调用 + (id)forwardingTargetForSelector:(SEL)aSelector

方法2如果消息转发里forwardingTargetForSelector 返回了 nil则开始调用方法签名 methodSignatureForSelector 方法和 forwardInvocation 方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(makeLiving)) {
//        return [[PersonHelper alloc] init];
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // anInvocation 是包含了函数调用的所有信息
	  //	anInvocation.target; 调用者
    // anInvocation.selector; 方法名
    // anInvocation getArgument: atIndex:;	方法参数
    // anInvocation.target = [[PersonHelper alloc] init];
    // [anInvocation invoke];
    [anInvocation invokeWithTarget:[[PersonHelper alloc] init] ];
}

注意:

  1. methodSignatureForSelector 如果返回 nilforwardInvocation 不会执行
  2. 方法签名 methodSignatureForSelector 必须正确,否则会获取参数 crash报错 Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSInvocation getArgument:atIndex:]: index (2) out of bounds [-1, 1]' 的错误

上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。

通过 NSInvocation 获取参数一般是从2开始因为第一个是 self第二个是 _cmd第三个是 cost

类方法消息转发 Demo

上述是实例方法找不到的情况,我们也可以给类方法,增加消息转发的处理。

  • 基本上的处理上对象方法转发所用到的方法,前面的 - 变为 + 即可。
  • 某些关于方法签名或者转发的对象换为类对象即可
+ (id)forwardingTargetForSelector:(SEL)aSelector  {
    return [PersonHelper class];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [[PersonHelper class] methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[PersonHelper class]];
}

注意:实例对象有消息转发,类方法也有消息转发机制。但是在 Xcode 中只可以提示 -(id)forwardingTargetForSelector:(SEL)aSelector

不提示 +(id)forwardingTargetForSelector:(SEL)aSelector 所以有人文章说 OC 不支持类方法的动态方法解析和转发,这是错误的

OC 消息机制是什么样的?

OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 receiver方法调用者发消息selector 方法名),就是 objc_msgSend 的工作流程具体分为3个阶段

  • 消息发送objc_msgSend 的完整流程
  • 动态方法解析
  • 消息转发

消息传递流程

  1. 先判断是否命中缓存,利用 sel 和 _mask计算出哈希值然后在 bucket_list 中查找方法缓存,找到则调用。没有找对则继续查找

  2. 没有找到缓存,则根据对象的 isa 在类对象的方法列表中,进行方法查找

    • 对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数
    • 对于没有排序好的列表,采用一般遍历查找方法对应执行函数
  3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找

    先根据 superclass 找到父类然后在父类中也按照上面3点进行递归查找

    • 先判断能否命中方法缓存,根据 sel 和 _mask 计算哈希,然后判断是否命中
    • 没有命中,则在父类的类对象方法列表中查找。对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数;对于没有排序好的列表,采用一般遍历查找方法对应执行函数
    • 还是没有命中,则对父类的父类,继续上述流程

Super 底层原理

@interface Person: NSObject
  
@end
@implementation Person
@end


@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"%@", [self class]);        // Student
        NSLog(@"%@", [self superclass]);   // Person 
        NSLog(@"%@", [super class]);       // Student
        NSLog(@"%@", [super superclass]);  // Person 
    }
    return self;
}
@end

后面2个的打印似乎不符合预期转成 c++ 代码看看

static instancetype _I_Student_init(Student * self, SEL _cmd) {
    if (self = ((Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("superclass")));
    }
    return self;
}

[super class] 这句代码底层实现为 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));

__rw_objc_super 是什么?

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

objc_msgSendSuper 如下

/** 
 * Sends a message with a simple return value to the superclass of an instance of a class.
 * 
 * @param super A pointer to an \c objc_super data structure. Pass values identifying the
 *  context the message was sent to, including the instance of the class that is to receive the
 *  message and the superclass at which to start searching for the method implementation.
 * @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
 * @param ...
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method identified by \e op.
 * 
 * @see objc_msgSend
 */
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

所以 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")); 等同于下面代码

struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("class"))

也就是说 [super class] 中, super 调用的 receiver 还是 self。比如 [super class] 等价于 objc_msgSendSuper({self, class_getSuperClass(objc_getClass("Student"))}, @selector(class))

调用 class 方法,其实是在基类对象 NSObject 中的。因为是类方法,所以先从 Student 的父类 Person 类的元类对象中查找有没有 class 方法实现。发现没有,则继续根据 Person 类 superclass 指针,找到 Person 的父类的元类对象的类方法列表中中查找,没找到,则继续向上找,最后找到 NSObject 对象的元类对象的类方法列表中找到了,因为方法调用者是 self所以获取 class 得到的就是当前类,即 Student。

结构体的目的是为了在类对象查找的过程中,直接从当前类的父类中查找,而不是本类(比如 Student 类的 [super init] 会直接从 Person 的类对象中查找 init找不到则通过 superclass 向上查找)

大致推测系统的 class、superclass 方法实现如下

@implementation NSObject
- (Class)class{
    return object_getClass(self);   
}
- (Class)superclass {	// 先获取类对象,然后获取类对象的 superclass
    return class_getSuperclass(object_getClass(self));
}
@end

class 方法是在 NSObject 类对象的方法列表中的。所以

[self class] 等价于 objc_msgSend(self, sel_registerName("class"))

[super class] 等价于 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("class"))

其实2个方法本质上消息 receiver 都是 self也就是当前的 Student所以打印都是 Student

结论:[super message] 有2个特征

  • super 消息的调用者还是 self

  • 方法查找是根据当前 self 的父类的类对象方法列表开始查找

通过将代码转为 c++ 发现super 调用本质就是 objc_msgSendSuper,实际不然

我们对 iOS 项目[super viewDidLoad] 下符号断点,发现objc_msgSendSuper2

查看 objc4 源代码发现是一段汇编实现。

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START

ldp    x0, x16, [x0]        // x0 = real receiver, x16 = class
ldr    x16, [x16, #SUPERCLASS]    // x16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

所以 super viewDidLoad本质上就是

struct objc_super arg = {
    self, 
    [UIViewController class]    
};
objc_msgSendSuper2(arg, sel_registerName("viewDidLoad"));

objc_msgSendSuper2 和 objc_msgSendSuper 区别在于第二个参数

objc_msgSendSuper2 底层源码(汇编代码 objc-msg-arm64.s 422 行)会将第二个参数找到父类,然后进行方法缓存查找

objc_msgSendSuper 直接从第二个参数查找方法。

总结clang 转 c++ 可以窥探系统实现可以作为研究参考。super 本质上就是 objc_msgSendSuper2传递2个参数第一个参数为结构体第二个参数是sel。

为什么转为 c++ 和真正实现不一样?思考下

源代码变为机器码之前,会经过 LLVM 编译器转换为中间代码Intermediate Representation最后转为汇编、机器码

我们来验证下 super 在中间码上是什么

clang -emit-llvm -S Student.m

llvm 中间码如下,可以看到确实内部是 objc_msgSendSuper2

; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[Student sayHi]"(%0* %0, i8* %1) #1 {
  %3 = alloca %0*, align 8
  %4 = alloca i8*, align 8
  %5 = alloca %struct._objc_super, align 8
  store %0* %0, %0** %3, align 8
  store i8* %1, i8** %4, align 8
  %6 = load %0*, %0** %3, align 8
  %7 = bitcast %0* %6 to i8*
  %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0
  store i8* %7, i8** %8, align 8
  %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %10 = bitcast %struct._class_t* %9 to i8*
  %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1
  store i8* %10, i8** %11, align 8
  %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.6, align 8, !invariant.load !12
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.8 to i8*))
  ret void
}

指令介绍

@ - 全局变量
% - 局部变量
alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
i32 - 32位4字节的整数
align - 对齐
load - 读出store 写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支根据条件来转向label不根据条件跳转的话类似 goto
label - 代码标签
call - 调用函数

也可以在 Xcode 上可视化面板操作,路径为:菜单栏 Product -> Perform Action -> Assemble "ViewController.m"

消息发送的边界情况

  • objc_msgSend_stret 如果待发送的消息要返回结构体,那么可交给此函数完成。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。如果是返回值无法容纳于 CPU 寄存器中(比如结构体太大了),那么就由另一个函数执行派发,此时函数会通过栈上的某个变量来处理消息所返回的结构体
  • objc_msgSend_fpret如果消息返回的是浮点数那么可交给此函数处理。在某些架构的 CPU 中调用函数时,需要对浮点数寄存器做特殊处理,通常采用的 objc_msgSend 在这种情况下不合适,这个函数是为了处理 x86 等架构 CPU 中某些特殊的情况的
  • objc_msgSendSuper 要给超类发消息,就交给此函数处理。

编译器优化之尾调用优化

如果函数的最后一行是调用另一个函数,那么就可以采用“尾调用优化”技术。编译器会生成跳转至另一函数所需的指令码,而不会向调用堆栈中推入新的栈帧。

只有当函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行尾调用优化。这项优化对 objc_msgSend 很有用。如果不这么做,那么每次调用 OC 方法之前,都需要为调用 objc_msgSend 函数准备栈帧。而且很容易出现 stack overflow 的问题。

isKindOfClass、isMemberOfClass

Demo1

上面的打印有没有有疑惑2个判断都是调用对象方法的 isMemberOfClassisKindOfClass

由于 objc4 是开源的,查看 object.mm

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
  • isMemberOfClass 判断当前对象是不是传递进来的对象

  • isKindOfClass 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES不一样则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在NSObject 的父类为 nil。所以 isKindOfClass 其实判断的是当前类是传递进来的类或子类

Demo2

下面面2个判断都是调用类方法的 isMemberOfClassisKindOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

分析:

第一类:isMemberOfClass

可以看到 +(BOOL)isMemberOfClass:(Class)cls 方法内部就是对当前 receiver 类获取类对象,然后与传递进来的 cls 判断是否相等。因为本身就是类方法,方法 receiver 就是一个类对象,对类对象调用 object_getClass 方法,获取到的就是 receiver 类的元类对象

  • [Student isMemberOfClass:[Student class]] 等价于:先对 Student 类对象调用 object_getClass 方法,得到 Student 类的元类对象,等号左侧是 Student 的元类对象,等号右侧是 Student 类对象cls 参数也就是 [Student class] 是一个类对象元类对象等于类对象吗显然不是显然不成立输出0
  • [Student isMemberOfClass:[NSObject class]] 等价于:先对 Student 类对象调用 object_getClass 方法,得到 Student 类的元类对象,等号左侧是 Student 元类对象,等号右侧是 NSObject 类对象cls 参数也就是 [NSObject class] 是一个类对象元类对象等于类对象吗。显然不成立输出0

想让判断成立,可以改为 [Student isMemberOfClass:object_getClass([Student class])] 或者 [[Student class] isMemberOfClass:object_getClass([Student class])]

第二类:+isKindOfClass

可以看到 +(BOOL)isKindOfClass:(Class)cls 方法内部就是对当前 receiver 调用 object_getClass由于自身就是类方法所以receiver 就是类对象调用方法后返回元类对象。for 循环内判断,是否是右边传入对象的元类或者元类的子类。

  • [Student isKindOfClass:[Student class]]); 为什么会输出0因为 isKindOfClass 底层是 for 循环对传入的 self 使用 object_getClass((id)self) 获取类对象,因为传入的已经是类对象,所以 object_getClass((id)self) 内部得到的是元类对象。右边是传入的类对象。等号左边的 Student 的元类对象,不等于等号右边的类对象。所以为 false输出 0.

    如何更改使得输出1[Student isKindOfClass:object_getClass([Student class])]。右边也是元类对象,则为 True 输出1.

  • NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); 为什么输出1看右边的部分调用 isKindOfClass 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 [NSObject class] 判断是否想通过。

    第一次 for 循环当然不同,所以不能 return会将 tcls 走步长改变逻辑 tcls = tcls->superclass,也就是找到当前 Student 元类对象的父类。

    第二次 for 循环也一样不相等Person 元类不等于 [NSObject class] 继续向上,直到 tcls = NSObject。此时还是不等这时候 tcls  走步长改变逻辑,tcls = tcls->superclass NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 [NSObject class] == [NSObject class]return YES。

tips基类的元类对象指向基类的类对象。

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

QANSLog(@"%d", [Person isKindOfClass:[NSObject class]]); 为什么输出1

其内部流程:

  • 先对 receiver 调用 objcet_getClass 方法,得到 Person 的元类对象 tcls
  • 开启 for 循环,判断 tcls 是否等于方法参数(也就是 NSObject 的类对象)
  • for 循环一开始不满足,则不断进行,令 tcls = tcls->superClass。for 循环结束的条件是 tcls 为 nil所以最后找到 NSObject 的元类的时候继续往上找NSObject 元类对象的父类是 NSObject 的类对象,此时 tcls 就是 NSObject 的类对象cls 也是 NSObject 类的类对象相等输出1.

同理 [NSObject isKindOfClass:[NSObject class]] 也为 YES工作流程和上面的类似。也就是 [继承自 NSObject 及其继承自任何子类 isKindOfClass:[NSObject class]] 都为 YES

综合练习:

Runtime 刁钻题

NSObject 的内存布局、isa、对象属性访问原理

这道题目设计super 调用的本质、函数栈空间向下增长、runtime 消息调用本质isa、访问对象的成员变量找到 isa约过前面的8字节按照成员变量的大小去找成员

因为实例对象里存的就是isa + 各个成员变量的值

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHi;
@end
  
@implementation Person
- (void)sayHi {
    NSLog(@"himy name is %@", self->_name);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *temp = @"杭城小刘";
    id obj = [Person class];
    void *p = &obj;
    [(__bridge id)p sayHi];
}

程序运行什么结果?

为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下:

[Person class] 类对象是全局唯一的,存在于全局区。

第一、方法调用本质就是寻找 isa 进行消息发送

Person *person = [[Person alloc] init];
[person sayHi];

[[Person alloc] init] 在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。

[p sayHi] 编译后 objc_msgSend(p, @selector(sayHi))

第二、栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。

这个流程其实和上面的代码一样的。所以可以正常调用

void test () {
    long long a = 4;        // 0x7ff7bfeff2d8
    long long b = 5;        // 0x7ff7bfeff2d0
    long long c = 6;        // 0x7ff7bfeff2c8
    NSLog(@"%p %p %p", &a, &b, &c);
}

方法内的变量存储在栈上,堆向上增长,栈向下增长。

第三id 的本质是

typedef struct objc_object *id;

objc_object 的核心结构是什么?

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
 		// ... 
}

所以可以看到找到对象 id也就可以找到 isa 信息,也就可以获取到类对象信息,进而查找方法列表。

第四、实例对象的本质就是一个结构体存储所有成员变量isa 是一个特殊成员变量,其他的成员变量,这里就是 _namesayHi 方法内部的 self 就是 obj找成员变量的本质就是找内存地址的过程属性的地址 = 对象地址基础地址 + 属性的Offset。此时就是 isa 地址 + 8字节偏移量

上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 sayHi 消息也就是通过 obj 指针找到 isa恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 _name,所以我们给自定义的指针 void *p 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近下8个字节因为 isa 是指针长度为8_name 属性,此时也就找到了 temp 字符串。

struct Person_IMPL {
    Class isa; // 8字节
    NSString *_name;    // 8字节
}

整体流程是:

  • 调用 [p sayHi] 方法的的流程是,编译后 objc_msgSend(p, @selector(print))
  • Runtime 会认为 p 是对象,然后寻找 p 的 isa 信息,即使 isa 是 union 类型,但系统根据 isa 寻找类对象的时候,是调用 p 地址 & ISA_MASK 获取类对象地址的
  • 也就是获取到了 cls 指向的 [Person class] 类对象地址。然后 Person 内存在对象方法 print发起调用
  • 但内部访问的 self.name 本质就是:「知道对象的地址,如何访问属性值」。属性值 = 实例对象 baseAddress + 属性 offsetAddress对于 name 也就是基地址偏移8位找到 name
  • 由于方法内就是栈,由高地址向低地址增长(方法内从上到下,变量的地址依次降低)
  • 因为 obj 本身占8位然后找 name 也就是获取到上一个8位的对象值此时拿到了 temp所以 Runtime 会认为 temp 就是所需要的 self.name

再看一个变体1

打印输出是因为 *p 类似 isa 指针。本身占用8字节空间然后访问 self->_name 就是 base + 8 = isa地址 + 8 出的内存就是 name*p 是在栈中加8就是向上声明的变量当前情况下 Address(*p) + 8 就是 temp 变量。所以输出 <NSObject: 0x****>

再看一个变体2

分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂栈地址由高到低,向下生长super 调用的本质。

再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中isa 地址就是 class 的地址,所以按照地址 +8 的策略,其实前一个局部变量。

[super viewDidLoad]; 本质就是 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))

struct objc_super arg = {self, class_getSuperclass(objc_getClass("ViewController"))};
objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));

所以此时的“前一个局部变量” 也就是结构体 objc_super 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self所以“前一个局部变量” 也就是 selfViewController

结构体有2个成员变量顺序越高的成员变量地址越高。所以在栈上struct 中第一个成员变量地址更低。在通过 isa 内存偏移的时候,优先找到 self。所以会输出 ViewController

可能会疑问,知道了 super 调用本质知道了会产生一个局部变量的结构体但是结构体里面2个成员变量找属性的时候isa 下的8个字节会命中哪个

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

结构体 objc_super 存在2个成员变量self 是第一个,class_getSuperclass(objc_getClass("ViewController")) 是第二个self 地址更低,会被认为是 Person 的 name 属性值(虽然栈内变量地址由高到低,但是结构体 objc_super 的2个成员变量顺序并不会改变也就是相对位置不变。假设结构体地址为 structBase = 0x00007ff7bf961c28也就是第一个成员变量 receiver 地址为 0x00007ff7bf961c28由于 id 类型长度为8位所以第二个成员变量 super_class 地址为 = 0x00007ff7bf961c28 + 8 = 0x7FF7BF961C30 )。

假设 obj 地址为 0x7FF7BF961C20所以 name 地址为 0x7FF7BF961C20 + 8 = 0x7FF7BF961C28命中结构体的地址按照 name 自身长度为8也就取到了结构体第一个成员变量 self 的值。此时为 ViewController 对象。

画图如下,有助于理解

runtime 对象方法、类方法的查找过程熟悉吗?

下面的代码会 crash 吗?

id rs = [NSObject valueForKey:@"isa"];
NSLog(@"%@", rs);

不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 valueForKey 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 valueForKey 方法。

因为分类 NSObject(NSKeyValueCoding) 实现了 -valueForKey 方法。所以不会 crash在类对象方法中访问 isa也就是获取类对象的 isa也就是 NSObject 的元类对象。

查看了 objc 源码,会发现很多 NSObject 的基础方法:+ (id)init- (id)init 等均有 +- 方法。

为什么这么设计?猜测是为了代码的健壮。

  • 因为存在继承关系,所有的对象的基类需要有个源头。即使是 NSObject 也是如此。
  • 调用对象方法的本质就是根据对象的 isa 找到类对象,然后从方法缓存中去查找方法实现,有就调用,没有就从类对象的方法列表中查找,找到则写入方法缓存并调用,没有则根据 superclass 的类对象继续查找...,一直找到 NSObject 还是找不到,则走 Runtime 消息转发流程。
  • 调用类方法的本质是根据对象的 isa 找到类对象,再根据类对象的 isa 找到元类对象,从元类对象的方法方法列表中查找方法实现,如果没有则继续向上查找,直到找到基类的元类对象,也就是 NSObject如果找不到则走消息转发流程
  • 但是 Apple 的设计是为了 NSObject 元类对象的父类也要有个东西去接着,于是就让 NSObject 的类对象来充当 。

这也就是为什么 NSObject 子类对象调用 + 类方法不 crash 的原因。

应用场景

统计 App 中未响应的方法。给 NSObject 添加分类 NSObject+ExceptionHunter.m,利用 NSProxy 实现

#import "NSObject+ExceptionHunter.h"
#import <objc/runtime.h>

@implementation NSObject (ExceptionHunter)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    id receiver = anInvocation.target;
    NSString *methodName = NSStringFromSelector(anInvocation.selector);
    // 收集上报...
    NSLog(@"%@方法未调用成功", NSStringFromSelector(anInvocation.selector));
}

@end

修改类的 isa

类似 KVO 的实现,就是更改 isa。

object_setClass 实现

Person *p = [Person new];
object_setClass(p, [Student class]);

动态创建类

必须先创建类:objc_allocateClassPair、再注册类(用于向运行时系统注册一个新创建的类及其元类):objc_registerClassPair 成对存在

动态创建类、添加属性、方法

void study (id self, SEL _cmd) {
    NSLog(@"在学习了");
}

void createClass (void) {
    Class newClass = objc_allocateClassPair([NSObject class], "GoodStudent", 0);
    class_addIvar(newClass, "_score", 4, 1, "i");
    class_addIvar(newClass, "_height", 4, 1, "i");
    class_addMethod(newClass, @selector(study), (IMP)study, "v16@0:8");
    objc_registerClassPair(newClass);
    id student = [[newClass alloc] init];
    [student setValue:@100 forKey:@"_score"];
    [student setValue:@177 forKey:@"_height"];
    [student performSelector:@selector(study)];
    NSLog(@"%@ %@", [student valueForKey:@"_score"], [student valueForKey:@"_height"]);
}

注意:

  • 运行时注册的类会在程序生命周期内持久存在(调用 copy、create 等出来的内存),不使用的时候需要手动释放objc_disposeClassPair(newClass>)
  • 能否动态添加实例变量到已注册的类?不能。注册后类的内存布局已固定,修改会导致崩溃。

访问成员变量信息

void ivarInfo (void) {
    Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
    NSLog(@"%s %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar)); //_name @"NSString"
    // 设置、获取成员变量
    Person *p = [[Person alloc] init];
    Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
    object_setIvar(p, ageIvar, (__bridge id)(void *)27);
    NSLog(@"%d", p.age);
}

runtime 设置值 api object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 第三个参数要求为 id 类型,但是我们给 int 类型的属性设置值怎么办可以将27这个数字的地址传进去同时需要类型转换为 id (__bridge id)(void *)27)

KVC 可以根据具体的值,去取出 NSNumber ,然后调用 intValue

[p setValue:@27 forKey:@"_age"];

访问对象的所有成员变量信息-字典转模型

用到的 API 是 class_copyIvarList`

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end

unsigned int count;
// 数组指针
Ivar *properties = class_copyIvarList([Person class], &count);
for (int i =0 ; i<count; i++) {
    Ivar property = properties[i];
    NSLog(@"属性名称:%s 属性类型:%s", ivar_getName(property), ivar_getTypeEncoding(property));
}
free(properties);
// 属性名称_age 属性类型i
// 属性名称_name 属性类型:@"NSString"

注意:调用了 class_copyIvarList 方法,用到 copy、create 等创建的内存,都必须手动 free 掉。

根据这个可以做很多事情,比如设置解模型、给 UITextField 设 placeholder 的颜色

先根据 class_copyIvarList 访问到 UITextFiled 有很多属性,然后找到可疑累_placeholderLabel,通过打印 class、superclass 得到类型为 UILabel。所以用 UILabel 对象设置 color 即可,要么通过 KVC 直接设置

[self.textFiled setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];

设置字典转模型

@implementation NSObject (JSON)

+ (instancetype)modelWithJSON:(NSDictionary *)dict {
    id obj = [[self alloc] init];
    [obj setupPropertiesWithDict:dict class:[self class]]; // 从当前类开始遍历
    return obj;
}

- (void)setupPropertiesWithDict:(NSDictionary *)dict class:(Class)cls {
    if (cls == [NSObject class]) return; // 终止条件:到达 NSObject
    
    // 1. 递归处理父类
    [self setupPropertiesWithDict:dict class:[cls superclass]];
    
    // 2. 处理当前类的属性
    unsigned int count;
    Ivar *ivars = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarName = ivar_getName(ivar);
        NSString *propertyName = [NSString stringWithUTF8String:ivarName];
        
        // 去掉下划线前缀(如 _name → name
        if ([propertyName hasPrefix:@"_"]) {
            propertyName = [propertyName substringFromIndex:1];
        }
        
        // 从字典中取值并赋值
        id value = dict[propertyName];
        if (value && ![value isKindOfClass:[NSNull class]]) {
            [self setValue:value forKey:propertyName];
        }
    }
    free(ivars);
}

@end

不够健壮体现在:

  • 假设服务器返回的 json 有9个字段本地对象有8个字段怎么处理可能报错 [<Student 0x10072dec0> setNilValueForKey:]
  • 服务器给的有名字为 id 的数据OC 对象的属性名不能叫 id如何处理
  • Person 中嵌套 Cat 呢Person 对象有个 Cat 类型的属性

具体可以参考 YYModel

替换方法实现

注意

  • 类似 NSMutableArray 的时候,+load 方法进行方法替换的时候需要注意类簇的存在,比如 __NSArrayM

  • 方法交换一般写在类的 +load 方法中,且为了防止出问题,比如别人手动调用 load代码需要加 dispatch_once

void studentSayHi (void) {
    NSLog(@"Student say hi");
}
void changeMethodImpl (void){
    class_replaceMethod([Person class], @selector(sayHi), (IMP)studentSayHi, "v16@0:8");
    Person *p = [[Person alloc] init];
    [p sayHi];
}
// Student say hi

上述代码可以换一种写法

class_replaceMethod([Person class], @selector(sayHi), imp_implementationWithBlock(^{
    NSLog(@"Student say hi");
}), "v16@0:8");
Person *p = [[Person alloc] init];
[p sayHi];

imp_implementationWithBlock(id _Nonnull block) 该方法将方法实现替换为包装好的 block

Person *p = [[Person alloc] init];
Method sleep = class_getInstanceMethod([Person class], @selector(sleep));
Method sayHi = class_getInstanceMethod([Person class], @selector(sayHi));
method_exchangeImplementations(sleep, sayHi);
[p sayHi];    // 人生无常,抓紧睡觉
[p sleep];    // Person sayHi

runtime 方法交换的本质,就是交换类对象、元类对象的 class_rw_t 的 method_array_t<method_list_t<method_t>> 里的 method_t 的 IMP。

无痕埋点

对 App 内所有的按钮点击事件进行监听并上报。发现 UIButton 继承自 UIControl所以添加分类在 load 方法内替换方法实现。UIControl 存在方法 sendAction:to:forEvent:

@implementation UIControl (Monitor)
+ (void)load {
  	static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    	 Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:));
	     method_exchangeImplementations(method1, method2);
    });
}

- (void)lbp_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    // 调用系统原来的实现
    [self mj_sendAction:action to:target forEvent:event];
//    [target performSelector:action];
}
@end

为了对业务代码无影响,在 hook 代码内部又要调用回去,所以需要调用原来的方法,此时因为交换方法实现,所以原来的方法应该是 lbp_sendAction:to:forEvent:

method_exchangeImplementations 方法实现交换了,系统会清空缓存,调用 flushCaches 方法,内部调用 cache_erase_nolock 来清空方法缓存。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;
    rwlock_writer_t lock(runtimeLock);
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?
    flushCaches(nil);
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

static void flushCaches(Class cls)
{
    runtimeLock.assertWriting();
    mutex_locker_t lock(cacheUpdateLock);
    if (cls) {
        foreach_realized_class_and_subclass(cls, ^(Class c){
            cache_erase_nolock(c);
        });
    }
    else {
        foreach_realized_class_and_metaclass(^(Class c){
            cache_erase_nolock(c);
        });
    }
}

安全气垫

安全气垫就是对代码运行过程中出错的方法进行兜住比如数组越界等。ROI 和带来的一些业务异常问题就见仁见智了。实现手段就是 runtime hook 然后更改方法实现。做一些安全判断。和网易大白的作者交流过,发现安全气垫的副作用,比如一个商品价格运算后异常,会 crash 这时候起码 crash 不能交易下单了。但是用了安全气垫,好处是不会 crash缺点是给一个有问题的值要么价格为0要么为空。用户可以正常下单这时候产生资损了。电商业务虾带来的业务异常问题比稳定性异常问题更严重。电商公司一般会给商家有资损保障赔付率。

不写代码是因为没有什么难的地方,难点在于策略的选择。

单元测试的 mock

单元测试框架很多都依赖了 Runtime 的能力,来 mock 方法返回值。比如 OCMock 库。

热修复

动态替换方法实现修复线上 Bug。具体的可以看开源库 JSPatch

hook 库

比如 Aspects 的实现。

总结

OC 是一门动态性很强的编程语言允许很多操作推迟到程序运行时决定。OC 动态性其实就是由 Runtime 来实现的Runtime 是一套 c 语言 api封装了很多动态性相关函数。平时写的 oc 代码,底层大多都是转换为 Runtime api 进行调用的。

  • 关联对象

  • 遍历类的所有成员变量(可以访问私有变量,比如修改 UITextFiled 的 placeholder 颜色、字典转模型、自动归档解档)

  • 交换方法实现

  • 扩大点击区域

  • 利用消息转发机制,解决消息找不到的问题

  • 无痕埋点

  • 热修复

可以认为 oc 中对象上一个指向 CLassObject 地址的变量 id obj = &ClassObject

而对象的实例变量 void *ivar = &obj + offset(N*size) 。根据 isa 也就是对象的基地址,然后偏移访问 ivar。