154 KiB
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个字节,是结构体里面的 位域
-
nonpointer:0,代表普通的指针,存储着 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:是否有被弱引用指向过,如果没有,释放时会更快
-
unused(deallocating):对象是否正在释放
-
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_t(Read Only):- 内容:由编译器生成,包含类名、父类指针、实例大小、基础方法列表(
baseMethodList)、属性列表(baseProperties)、协议列表(baseProtocols)等。 - 内存位置:存储在 Mach-O 文件的
__DATA __objc_const段中。 - 访问方式:通过
objc_class->bits.data()直接访问。
- 内容:由编译器生成,包含类名、父类指针、实例大小、基础方法列表(
阶段 2:运行时初始化(
class_rw_t)-
触发时机:
- 首次调用
[MyClass alloc]或objc_getClass("MyClass")时触发类的realize。 - 运行时通过
realizeClassWithoutSwift()函数完成初始化。
- 首次调用
-
核心操作:
- 分配
class_rw_t:动态创建可读写结构体。 - 绑定
class_ro_t:将rw->ro指向编译期的class_ro_t。 - 更新指针:将
objc_class->bits.data()指向class_rw_t。 - 合并 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_t与class_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:
-
method_array_t中存储了method_list_t,method_list_t的元素为method_list_t, 为什么不直接称它为二维数组?严格来说,不是二维数组,只不过是 array 里添加的对象也是 array,且各个数组不等长,也不会补空。
-
为什么需要设计为这样的结构?
调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的,普通方法走的是消息机制。首先判断是对象方法还是类方法,然后根据 isa 找类对象(对象方法)和元类对象(对象方法)信息中先从方法缓存中查找方法是否有缓存(方法缓存查找的过程是:先根据方法的 SEL,SEL 本质上是一个指向方法名的 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 都是 array,category 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 - 1,1<<2 - 1 = 3 setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } }方法通过 superclass 从父类类对象、父类元类对象中查找方法,找到后则将方法实现缓存到当前类的方法缓存中:
- 根据方法的 SEL,SEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t,然后和方法缓存哈希表的 MASK 进行按位与,MASK 值初始为
1<<INIT_CACHE_SIZE - 1。
- 根据方法的 SEL,SEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t,然后和方法缓存哈希表的 MASK 进行按位与,MASK 值初始为
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 encoding 为 i24@0:8i16f20
解读下,上面的方法其实携带了2个基础参数,(id)self _cmd:(SEL)_cmd :
-
i代表方法返回值为 int -
24代表参数共占24个字节大小。4个参数分别为 id 类型的self、SEL类型的_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 完整流程
-
通过实例的
isa指针找到类对象实例方法存储在类对象中,类方法存储在元类中。方法调用时,首先通过实例的
isa指针定位到类对象。 -
找类对象的
cache-
检查类对象的方法缓存
cache(哈希表结构),若找到方法实现(IMP),直接调用并结束流程。 -
缓存目的:哈希表查找时间复杂度为 O(1),避免频繁方法查找的性能损耗。
-
-
查找类对象的
method_list_t方法列表-
若
cache未命中,遍历类对象的method_list_t(方法列表)。 -
方法列表结构:
- 由多个方法数组构成,顺序为:后加载的 Category 方法在前,类自身方法在后(例如:Category2 → Category1 → 类原始方法)。
- 这是 Runtime 在启动时合并
class_ro_t(只读数据)到class_rw_t(可读写数据)时确定的,后编译的 Category 方法会插入到列表前端,覆盖同名方法。
-
命中缓存:若在方法列表中找到方法,将其缓存到当前类对象的
cache中,随后调用方法。
-
-
沿继承链向父类逐级查找
若当前类未找到方法,通过
superclass指针查找父类,重复以下步骤:- 查找父类
cache:若父类cache命中,将方法缓存到最初类对象的cache(即子类的cache),调用方法 - 如果 cache 中 没有找到,则查找父类
method_list_t:若父类方法列表命中,同样缓存到子类的cache,调用方法 - 递归查找:若未找到,继续向更上层父类查找...
- 直到根类(
NSObject),如果 NSObject 的 cache 中找到方法缓存则执行方法,且把方法在当前的子类的 cache 中缓存一份。如果没找到则继续在 method_list_t 中查找,找到也继续在当前的子类 cache 中缓存一份
- 查找父类
-
若当前类未找到方法,通过
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_t的buckets中查找,假设 key 相等,则直接返回对应的方法 -
如果没有找到则执行
cache_next方法,该方法则会判断 i 是不是等于0,不等于0则自减1,等于0则设置为 mask 的值 -
mask 值,在设计上等于
buckets散列表的长度减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 -
所以哈希函数设计位:方法选择器
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),逐步插入 test1、test2、test3、test4 方法后触发扩容,最终容量变为 8(_mask = 7)。
-
初始状态,散列表容量为4,
_mask=3,_occupied = 0,目前没有方法写入缓存。散列桶数组为空buckets[0]: null buckets[1]: null buckets[2]: null buckets[3]: null -
不断插入方法:
-
假设 test1 的 sel 的 uintptr_t 为5,计算哈希索引
5 & 3 = 10b 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= 2buckets[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 * capacity(3/4 * 4 = 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
-
读取方法流程
- 从缓存中查找 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
- 从缓存中查找 test1 方法,假设 test1 的 sel 的 uintptr_t 为5,计算哈希索引
结论:
- 存储位置动态变化:扩容后方法的存储位置会发生变化(如
test3从旧索引 2 → 新索引 1),但通过 重新哈希,所有方法被重新分配到新容量的正确位置。 - 查找逻辑一致性:无论缓存是否扩容,查找时始终使用 当前的
_mask计算索引,确保能正确命中方法。 - 性能与空间的平衡:空间换时间
- 扩容代价:重新哈希需要遍历旧桶,时间复杂度为 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; 所以长度为8,mask 为长度-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 中查找方法实现。
- 如果找到了,则更新到子类类对象的方法缓存中,然后执行方法
- 如果没找到,则继续沿着父类类对象的 superclass 指针,继续往上找,查找流程和上面的步骤类似
- 如果找到了则将方法更新到方法缓存中去,然后立马执行(查找的过程则是根据 SEL 的方法名 C 字符串指针地址值与 _mask 按位与运算,然后在方法缓存的哈希表中查找 比如
- 如果一直找到根类,还是找不到则开始走消息机制...
散列表的设计哲学
阅读了方法缓存相关的源码,可以看到 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 == 0,NORMAL
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
Tips:c 方法在汇编中使用的时候,需要在方法名前加 _ 。所以在汇编中某个方法为 _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.
如果代码没有找到,则不会 goto 到 done,开始走父类缓存查找逻辑
// 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];
}
消息转发阶段
能走到消息转发,说明
-
类自身没有该方法(
objc_msgSend的消息发送) -
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 self、SEL _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] ];
}
注意:
methodSignatureForSelector如果返回 nil,则forwardInvocation不会执行- 方法签名
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 的完整流程
- 动态方法解析
- 消息转发
消息传递流程
-
先判断是否命中缓存,利用 sel 和 _mask,计算出哈希值,然后在 bucket_list 中查找方法缓存,找到则调用。没有找对则继续查找
-
没有找到缓存,则根据对象的 isa 在类对象的方法列表中,进行方法查找
- 对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数
- 对于没有排序好的列表,采用一般遍历查找方法对应执行函数
-
如果在当前类的方法列表中没有找对方法实现,则根据类对象的 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个判断都是调用对象方法的 isMemberOfClass 、isKindOfClass
由于 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个判断都是调用类方法的 isMemberOfClass 、isKindOfClass
+ (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->superclassNSObject 元类的 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;
}
QA:NSLog(@"%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(@"hi,my 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 是一个特殊成员变量,其他的成员变量,这里就是 _name),sayHi 方法内部的 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,所以“前一个局部变量” 也就是 self(ViewController)
结构体有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。






