前文 一. 起源 — runtime 要解决什么 & 为什么这样设计 中提过,
Smalltalk 设计者 Alan Key 期望程序像细胞一样,独立运行, 并且通过消息传递而非互相调用的方式来作为沟通机制
消息传递是其 “面向对象” 的核心.
这篇主要总结 runtime 的 ”消息机制”, 并且会发现, runtime 的数据结构很大程度上是为”消息机制”服务的
其源码主要在以下文件中:
objc-msg-arm.s
objc-msg-arm64.s
objc-runtime-new.mm
对应官方文档: Messaging
源码分析流程我总结在: 其他: 源码中 objc_msgSend 分析
需要具备 runtime 的数据结构知识: 二. runtime 怎么实现封装 | runtime 的基础数据结构
objc_msgSend 是什么
先看下面一段代码
//Test.m
- (void)testFunction{}
+ (void)testClassFunction{}
//Main.m
[Test testClassFunction];
[test testFunction];
// 编译后
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Test"), sel_registerName("testClassFunction"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("testFunction"));
OC “类”/”对象” 调用函数通过[]语法, 经过预编译后:
OC (成员\类)调用函数, 本质是给这个 “成员”\“类”发消息)
我们把所有的类型转换去掉, 最终结果就是执行 objc_msgSend 函数,传入两个参数: 调用者(“类结构体指针”,”对象指针”), 消息:(SEL)
void objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend 的流程
objc_msgSend 执行后的逻辑如图, 核心源码分析: 其他: 源码中 objc_msgSend 分析

上述流程中:(逻辑流程, 另外代码调用流程图待整理)
- 消息传递分成三个阶段, 消息查找、动态解析、消息转发. 前面阶段失败的情况下才会进行后一步
-
这里面”查找”: 指的是利用入参的
SEL, 在入参的id的缓存, 函数表, 其逐层的父类中找到SEL对应的IMP - 消息查找:
- 缓存中查找(
id.isa -> objc_class -> objc_class.cache -> objc_class.cache.buckets) - 函数表中查找, 其中排过序的用二分查找, 未排序的直接遍历(
objc_class -> objc_class.bits) - 递归, 在父类中查找(
objc_class -> objc_class.superClass)
- 缓存中查找(
- 动态解析:
- 发送另一个消息
resolveClassMethod,resolveInstanceMethod, 尝试添加函数 - 再执行”消息查找”, 尝试再次找到 IMP
- 发送另一个消息
- 消息转发:
- 发送消息
forwardingTargetForSelector, 尝试换一个消息接收者 - 发送消息
methodSignatureForSelector, 打包函数签名 - 发送消息
forwardInvocation, 处理函数签名
- 发送消息
其中从动态解析开始, OC 提供几个函数, 供自定义类覆写, 用于处理消息查找失败时的解决方案
这些函数在NSObject中有默认实现, 比如常见的”unrecognized selector sent to instance”
这些函数灵活使用, 可以完成类似多继承等 OC 本身所不具有的特性:官方文档: Message Forwarding
- 用于动态添加消息:
resolveClassMethod&resolveInstanceMethod - 用于修改消息接收对象:
forwardingTargetForSelector - 用于打包并处理消息:
methodSignatureForSelector&forwardInvocation
自定义的消息 ”动态解析” 和 “转发”
官方文档: Dynamic Method Resolution
动态解析
前面提到, 在找不到函数后, 底层会执行消息动态解析.
其调用的函数resolveClassMethod & resolveInstanceMethod 被实现在 NSObject 中
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- 入参为 SEL, 返回值为 BOOL. 根据 objc_msgSend 的流程看, 其返回值并不会被用到.
- 不管内部执行与否, 后续都会调用
lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE); - 所以只要我们在这两个函数中, 将 sel 对应的函数加入 cache 中, 即可实现动态解析
对此,OC 提供了 (其实现在源码中能搜到, 主要是加入缓存, 不再赘述)
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
所以如果我们要实现消息动态解析, 只需要在子类中, 重写 NSObject 的 resolveXXX 函数.并在其中调用
class_addMethod(返回值不会影响动态解析结果)即可(比如指定另一个 IMP 对应当前 sel, 第四个入参 type encoding 前面有介绍 其他: Type Encodings )
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
Method method = class_getInstanceMethod(self, @selector(otherMethod));
class_addMethod(self,
sel,
method_getImplementation(method),
method_getTypeEncoding(method)
);
return YES;
}
return [super resolveInstanceMethod:sel];
}
消息转发
先看 NSObject 实现的源码
// 消息转发, 一个类方法, 一个成员方法, 实现一样
- (id)forwardingTargetForSelector:(SEL)sel
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
// 打包函数签名, 一个类方法, 一个成员方法, 实现一样
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// 处理函数签名, 一个类方法, 一个成员方法, 实现一样
- (void)forwardInvocation:(NSInvocation *)invocation
+ (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
// 函数报错
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
自定义消息转发很简单, 子类覆写forwardingTargetForSelector, 并在其内部返回要接收的 Target
- (id) forwardingTargetForSelector:(SEL) sel
{
if (sel == @selector(xxx)) {
return [SubClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
后面 methodSignatureForSelector和forwardInvocation是需要子类实现后, 将消息签名打包. 然后发送给未知对象处理(这块理解不深, 暂时到这)
围绕 objc_msgSend 实现的数据结构
可以看到的是, objc_sendMsg 的实现与 struct objc_class 的数据结构息息相关.
换句话说, 结合前面 一. 起源 — runtime 要解决什么 & 为什么这样设计
可以认为 runtime 的核心就是围绕 objc_sendMsg 设计的, 包括其流程, 其数据结构.
struct objc_object: 对象本身
里面存放着 isa 用于找到与对象关联的类结构体objc_class
struct objc_object {
isa_t isa;
}
struct objc_object: 类的数据结构
在汇编阶段由 isa 指针偏移获得(objc_msgSend 源码主线流程)
superClass:用于查找函数过程中, 自身查找失败后, 逐步遍历 supercache: 存放着 {SEL, IMP}bits: 封装过的类整体信息(‘变量’, ‘函数’,’协议’等), 用于在缓存查找失败后, 到函数表中查询typedef struct objc_class *Class; struct objc_class : objc_object { Class superclass; // 缓存 cache_t cache; // 类整体信息 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags //... 后面是一堆处理函数 }
struct cache_t: 缓存的数据结构
里面的 buckets 在汇编阶段由 cache 指针偏移获得(objc_msgSend 源码主线流程)
- 内部存放着
buckets数组 bucket_t存放着一对 key, valuemask: 代表用来缓存的大小_occupied: 代表实际大小
(这里面还涉及哈希扩容等情况,newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)
(网上其他文章中 cache 有对应的读 cache C++版源码, 可能因为版本原因, 我只找到汇编的)struct cache_t { // 哈希表 explicit_atomic<struct bucket_t *> _buckets; mask_t _mask; mask_t _occupied; //... } struct bucket_t { // 一个 key(SEL), 一个 value(imp) explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; }
struct class_data_bits_t & struct class_rw_t: 类信息的封装结构体
class_data_bites_t 主要是 class_rw_t* 的二次次封装,
内部的 bit 与不同的FAST_ 前缀的 flag 掩码做按位与操作,可以获取不同的数据
(具体代码在objc-runtime-new.h中有定义一系列的FAST_)
(前文提到过的 Tagged Pointer 机制其他: Tagged pointer 与 isa )
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
struct class_rw_t 可以视为 struct class_or 的二次封装
这个 struct class_or_t 就是前面编译代码后遇到的那个其他: Clang 编译后的数据结构分析
- 包含函数表, 属性表, 协议表
- 一个
struct class_ro_t指针(源码在上面链接中)struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint16_t witness; explicit_atomic<uintptr_t> ro_or_rw_ext; Class firstSubclass; Class nextSiblingClass; using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>; const ro_or_rw_ext_t get_ro_or_rwe() class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false); class_rw_ext_t *ext() class_rw_ext_t *extAllocIfNeeded() const class_ro_t *ro() const method_array_t methods() const property_array_t properties() const protocol_array_t protocols() };
回顾 二. runtime 怎么实现封装 runtime 的基础数据结构, 编译后的 函数 和 变量 被装到 class_or
struct class_rw_t 描述了方法表,变量表,协议表等
runtime 很多东西是动态添加/修改的, 其既需要兼顾编译时加入类的数据, 还要兼顾很多运行时才加入的数据
(下面的结论我在代码中没有找到, 是从参考文章中得来深入解析 ObjC 中方法的结构)
- 编译期间,
class_data_bits_t *data指向的是class_ro_t *指针 - 在程序启动后,加载运行时时调用
realizeClass函数
1. 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
2. 初始化一个 class_rw_t 结构体
3. 设置结构体 ro 的值以及 flag
4. 最后设置正确的 data realizeClass调用methodizeClass将类自己实现的方法(包括分类)、属性和遵循的协议加载到methods、properties和protocols列表中
源码分析过程中也常见的判断
realizeClass, 在类初始化以前返回的指针其实就是class_ro_t
这个static Class realizeClass(Class cls)在被调用后才会开辟class_rw_t空间, 赋值class_rw_t->ro
作为全局查找索引的 isa
上面流程可以看到 objc_msgSend 的流程, 不论查找缓存还是查找函数表, 全局都是通过 isa 在做索引
可以理解为 isa 的设计目的, 就是让”对象”能找到”类”, 类能找到”元类”, 从而实现函数查找用的.
我通过代码对 isa 指向的探究 其他:探究 isa 的指向
对于 “对象” , “类”, “元类” 间的关系, 见 二. runtime 怎么实现封装 | runtime 的基础数据结构
这两张出自苹果方法, 很常见的 isa 传递图
- 下图, 描述的是 isa 的指向,
struct objc_object&struct objc_object生成的一系列描述类的”对象”, “类”,”元类”串起来的流程. - 对于 isa:
- 对象
struct objc_object *的 isa 指向其类struct objc_class - 类
struct objc_class的 isa 指向其元类struct objc_class - 所有的元类
struct objc_class, isa 指向 NSObject 的元类
- 对象
- 对于 superclass:
- 类
struct objc_class的 superclass 指向其父类 - 元类
struct objc_class的 superclass 指向上一级元类 - ⚠️最需要注意的: (这在找最终函数归属时有帮助)
- NSObjectMetaClass 的 isa 指向其本身
- NSObjectMetaClass 的 superclass 指向 NSObjectClass

- 类
- 下图描述的是在消息查找阶段, 查找的走向(Messaging)
- 先找到
reciver的 isa 既 object class - object class 缓存和函数表中查不到后, 找到 super class
- super class 逐级递归到 nil

- 先找到
参考资料: 深入解析 ObjC 中方法的结构