iOS 底层探索 方法的本质

689 阅读12分钟

1.OC方法调用的本质

将一段简单的方法调用代码,放在main方法中,然后使用clang命令编译。

 ZPerson *person = [ZPerson alloc];
 [person sayHello];

然后使用clang -rewrite-objc main.m命令编译。得到如下编译后的源码:

 ZPerson *person = ((ZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

去掉强制类型转换后:

objc_msgSend(objc_getClass("ZPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));

通过上面这段代码,我们发现不管是alloc方法调用还是sayHello方法的调用,都是通过objc_msgSend函数来实现的。 因此我们可以说OC方法的本质就是通过objc_msgSend来发送消息。 objc_msgSend包含有方法的调用的两个隐藏参数:self(消息接受者)sel(方法编号)sel_registerName等同于oc中的@selector(),可以根据传进的方法名得到一个sel

2.objc_msgSend

2.1 objc_msgSend的方法实现

在源码总查找objc_msgSend方法,只能找到方法定义的声明,不能点进去看到方法的实现。

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这是因为objc_msgSend是由汇编语言实现的。原因有二:

  • 从性能方面考虑,方法调用需要被快速的处理和响应,而汇编更容易被机器识别。
  • 由于未知参数的原因(个数未知、类型未知,比如NSLog()),c和c++作为静态语言,并不能满足这一特性。

根据一条约定俗成的原则,我们在objc_msgSend前加上下划线,我们继续搜索_objc_msgSend,以期找到objc_msgSend的汇编源码。 根据不同的平台,obcj提供了不同版本的方法实现,我们选择arm64版本进行分析。ENTRY为方法的入口标志,我们将从开始入手分析。

2.2 _objc_msgSend

objc-msg-arm64.s中找到ENTRY _objc_msgSend,这是_objc_msgSend函数的入口。本人汇编水平有限,根据网上查找的资料,源码做了如下注释。

//_objc_msgSend 方法入口
	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

    // p0 和 0 比较,即判断接收者是否存在,
    // 其中 p0 是 objc_msgSend 的第一个参数,消息接收者 receiver
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //
// 支持 tagged pointer 的流程, 并且比较的结果 le 小于或等于
// 跳转到 LNilOrTagged 标签处执行 Taggend Pointer 对象的函数查找及执行
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
// p0 等于 0 的话,则跳转到 LReturnZero 标签处
// LReturnZero 置 0 返回 nil 并直接结束 _objc_msgSend 函数
	b.eq	LReturnZero
#endif

// 不过方法接收者不为nil 就会继续向下执行
// p0 即 receiver 肯定存在的流程,实际规定是 p0 - p7 是接收函数参数的寄存器
// 从 x0 寄存器指向的地址取出 isa,存入 p13 寄存器
	ldr	p13, [x0]		// p13 = isa
// 从 isa 中获取类指针并存放在通用寄存器 p16 中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

// 本地标签(表示获得 isa 完成)
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
// 如果有 isa,走到 CacheLookup 即缓存查找流程,也就是所谓的 sel-imp 快速查找流程,
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil 检测,如果是 nil 的话也跳转到 LReturnZero 标签处
	b.eq	LReturnZero		// nil check
//从tagged pointer指针中查找class, 并存放到x16中
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
    // 置 0
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
    // return 结束执行
	ret

    // LExit 结束 _objc_msgSend 函数执行
    END_ENTRY _objc_msgSend

前面我们已经分析对象和类的结构,此时对照前面的知识,再来看_objc_msgSend的方法流程,应该不会陌生了。 首先是检查方法接收者是否为nil。 然后是从对象中获取isa, 然后根据isa获取class。我们知道,对象的方法是存在class里的。 拿到class后,就可以去根据方法签名(sel)去查找方法的实现(imp)了。 在class中查找imp,有两种途径:

  • 一是利用cache_t,查看缓存中有没有,也就是常说的快速查找
  • 二是利用class_data_bits_t,一步步深入查找,可以参考前面的类的结构之数据存储。效率不如第一种方式高,也叫慢速查找

objc_msgSend的大致流程就是这些了,可以结合下图理一下思路。 具体方法的实现的查找流程,请看下面对CacheLookup的分析。

3.CacheLookup 快速查找

CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant

NORMAL|GETIMP|LOOKUP 分别代表三种不同模式:

  • NORMAL,正常模式,找到对应的IMP, 并执行
  • GETIMP,找到IMP, 并返回
  • LOOKUP,仅查找IMP

<function> 代表了在哪个函数中使用CacheLookup代码块。比如_objc_msgSend函数中,使用时,传递过来的就是_objc_msgSend

CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

MissLabelDynamicMissLabelConstant:当没能找到缓存时,执行的代码块,在外部执行CacheLookup时传入。MissLabelConstant仅在GETIMP模式下使用。

CacheLookup准备执行时,p1 = sel, p13 = isa,p16 = class

当执行结束时,如果从缓存中找到对应的IMP,x16 = class, x17 = IMP。如果没有找到,x15 = class

3.1 CacheHit

CacheHit 是缓存命中时,指定的代码宏定义。 缓存命中:x17 IMP的地址, x10 buckets 的地址, x1 中存的是SEL, x16 中保存类指针

.macro CacheHit
.if $0 == NORMAL // NORMAL 表示通常情况下在缓存中找到了函数执行并返回
// TailCallCachedImp 定义在 arm64-asm.h 中
// 验证并执行 IMP
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP // GETIMP 仅在缓存中查找 IMP
// p17 中是 cached IMP,放进 p0 中
	mov	p0, p17
// cbz 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
// 如果 p0 是 0,则跳转到 标签 9 处,标签 9 处直接执行 ret
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
// return IMP
9:	ret				// return IMP

.elseif $0 == LOOKUP // LOOKUP 进行查找
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.

// 不去检测imp是否为nil, 也不关心跳转是否会失败
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
// .abort 停止汇编
// Linux内核在发生 kernel panic 时会打印出 Oops 信息,
// 把目前的寄存器状态、堆栈内容、以及完整的 Call trace 都 show 给我们看,
// 这样就可以帮助我们定位错误。
.abort oops
.endif
.endmacro// 结束 CacheHit 汇编宏定义

3.2 TailCallCachedImp

当缓存命中时,如果缓存的查找模式是NORMAL,就会执行TailCallCachedImp。 验证并执行 IMP。

.macro TailCallCachedImp

   // eor 异或指令(exclusive or)
   // eor 指令的格式为:eor{条件}{S}  Rd,Rn,operand
   // eor 指令将 Rn 的值与操作数 operand 按位逻辑”异或”,
   // 相同为 0,不同为 1,结果存放到目的寄存器 Rd 中。
   
   // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
   
   // 把 SEL 和 imp 的地址按位进行异或操作,
   // 并把结果放在 $1 中 (混合 SEL 到 ptrauth modifier 中)
	eor	$1, $1, $2	// mix SEL into ptrauth modifier
    // 把 isa 和 $1 按位进行异或的操作放在 $1 中 (混合 isa 到 ptrauth modifier 中)
	eor	$1, $1, $3  // mix isa into ptrauth modifier
// 验证 ptrauth modifier,验证通过就跳转到 $0分支中执行
	brab	$0, $1
.endmacro

3.3 __objc_msgSend_uncached

__objc_msgSend_uncached是在_objc_msgSend中查找方法缓存是,传入的方法。当没有找到缓存中没有找到对应的imp时,就会执行__objc_msgSend_uncached,进入慢速查找流程,将在后面深入探究。

STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

3.4 CacheLookup

下面开始进入本节的正题——CacheLookup

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
    // 重启协议:
	//
	//   As soon as we're past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
    //   一旦超过 LLookupStart$1 标签,我们可能已经加载了无效的 缓存指针 或 掩码。
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
    //   requirements:
    //   当我们在超过 LLookupEnd$1 之前(或当 信号   命中我们)调用task_restartable_ranges_synchronize(),我们的 PC 将重置为  LLookupRecover$1,这将强制跳转到缓存未命中的代码路径,其中包含以下内容。

	
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
    //     缓存未命中只是返回 NULL
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver // x0 存放函数接收者 (就是我们日常的 self)
	//   - x1 contains the selector // x1 存放 SEL (就是我们日常的 @selector(xxxx))
	//   - x16 contains the isa     // x16 是 class 的 isa (也就是 self 的 isa,根据它来找到对象所属的类)
	//   - other registers are set as per calling conventions // 其它寄存器根据调用约定来设置
	//

//将原始的isa存储到x15
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS

//#define CACHE            (2 * __SIZEOF_POINTER__)   就是16
//将存储器地址为(x16+16)的字数据读入寄存器 p10
//类的地址偏移16个字节后,刚好是cache_t的起始地址,地址上的数据, 也就是cache_t的第一个成员变量_bucketsAndMaybeMask
//将cache的内容读取到p10
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
//p10存储的数据右移48位 后 存入p11, 此时11为mask
	lsr	p11, p10, #48			// p11 = mask
//p10 和 bucketsmask 与运算后 再存入p10  此时p10存的是buckets地址
	and	p10, p10, #0xffffffffffff	// p10 = buckets
//w小端模式 x1是sel x11是mask
//计算后 x12存的的当前sel经过hash计算后得到的key
	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

// 在 Project Headers/arm64-asm.h 中可以看到 PTRSHIFT 的宏定义
/*
    #if __arm64__
    #if __LP64__ // 64 位系统架构
    #define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE // 0b1000 表示一个指针 8 个字节
    // "p" registers are pointer-sized
    // true arm64
    #else
    // arm64_32 // 32 位系统架构
    #define PTRSHIFT 2  // 1<<PTRSHIFT == PTRSIZE // 0b100 表示一个指针 4 个字节
    // "p" registers are pointer-sized
    // arm64_32
    #endif
 */


// p12, LSL #(1+PTRSHIFT) 将p12(key) 逻辑左移4位, 左移相等于 *16(bucket大小), 又由于key相当于bucket在bucket中的下标, 所以这一步就是 计算出内存偏移量
// p10 是buekets的首地址, 与偏移量相加 ,计算出bucket 实际的内存地址, 存到p13
// p13 就是buckets中下标为key的元素的 地址
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//ldp 出栈指令(`ldr` 的变种指令,可以同时操作两个寄存器)
//将x13偏移BUCKET_SIZE (16) 个字节的内容取出来, 分别存入x17 和 x9
//p17是imp p9是sel
//然后将bucket指针前移一个单位长度
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
//比较sel和cmd 比较从buckets中取出的sel和传入的sel是否相等
	cmp	p9, p1				//     if (sel != _cmd) {
//如果不相等, 就跳转到3执行
	b.ne	3f				//         scan more
						//     } else {
//如果相等 缓存命中 跳转到CacheHit执行
2:	CacheHit \Mode				// hit:    call or return imp
						//     }

//如果p9为0 取出的sel为空 未找到缓存 就跳转到 __objc_msgSend_uncached执行
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
//比较bucket地址和buckets地址
//当bucket >= buckets 还成立时 跳回1 继续执行 哈希探测
	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:
//最后强制跳转到cache miss
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

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// 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
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 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

CacheLookup的流程大致如下:

  • 先将原始的isa存储到x15
  • 再经过对class的一系列位移操作,得到存放缓存的buckets
  • 对当前sel进行哈希计算,得到对应下标key
  • 根据下标,取出buckets中对应的缓存bucket,同时也拿到了bucketselimp
  • 然后就是一些判断:
    • 如果取出的sel为空,说明未缓存当前的sel,跳到__objc_msgSend_uncached
    • 如果取出的sel和当前的sel相等,说明缓存命中,跳到CacheHit
    • 如果sel不为空,且和当前的sel相等不相等,说明发生了哈希碰撞,可能还是有缓存的,继续哈希探测,取出key下标前面的bucket,继续进行判断
  • 如果全部遍历完buckets, 还是没有发生缓存命中,那么遍历一遍buckets进行查找。可能由于多线程加锁的原因,其他线程向缓存中添加了当前线程正在调用的方法。如果这次找到了,跳到CacheHit执行
  • 如果还是没找到,本段代码也执行到最后了,跳到__objc_msgSend_uncached执行。

4.慢速查找流程

4.1 __objc_msgSend_uncached

前面提到当在类的缓存中没有对应的方法的实现时,会去执行__objc_msgSend_uncached。将会开启方法慢速查找流程。

__objc_msgSend_uncached

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	
	/*
 .macro TailCallFunctionPointer
     // $0 = function pointer value
     braaz    $0
 .endmacro
 */
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached

MethodTableLookup

.macro MethodTableLookup
	//保存方法参数到寄存器中
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
    // 位移 为调用_lookUpImpOrForward方法准备参数
    // receiver 和 selector此时已经存储到x0 和 x1 了

    //将class作为第三个参数
	mov	x2, x16
    //第四个参数传递 3
	mov	x3, #3
    
    //bl bl
    // 如果缓存中未找到,则跳转到 _lookUpImpOrForward(c 函数) 去方法列表中去找函数,
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

    // 恢复寄存器并返回
	RESTORE_REGS MSGSEND

.endmacro

__objc_msgSend_uncached就两部分:执行MethodTableLookupTailCallFunctionPointerTailCallFunctionPointer其实是一个跳转指令宏定义,跳转到方法实现的地址去执行,简言之就是器执行方法。所以方法的查找主要看MethodTableLookup就行了。

MethodTableLookup主要执行流程:

  • 保存参数到寄存器中,为_lookUpImpOrForward执行做准备
    • x0 消息接收者(self)
    • x1 selector
    • x2 class
    • x3 3
  • 跳转到lookUpImpOrForward函数执行,去进行慢速查找
  • lookUpImpOrForward方法的返回值,存储在x0, 将x0寄存器的值存储到x17, 后面会跳转到x17地址去执行

4.2 lookUpImpOrForward

lookUpImpOrForward的主要作用就是从cls的方法列表里查找imp,如果没有找到,就去父类中查找(包括父类的缓存和方法列表)。

  • 如果找到了imp,就存储到classcache_t中,以便下次调用时可以快速查找,并将imp返回
  • 如果没有找到imp,就进行行为(behavior)判断:
    • 如果behavior允许进行方法决议,就去执行方法决议resolveMethod_locked),并将决议结果 return
    • 如果behavior不再允许方法决议,继续向下执行
  • done:标签处的代码,会将imp记录,并缓存到cache_t
    • 此时imp可能是真正的方法实现
    • 也可能是由于没有找到真正的方法实现,而被赋值的替代forward_imp
  • 执行结果返回
    • 可能返回真正的方法实现地址
    • 可能返回nil
    • 可能返回forward_imp
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //1. forward_imp  当慢速查找未找到imp时返回
    //实际是 objc_defaultForwardHandler
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    //临时变量
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    //2. 查找方法列表前的判断和准备
    //2.1 判断类是否初始化,如果没有, 就标记behavior 为不缓存imp
    if (slowpath(!cls->isInitialized())) {
        // |= 与运算 并将结果进行赋值
        behavior |= LOOKUP_NOCACHE; 
    }

    //2.2 runtimeLock 在方法查找和缓存期间保持, 以使方法方法添加保持原子性.
    //加锁
    runtimeLock.lock();

    //2.3 检查是否是已知类 不是就报错 中断执行
    checkIsKnownClass(cls);

    //2.4 检查类是否已实现, 没有就实现一下
    //检查类是否已初始化, 没有就进行初始化
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    
    // 确保已经加锁
    runtimeLock.assertLocked();
    
    //为curClass赋初值, 初始值为cls
    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().
    // 唯一没有执行某种缓存查找的代码路径就是class_getInstanceMethod()。
    
    //for 循环负责遍历查找 根据sel找到imp
    //unreasonableClassCount() 获取类的迭代的上限
    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 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)) {
        //LOOKUP_RESOLVER = 2  behavior = 3
        //behavior 异或为1
        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);
    }
 done_unlock:
    //解锁
    runtimeLock.unlock();
    
    //返回imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

forward_imp实际就是objc_defaultForwardHandler,一个c方法。当没有找到真正的imp时,会返回objc_defaultForwardHandler的地址,然后调用。这时候就是我们熟悉的报错了,“xxx unrecognized selector sent to instance xxx”。

__attribute__((noreturn, cold)) 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

遍历查找的过程大致如下:

  1. curClass的方法列表里查找, 列表路径: cls->data()->methods() 1.1 如果找到imp, 就跳到done标记处 继续向下执行 1.2 如果没有找到, curClass指向curClass父类, 准备去父类中查找 1.3 如果curClass父类 为nil, 说明遍历完所有的父类都没有找到imp, 循环结束, break

  2. curClass(父类)cache中查找, 缓存中是否有imp 2.1 如果imp不为空, 但是imp=forward_imp, 说明此前父类在查找sel对应的imp时没有找到,只能将forward_imp放入缓存,这时也不用继续找了,因为找也找不到. 结束循环, break 2.2 如果imp不为空, 并且不等于forward_imp, 说明在父类中找到了对应的imp, 跳到done标记处 继续向下执行 2.3 imp为空, curClass(父类)的缓存中没有没有找到, 继续循环, 去curClass(父类)的方法列表里查找, 此时开始下次一次遍历查找。

  3. 如果第二次遍历还没找到就去curClass(父类)的父类(父类的父类)的缓存中查找, 如此循环往复, 直到结束(找到或未找到)。找到跳转到done标记,没找到会向下进行方法决议判断。

方法决议的判断如下:

if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //LOOKUP_RESOLVER = 2  behavior = 3
        //behavior 异或为1
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

当没有找到方法实现的时候,会尝试一次方法决议。 之所以说是一次, 是因为从MethodTableLookup汇编代码中,调用lookUpImpOrForward方法时,behavior=3。 进到if 里后, 会对behavior执行异或操作,之后behavior=1。 在resolveMethod_locked方法决议时,会再次尝试调用lookUpImpOrForward, 这时behavior=1, 不能再去if里去执行了。 将会向下执行done:

done:标记的执行,也会有两种情况:

  1. 通过上面的for循环,找到了方法的实现,直接跳转到done执行
  2. 通过上面的for循环,没有找到方法的实现,进行方法决议, 在方法决议时会再次执行lookUpImpOrForward方法,仍然没有找到方法实现imp. 前面说过,方法决议只会执行一次, 所以不会再去方法决议了, 而是向下执行done: 执行可能2时, imp=forward_imp, 会把forward_imp存入cls的缓存中。

lookUpImpOrForward方法的慢速查找流程,基本了解了。

5. 总结

方法的执行流程:

  • 先去缓存中查找,找到执行
  • 缓存中找不到,去遍历方法列表查找,找到执行
  • 当前类方法列表中没有找到,父类中找(缓存和方法列表),找到去执行
  • 仍没找到,进行方法决议,进入转发流程,处理消息,执行
  • 最后仍没有处理,崩溃