[OC 底层] (三) 方法缓存与消息发送机制

[OC 底层] (三) 方法缓存与消息发送机制

文章目录

前言

这篇文章的脉络我其实想从消息发送机制开始讲起,在后面我会补充方法缓存相关的内容。

探索方法的本质

在这之前我们要知道什么是消息发送,就比如我们调用了一个方法[person sayHello],但底层并不是像C/C++ 直接静态调用函数,而是会被编译成类似的以下形式:

objc 复制代码
objc_msgSend(person, @selector(sayHello));

这个本质上就是消息发送。

objc 复制代码
[person sayHello];
[person sayBye];
[person sayNB];
[person sayMaster];
//clang编译后的底层实现
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayBye"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayMaster"));

通过上面的c 底层代码可以看出,方法调用本质是消息发送,这里我们看到objc_msgSend() 的两个参数:(结构体, sel)。在这里我们要说三个有关消息发送的关键概念就是 消息接收者(要消息接受的对象)、方法选择器(SEL ,表示要调用的方法)和方法实现(本质上是一个函数指针,指向真正执行代码的地址)。

SEL

我们先看Apple公示的 SEL

objc 复制代码
typedef struct objc_selector *SEL;

SEL是一个不透明的类型,代表方法的选择子,定义如下

objc 复制代码
// GNU OC 中的 objc_selector
struct objc_selector {  
  void *sel_id;  
  const char *sel_types;  
};  

SEL是一个方法选择器,告诉编译器我们当前需要调用哪一个方法。

定义了三个 SEL 类型的变量:selAselBselC,它们都表示同一个方法选择器:sayBaGa

在这里 oc 会在编译的时候根据方法名生成唯一一个区分的 ID,这个 ID 是 SEL 类型的,只要方法名字相同,SEL 返回的就相同。

Runtime中维护了一个SEL的表,这个表按NSSet来存储 ,只要相同的SEL就会被看作是同一个方法并被加载到表中,因此OC中要尽量避免方法重载。

IMP

指向方法实现的首地址的指针,从这里可以看出它的一个定义

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

IMP 本质是一个指向函数实现地址的指针。

Method

这同样也是一个不透明的类型,表示类中定义的方法:

objc 复制代码
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name   OBJC2_UNAVAILABLE; //表示方法名的字符串
    char * _Nullable method_types   OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型
    IMP _Nonnull method_imp    OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
}                                                            OBJC2_UNAVAILABLE;

从中就可以看出 Method 是一个结构体类型指针。

Method操作函数如下

objc 复制代码
方法操作主要有以下函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); 
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)

SEL, IMP, Method

objc 复制代码
Class
 └── method list
      ├── Method 1: SEL + types + IMP
      ├── Method 2: SEL + types + IMP
      └── Method 3: SEL + types + IMP

上面就是一个很直观的 SEL ,Method, IMP 的关系图。

SEL方法选择器,实际上就是一个字符串的名字,编译器生成选择器在类加载时由运行时自动映射。可以理解为书的一个标题

IMP指向方法实现首地址的指针,理解为书的页码

Method是一个结构体,包含上述两者,同时包含char*表示函数的类型(包含返回值和参数类型),理解为书哪一章节的具体内容

SEL、IMP、Method之间的关系

当向对象发送消息时,调用SEL在对象的类以及父类方法列表中进行查找Method,因为Method结构体中包含IMP指针,因此一旦找到对应的Method就直接调用IMP去实现方法

子类调用父类方法

定义子类和父类

首先我们先定义两个类,一个类继承自另一个类

objc 复制代码
- (void) sayBye {
  NSLog(@"%@ say: %s", [self class], __func__);
  NSLog(@"%@ say: %s", [super class], __func__);
}

这里的打印结果是

这里我们可以发现为什么我们使用了 superclass 打印出来还是Person。

编译成 c 以后我们可以发现

objc 复制代码
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_CJLPerson_d760c5_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("CJLPerson"))}, sel_registerName("class")), __func__);

这里我们可以看到第二条的super变成了objc_msgSendSuper
苹果官方文档对其方法解释为:当遇到方法调用时,编译器会生成对以下函数之一的调用:objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送;其他消息使用objc_msgSend发送。使用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。

在这里我们可以重新认识一下objc_super数据结构的指针,传递值,标志消息发送的上下文,包括要接受消息的类的实例和要开始搜索方法的实现

objc 复制代码
((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}

方法的接受和查找不一定是同一个人,方法的接受者是self,方法的查找是在super中去查找的。

super只是关键字,结构体中的super_class 等于父类,代表从父类对象开始查找;不代表接收者receiver是父类对象,objc_msgSendSuper的区别在于找方法的初始位置不一样。

objc_msgSend快速查找流程分析CacheLookup

下面是源码

objc 复制代码
MSG_ENTRY _objc_msgSend
    // 定义 _objc_msgSend 函数入口。
    // Objective-C 中普通方法调用最终都会走到这里,例如:
    // [person sayNB] -> objc_msgSend(person, @selector(sayNB))

UNWIND _objc_msgSend, NoFrame
    // 栈展开相关信息。
    // NoFrame 表示这个函数没有创建标准栈帧。
    // objc_msgSend 是高频调用函数,使用汇编实现,尽量减少额外开销。
cmp p0, #0 
    // 判断消息接收者 receiver 是否为 nil。
    // objc_msgSend 的第一个参数是 receiver,通常在 x0 / p0 寄存器中。
    //
    // 例如:
    // objc_msgSend(person, @selector(sayNB))
    //
    // 此时:
    // p0 / x0 = person
    // p1 / x1 = @selector(sayNB)
    //
    // 这里就是判断 person 是否为空。
    //
    // 同时,这里也和 tagged pointer 的判断有关。
    // tagged pointer 是一种特殊的小对象优化,比如某些 NSNumber、NSString 可能不是真正的堆对象。
#if SUPPORT_TAGGED_POINTERS

b.le LNilOrTagged 
    // 如果支持 tagged pointer:
    //
    // b.le 表示 less or equal,小于等于时跳转。
    // 这里包含两种情况:
    //
    // 1. receiver == nil
    // 2. receiver 是 tagged pointer
    //
    // 如果满足条件,就跳转到 LNilOrTagged 处理。
    //
    // 注意:在某些架构下 tagged pointer 的最高位有特殊标记,
    // 所以可以通过这个比较判断是否需要进入 tagged pointer 处理流程。

#else
b.eq LReturnZero
    // 如果不支持 tagged pointer:
    //
    // b.eq 表示 equal,也就是 receiver == nil 时跳转。
    // 如果 receiver 是 nil,就直接跳转到 LReturnZero。
    //
    // Objective-C 里向 nil 发送消息不会崩溃,
    // 而是直接返回 0 / nil / 空值。

#endif
ldr p14, [x0] 
    // receiver 不为空,也不是 tagged pointer,就继续执行。
    //
    // 这里从 receiver 对象内存的起始位置读取 isa。
    //
    // Objective-C 对象的第一个成员通常就是 isa。
    // 所以:
    //
    // x0 = receiver
    // [x0] = receiver->isa
    //
    // 执行后:
    // p14 = raw isa
    //
    // raw isa 表示原始 isa,里面可能不只是单纯的 Class 指针,
    // 还可能包含一些标志位,需要进一步处理。
GetClassFromIsa_p16 p14, 1, x0 
    // 从 raw isa 中提取真正的 Class。
    //
    // 因为现代 Objective-C 使用 non-pointer isa,
    // isa 里面不一定直接就是类对象地址,
    // 它可能还包含引用计数、标志位等信息。
    //
    // 所以这里需要通过掩码等方式,从 raw isa 里取出真正的 class。
    //
    // 执行后:
    // p16 = class
    //
    // 也就是:
    // receiver -> isa -> Class
    //
    // 到这里,Runtime 已经知道应该从哪个类开始查找方法了。
LGetIsaDone:
    // 标签:表示 isa / class 获取完成。
    //
    // 普通对象会从上面的 GetClassFromIsa_p16 走到这里。
    // tagged pointer 对象经过 GetTaggedClass 后,也会跳到这里。
    //
    // 换句话说,到这个位置时,p16 里应该已经有了 class。
// calls imp or objc_msgSend_uncached

CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    // 进入方法缓存查找流程。
    //
    // 这是 objc_msgSend 的核心逻辑之一。
    //
    // 当前关键寄存器大概是:
    //
    // x0 / p0  = receiver
    // x1 / p1  = selector,也就是 _cmd / SEL
    // x16 / p16 = class
    //
    // CacheLookup 会去 class 的 cache 里查找:
    //
    // SEL -> IMP
    //
    // 如果缓存命中:
    //     直接跳转到对应 IMP 执行方法实现。
    //
    // 如果缓存未命中:
    //     跳转到 __objc_msgSend_uncached,
    //     进入慢速查找流程。
    //
    // 慢速查找大致会去:
    //
    // class -> data -> method list
    //
    // 里用 SEL 找 Method,再从 Method 中取出 IMP。
    // 如果本类没有,就沿着 superclass 往父类查找。
#if SUPPORT_TAGGED_POINTERS

LNilOrTagged:
    // 这里处理两种情况:
    //
    // 1. receiver == nil
    // 2. receiver 是 tagged pointer
b.eq LReturnZero
    // 如果 receiver == nil,跳转到 LReturnZero。
    //
    // 也就是说,向 nil 发送消息,直接返回 0。
    //
    // 例如:
    //
    // Person *p = nil;
    // [p sayNB];
    //
    // 不会崩溃,而是直接返回。
GetTaggedClass
    // 如果不是 nil,那就说明它是 tagged pointer。
    //
    // tagged pointer 不是真正的普通对象,
    // 不能像普通对象那样通过 [x0] 读取 isa。
    //
    // 所以这里要通过 tagged pointer 的编码规则,
    // 获取它对应的 Class。
    //
    // 执行后,同样会得到 class 信息。
b LGetIsaDone
    // tagged pointer 的 class 获取完成后,
    // 跳回 LGetIsaDone。
    //
    // 后续流程和普通对象一样:
    //
    // class -> cache -> SEL 查 IMP
// SUPPORT_TAGGED_POINTERS

#endif  // SUPPORT_TAGGED_POINTERS 
LReturnZero:
    // 处理 receiver == nil 的返回逻辑。
    //
    // Objective-C 允许向 nil 发送消息。
    // 这种情况下,不查方法、不走消息转发,直接返回空值。
    //
    // 根据返回值类型不同,可能需要清空不同的寄存器。

// x0 is already zero
    // x0 本身已经是 0。
    //
    // 如果返回对象指针、整数、BOOL 等,
    // x0 = 0 就表示 nil / 0 / NO。
mov x1, #0
    // 把 x1 也清零。
    // 某些返回场景可能需要多个通用寄存器配合返回。
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
    // 把浮点 / 向量返回寄存器清零。
    //
    // 如果方法返回的是 float、double、结构体中的浮点相关值,
    // 这些寄存器可能会被用作返回值。
    //
    // 向 nil 发送消息时,也应该返回 0 值,
    // 所以这里统一清空。
ret
    // 返回调用方。
    //
    // receiver 为 nil 的消息发送流程到这里结束。

END_ENTRY _objc_msgSend
    // _objc_msgSend 函数结束。

这里的主要流程是这些

复制代码
objc_msgSend(receiver, SEL)
        |
        v
判断 receiver 是否为 nil
        |
        |-- nil -> 直接返回 0
        |
        v
判断是否 tagged pointer
        |
        |-- tagged pointer -> 通过特殊规则拿到 Class
        |
        v
普通对象 -> 从 receiver 的 isa 中取出 Class
        |
        v
拿 Class 和 SEL 去 cache 里查 IMP
        |
        |-- cache 命中 -> 直接执行 IMP
        |
        |-- cache 未命中 -> 进入 __objc_msgSend_uncached 慢速查找
  • 在这块有一个很重要的一点也就是第一部分,就是怎么拿到Class,这也就是 objc_msgSend 的前半段准备工作,我们需要先判断 receiver 是否为 nil 或 tagged pointer;如果是 nil 就直接返回,如果是 tagged pointer 就特殊获取 Class;如果是普通对象,就从 receiver 的 isa 中解析出 Class,为后面的 cache 查找方法实现做准备。
  • 然后下面是第二部分,也就是快速查找的函数实现部分:
objc 复制代码
mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
	and	p12, p1, p11			// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
	// keep the remaining 38 bits for the IMP offset, which may need to reach
	// across the shared cache. This offset needs to be shifted << 2. We did this
	// to give it even more reach, given the alignment of source (the class data)
	// and destination (the IMP)
	ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs << 38) | imp_offs
	cmp	x12, x17, LSR #38

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub	x0, x16, x17        		// imp = isa - imp_offs
	SignAsImp x0, x17
	ret
.else
	b.ne	5f				        // cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
	sub x17, x16, x17               // imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17, x10
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldur	x9, [x10, #-16]			// offset -16 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

这个方法最核心的流程就是

复制代码
Class -> cache -> buckets -> 用 SEL 匹配 bucket -> 找到 IMP -> 执行

里面的内容也就是,从 Class 中取出 cache 的 buckets 和 mask,用 SEL 计算 bucket 下标,然后在 buckets 里按开放寻址规则查找匹配的 SEL;如果找到,就取出对应 IMP 并直接执行;如果遇到空 bucket 或完整扫描后仍未找到,就跳到慢速查找。

慢速查找lookUpImpOrForward

这个慢速查找就是在如果没有存放在缓存中的话,就会到慢速查找中。下面是这个的相关函数

objc 复制代码
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    lockdebug::assert_unlocked(&runtimeLock.get());

    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // 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.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    lockdebug::assert_locked(&runtimeLock.get());
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    // Has this class been disabled? Act like a message to nil.
    if (!cls || !cls->ISA()) {
#if __arm64__
        imp = _objc_returnNil;
        goto done;
#elif __x86_64
        if (behavior & LOOKUP_FPRET)
            imp = _objc_msgNil_fpret;
        else if (behavior & LOOKUP_FP2RET)
            imp = _objc_msgNil_fp2ret;
        else
            imp = _objc_msgNil;

        // We can't cache these on x86, in case some other caller tries sending
        // this selector with a different return type. If we con't cache then we
        // always come back here, and always choose the correct IMP for the
        // caller's expected return type.
        behavior |= LOOKUP_NOCACHE;

        goto done;
#else
#error Don't know how to handle messages to disabled classes on this target.
#endif
    }

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

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

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
#if CONFIG_USE_PREOPT_CACHES
 done_unlock:
#endif
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

下面是完整的流程图

objc 复制代码
objc_msgSend
    |
    v
CacheLookup 快速查找
    |
    |-- cache 命中 -> 直接执行 IMP
    |
    |-- cache 未命中
            |
            v
    __objc_msgSend_uncached
            |
            v
    lookUpImpOrForward
            |
            v
    检查类状态 / 加锁 / realize / initialize
            |
            v
    curClass = cls
            |
            v
    查 curClass 的 method list
            |
            |-- 找到 Method -> 取 IMP -> done
            |
            |-- 没找到
                    |
                    v
            curClass = curClass->superclass
                    |
                    |-- nil -> imp = forward_imp -> break
                    |
                    v
            查父类 cache
                    |
                    |-- 命中 IMP -> done
                    |
                    |-- 命中 forward_imp -> break
                    |
                    |-- 未命中 -> 下一轮查父类 method list
            |
            v
    如果整个继承链都没找到
            |
            v
    动态方法解析 resolveMethod_locked
            |
            v
    如果还是没处理 -> 消息转发
            |
            v
    找到 IMP 后写入 cls.cache
            |
            v
    返回 IMP

在这里我们在类和父类继承链中查找IMP的时候,有一个重要的函数逻辑,就是查找的相关方法

getMethodNoSuper_nolock方法

这块有方法调用链

objc 复制代码
getMethodNoSuper_nolock(curClass, sel)
    -> search_method_list_inline(method_list, sel)
        -> findMethodInSortedMethodList(sel, method_list)   // 有序:二分查找
        -> findMethodInUnsortedMethodList(sel, method_list) // 无序:线性遍历
    -> method_t
    -> meth->imp(false)

这个二分查找只是查找一个已经有序表的调用的函数

objc 复制代码
template<class compareFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const compareFunc &compare)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;

    uint32_t count;

    // When to stop the binary search and move to a linear search.
    const uint32_t threshold = 4;

    for (count = list->count; count > threshold; count >>= 1) {
        auto probe = base + (count >> 1);

        int comparison = compare(probe);
        if (comparison == 0) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && compare(probe - 1) == 0) {
                probe--;
            }
            return &*probe;
        }

        if (comparison > 0) {
            base = probe + 1;
            count--;
        }
    }

    // Once we've shrunk the range enough, it's faster to do a linear search.
    while (count-- > 0) {
        auto comparison = compare(base);
        if (comparison == 0)
            return &*base;
        if (comparison < 0)
            return nil;
        base++;
    }

    return nil;
}

下面有个图可以直接来解说这个比较重要的查找函数

我们可以给出一个较为完整的流程来去看一下所有函数调用的流程,

在当前类 curClass 中,根据 sel 查找方法;

具体查找时,会遍历当前类的方法列表;

如果某个方法列表是有序的,就用二分查找;

如果方法列表是无序的,就用线性遍历;

找到后得到 method_t;

再从 method_t 中取出 IMP;

最后执行这个 IMP`

动态方法解析

前面在提到查找流程的时候,我们如果在前两次都没找到对应的方法,这个时候还有一个补救方法就是动态方法解析。

下面是相关源码

objc 复制代码
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    lockdebug::assert_locked(&runtimeLock.get());
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    }
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

这块有几个重要内容

  1. 如果是类,执行实例方法的动态决议方法resolveInstanceMethod
  2. 如果是元类,执行类方法的动态决议方法resolveClassMethod,如果在元类中没有找到或者未空,则在元类的实例方法的动态决议方法resolveInstanceMethod(因为类反复该在元类中是实例方法),所以还需要查找元类中实例方法的动态决议.
  3. 如果动态方法决议中,将其指向了其他方法,则要继续查找对应的一个imp,就是慢速查找流程

实例方法

下面是实例方法的源码

objc 复制代码
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    lockdebug::assert_unlocked(&runtimeLock.get());
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, 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 = lookUpImpOrNilTryCache(inst, sel, cls);

    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));
        }
    }
}

在这里他多次调用了lookUpImpOrNilTryCache,我们可以看一下这个函数的逻辑:

objc 复制代码
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    lockdebug::assert_unlocked(&runtimeLock.get());

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        // dtrace probe
        OBJC_RUNTIME_CACHE_MISS(inst, sel, cls);

        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

我们可以总结一下里面的流程:

  1. 确认没加锁
  2. 确认 cls 已 realized
  3. 准备 resolve_sel = @selector(resolveInstanceMethod:)
  4. 查找 cls 的元类链里有没有 +resolveInstanceMethod:
  5. 如果没有,直接 return
  6. 如果有,主动调用 [cls resolveInstanceMethod:sel]
  7. 开发者可以在这个方法里 class_addMethod
  8. 调用完成后,重新查找原来的 sel
  9. 如果找到 IMP,说明动态解析成功
  10. 如果没找到,后续会进入消息转发

类方法

resolveClassMethod: 是在类方法找不到的时候用

objc 复制代码
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    lockdebug::assert_unlocked(&runtimeLock.get());
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(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 = lookUpImpOrNilTryCache(inst, sel, cls);

    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));
        }
    }
}

下面是 class 相关的流程

objc 复制代码
[Person sayHello]
        |
        v
Person 元类 cache 查找失败
        |
        v
Person 元类 method list 查找失败
        |
        v
父元类链查找失败
        |
        v
进入 resolveClassMethod
        |
        v
查找 +resolveClassMethod: 是否存在
        |
        v
调用 [Person resolveClassMethod:@selector(sayHello)]
        |
        v
class_addMethod(object_getClass(self), @selector(sayHello), imp, types)
        |
        v
重新查找 Person 元类中的 sayHello
        |
        v
找到 IMP
        |
        v
动态解析成功
        |
        v
返回 IMP
        |
        v
执行 IMP

流程小结:

  1. 会从元类的继承链里找resolveClassMethod的imp;
    如果本类里实现了+(Bool)resolveClassMethod方法,则在元类里能找到imp;
    如果本类里没有实现它,则最终在根元类NSObject里找imp,因为NSObject类里默认声明了+(Bool)resolveClassMethod方法实现。
  2. 获得这个类的元类,进入这个getMaybeUnrealizedNonMetaClass方法里面,将这个nonmeta赋值成类对象
  3. 主动发送resolveClassMethod,不管是在那一个层级找到的都会把它赋值到我们的本类的一个cache中
  4. 获取之前发送的一个imp.
  5. 这个resolveClassMethod不生效之后,再去在元类中查找那个实例方法resolveInstanceMethod(因为类方法在元类中是以实例方法的样式来存储的)

崩溃修改

我们其实可以实现一个保底机制

objc 复制代码
#import <Foundation/Foundation.h>
#import "LKPerson.h"
#import "LKPerson2.h"
@interface Person : NSObject
@property (nonatomic, copy) NSString *LK_Name;
@property (nonatomic, strong) NSString *nickName;
- (void) sayHello;
- (void) sayCode;
- (void) sayMaster;
- (void) sayBaGa;
- (void) sayBye;
@end

@implementation Person
//- (void) sayHello {
//  NSLog(@"Person say : %s", __func__);
//}
- (void)sayCode {
  NSLog(@"Person say : %s", __func__);
}
- (void)sayBaGa {
  NSLog(@"Person say : %s", __func__);
}
- (void)sayMaster {
  NSLog(@"Person say : %s", __func__);
}
//- (void) sayBye {
//  NSLog(@"%@ say: %s", [self class], __func__);
//  NSLog(@"%@ say: %s", [super class], __func__);
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%@, 保底机制", NSStringFromSelector(sel));
//    if (sel == @selector(sayHello)) {
//        IMP imp = class_getMethodImplementation(self, @selector(sayNB));
//        Method sayMethod = class_getInstanceMethod(self, @selector(sayNB));
//        const char* type = method_getTypeEncoding(sayMethod);
//        return class_addMethod(self, sel, imp, type);
//    }
    return [super resolveInstanceMethod:sel];
}

@end

@interface Teacher : Person
@end

@implementation Teacher
@end


#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//      Person *p = [[Person alloc] init];
//      NSObject *obj1 = [NSObject alloc];
//      LKPerson2 *obj3 = [LKPerson2 alloc];
//      NSLog(@"Hello, World!");
//      NSObject *obj1 = [NSObject alloc];
//      LKPerson *obj2 = [LKPerson alloc];
//      LKPerson *obj4 = [LKPerson alloc];
//      Person *person = [Person alloc];
//      NSLog(@"%@", person);
//      NSLog(@"hello world %@ - %@", obj1, obj2);
      Person *p = [Person alloc];
      Class pClass = [Person class];
      [p sayBye];
      [p sayHello];
//      [p sayHello];
//      [p sayCode];
//      [p sayMaster];
//      NSLog(@"%@", pClass);
//      SEL selA = @selector(sayBaGa);
//      SEL selB = sel_registerName("sayBaGa");
//      SEL selC = NSSelectorFromString(@"sayBaGa");
//      NSLog(@"123");
//      [p sayBaGa];
    }
    return 0;
}

动态方法添加函数

objc 复制代码
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

我们先解释一下这个的用意,给 cls 这个类添加一个叫 name 的方法这个方法执行 imp 指向的代码

方法的返回值和参数类型由 types 描述

然后我们来介绍一下这个核心方法,我们先从参数开始,
Class cls:给哪个类添加方法
SEL name:要添加的方法名
IMP imp:方法的真实实现
const char *types:方法类型编码

最后我们看返回值,返回 YES 表示添加成功。返回 NO 通常表示这个类中已经有这个方法了,添加失败。

这时我们回过头来看一下,我们把 +resolveInstanceMethod:(SEL)sel 这个方法这么写

objc 复制代码
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%@, 保底机制", NSStringFromSelector(sel));
    if (sel == @selector(sayHello)) {
        IMP imp = class_getMethodImplementation(self, @selector(sayBaGa));
        Method sayMethod = class_getInstanceMethod(self, @selector(sayBaGa));
        const char* type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

就可以真正意义上实现一个保底的效果:

消息转发

快速转发 forwardingTargetForSelector

我们先看下快速转发的入口在哪里

objc 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector;

快速转发,它的本质也就是把当前的消息交转给另一个对象处理

objc 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello)) {
        return [Teacher new]; // 返回其他类中实现过这个方法的方法.
    }
    return [super forwardingTargetForSelector:aSelector];
}

这样就可以让 class1 转发到 class2 了。

慢速转发 (methodSignatureForSelector: / forwardInvocation:)

这个是最后一次挽救的机会,那么如何会进入一个慢速转发呢?如果原对象的快速转发没有返回有效的新接收者,那么原对象就会进入慢速转发。

如果是如下情况就会进入慢速转发

objc 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return [super forwardingTargetForSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;
}

下面是慢速转发需要实现的方法

objc 复制代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

上面的这两个方法是都需要实现的,如果需要实现慢速转发的话

  • 第一个方法 这个方法的作用是:告诉 Runtime,这个未知方法的参数和返回值长什么样。
  • 第二个是建立在第一个以上如果 methodSignatureForSelector: 返回了有效的方法签名,Runtime 就会把原来的消息调用封装成一个 NSInvocation 对象,然后调用:- (void)forwardInvocation:(NSInvocation *)anInvocation;这个 NSInvocation 里面包含了这次调用的完整信息,比如:
objc 复制代码
target:原始接收者
selector:原始方法名
arguments:参数
return value:返回值位置
method signature:方法签名

总结

其实一句话就可以概括完消息转发就是先查缓存和方法列表,找不到就动态解析,再不行就消息转发,最后仍没人处理才崩溃。​

最终最终的本质就是一个消息发送,SEL-IMP 的查找过程。

最后拿两张图来结尾

相关推荐
kyrie学java10 小时前
多级缓存
缓存
月诸清酒11 小时前
豆包输入法 Mac 版上线了,跟我之前用的 Typeless 对比了下
macos
星辰即远方11 小时前
UICollectionView
macos·objective-c·cocoa
水云桐程序员12 小时前
APP 的架构设计
macos·objective-c·cocoa·软件工程
YL2004042612 小时前
【Redis基础篇】Redis常见命令
数据库·redis·缓存
Jing_jing_X12 小时前
DeepSeek 的上下文缓存是什么?它和程序里的 Redis 缓存一样吗?
redis·spring·缓存·ai
ujainu小13 小时前
CANN hixl:大模型 PD 分离场景的零拷贝通信库
android·java·缓存
开开心心loky13 小时前
[OC 底层] (四) 多线程相关内容
macos·ios·objective-c·cocoa
白玉cfc14 小时前
【iOS】底层原理:理解dyld
macos·objective-c·cocoa