【OC源码】Runtime | 三. runtime 的消息机制 & 围绕消息机制设计的数据结构

前文 一. 起源 — 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

  • 消息查找:
    1. 缓存中查找(id.isa -> objc_class -> objc_class.cache -> objc_class.cache.buckets)
    2. 函数表中查找, 其中排过序的用二分查找, 未排序的直接遍历(objc_class -> objc_class.bits)
    3. 递归, 在父类中查找(objc_class -> objc_class.superClass)
  • 动态解析:
    1. 发送另一个消息 resolveClassMethod, resolveInstanceMethod, 尝试添加函数
    2. 再执行”消息查找”, 尝试再次找到 IMP
  • 消息转发:
    1. 发送消息 forwardingTargetForSelector, 尝试换一个消息接收者
    2. 发送消息 methodSignatureForSelector, 打包函数签名
    3. 发送消息 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)  

所以如果我们要实现消息动态解析, 只需要在子类中, 重写 NSObjectresolveXXX 函数.并在其中调用
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];  
}  

后面 methodSignatureForSelectorforwardInvocation是需要子类实现后, 将消息签名打包. 然后发送给未知对象处理(这块理解不深, 暂时到这)

围绕 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:用于查找函数过程中, 自身查找失败后, 逐步遍历 super
  • cache: 存放着 {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, value
  • mask: 代表用来缓存的大小
  • _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将类自己实现的方法(包括分类)、属性和遵循的协议加载到methodspropertiesprotocols列表中

源码分析过程中也常见的判断 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)
    1. 先找到 reciver 的 isa 既 object class
    2. object class 缓存和函数表中查不到后, 找到 super class
    3. super class 逐级递归到 nil

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