[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 的查找过程。

最后拿两张图来结尾

相关推荐
随风一样自由1 小时前
【前端+React+缓存竞态】导航切换缓存刷新机制
前端·react.js·缓存
itwangyang5204 小时前
Claude Code macOS 安装完整指南:Homebrew、原生脚本与常见问题解决
macos
桌面运维家12 小时前
如何用半缓存云桌面将服务器硬盘容量扩展至本地终端?
运维·服务器·缓存
YOU OU18 小时前
Redis初识
数据库·redis·缓存
livemetee20 小时前
【关于redis高性能,高可用处理】
数据库·redis·缓存
汤姆yu1 天前
macOS系统下Aider完整安装、配置与实战使用教程
大数据·人工智能·算法·macos·github·copilot
wabil1 天前
【LVGL】滑动切换页面的界面优化实践
开发语言·ios·swift
威武的花瓣1 天前
调用Page.RegisterAsyncTask()的异步页
ios·iphone
青山木1 天前
Hot 100 --- LRU 缓存
java·数据结构·算法·leetcode·链表·缓存·哈希
无小道1 天前
Redis——哨兵
数据库·redis·缓存·哨兵