iOS底层原理探索-07- Runtime之消息查找

740 阅读6分钟

《目录-iOS & OpenGL & OpenGL ES & Metal》
既然知道了方法的本质就是发送消息,那我们继续研究一下runtime的消息查找

前言

runtime的消息查找分为2步:

  • 快速查找流程
  • 慢速查找流程

一、objc_msgSend

objc_msgSend是用汇编写的,那我们就从汇编开始探索一下objc_msgSend都做了些什么

延伸:为什么objc_msgSend是用汇编而不是用C编写的呢?

  • 是因为:
    • c语言不能通过 只写一个函数,然后保留未知参数,就跳转到任意的指针。
    • 汇编有寄存器(arm64下有31个寄存器,每一个代表64位)
    • 汇编更容易能被机器识别,对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能

1、开始探索objc-msg-arm64.s

来到源码中,找到objc-msg-arm64.s,再找到ENTRY _objc_msgSend

我的天啊!这都是些什么鬼?

没关系,我也看不懂,我们边百度指令,边分析注释,硬读吧~

2、GetClassFromIsa_p16

搜索看一下这个方法,是怎么通过isa拿到class的: 的确看到了熟悉的代码ISA_MASK,就是通过isa & ISA_MASK运算,拿到class,然后走到 CacheLookup方法

二、快速流程

1、CacheLookup

先看一下注释内存,我们一般用到都是NORMAL 模式。这时,我们已经拿到了selclass

(吐槽一下,这段代码太长了,截个图费劲巴拉的) 我们上一段代码分析一段:

	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

x16将类对象内存地址平移16位赋值给p11。我们之前研究过,平移16位刚好就是缓存cache。其实后面注释就有了解释

	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask

p11通过and = &与运算,拿到缓存中的buckets赋值给p10
p11通过LSR右移48位,得到mask,和sel进行与运算,赋值给p12

  • _cmd & maskcache_t中的cache_hash方法一样,经过哈希运算之后,得到了 bucket 结构体指针
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket

p12通过LSL左移(1+PTRSHIFT),然后和p10进行与运算,赋值给p12
p12imp赋值给p17sel赋值给p9

1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp

比较p9p1,就是对比我们传进来的方法编号,是否和缓存中找到的匹配,匹配就是缓存命中CacheHit返回imp,不匹配进入第二步

2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop
3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

如果bucket->sel == 0,就跳到CheckMiss方法
p10p12进行比较,如果eq 相等,就跳到第三步
如果不相等,p12的指针进行--操作,拿到新的selimp
再跳到1第一步,进入循环,重新执行一遍

  • 这里是为了防止多线程操作,刚好缓存进来了

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

这里再执行一遍1~3的流程,相当于给了一个容错的机会,如果第二次还是找不到我们需要的sel对应的imp,就跳到JumpMiss方法,开始进入慢速流程。

我们再看一下流程中间遇见CheckMiss方法、JumpMiss方法

2、CheckMissJumpMiss

cbz比较,如果结果为0就跳转后面
因为我们是 NORMAL 模式,所以不管进哪个方法都会来到 __objc_msgSend_uncached方法

3、__objc_msgSend_uncached

__objc_msgSend_uncached方法中最核心的逻辑就是 MethodTableLookup方法,意为查找方法列表。

4、MethodTableLookup

大致一看,又要计算?我们直接抓住核心的点:bl _lookUpImpOrForward,跳转了这个方法,全局搜索一下_lookUpImpOrForward发现并没有。那搜索一下lookUpImpOrForward,有这个方法!

其实,我们这里去掉下划线找方法,属于开启了上帝视角。如果按正常流程,我们应该打开汇编,断点方法,看汇编里面的jumpcallq命令都走了哪些方法

因为lookUpImpOrForward这个方法是一个C/C++方法,它的参数必须是确定的,这样就可以解释通bl _lookUpImpOrForward这行代码前面的操作了,就是为了传入确定的参数做准备。

快速流程就到这里,就是在缓存中通过sel找imp,找不到就进入慢速流程。

三、慢速流程

我们在上一篇章中验证过,方法存储在 类 -> bit -> rw -> ro -> methodList里面,带着这个思路,看一看runtime在源码中查找消息的流程是不是一模一样的?

1、lookUpImpOrForward

哇,这里面又臭又长,我们就简述一下,讲一下几个需要注意的点:

  • 有一步容错,通过cache_getImp方法,如果找到了imp就直接返回

  • 细节点

    • runtimeLock.lock();在这里加锁,防止同时访问2个方法,出现imp返回错误
    • checkIsKnownClass(cls);判断这个类是否是被编译过的,如果不是就输出错误信息
    • 这里还有一些准备工作,拿到对应的类和元类的信息
  • 如果是对象方法,就在当前类的方法列表中使用二分查找法寻找imp,找到就进行缓存,然后返回imp

    • 如果是类方法,就在当前的元类中找
  • 判断,如果当前类,没有父类,直接崩溃并打印错误信息

  • 如果有父类,直接在父类的缓存中找

    • 找到就缓存并返回imp
    • 找不到就在父类的方法列表中使用二分查找法寻找imp,找到就进行缓存,然后返回imp
  • 如果父类中没有,就在元类(父类的父类)中找,继续上一步的循环

    • 当前类 -> 父类 -> 元类 -> 根元类(NSObject)
  • 最后都没找到,就进行动态方法解析resolveMethod_locked(inst, sel, cls, behavior);

2、方法崩溃的代码

在上面的流程中有一点要提一下,_objc_msgForward_impcache这个方法,我们去看一下~

c函数没找到这个方法,汇编中找到了。

	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

继续找__objc_msgForward

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
	
    

c函数__objc_forward_handler

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
// Default forward handler halts the process.
__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);
}

这个找到最后,好熟悉!这不就是找不到实现方法崩溃,在控制台打印的信息吗!!!

四、总结

1、对象方法查找流程

  • 对象的实例方法 - 自己有
  • 对象的实例方法 - 自己没有 - 找父类的
  • 对象的实例方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject
  • 对象的实例方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 崩溃

2、类方法查找流程

  • 类方法 - 自己有

  • 类方法 - 自己没有 - 找父类的

  • 类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject

  • 类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 崩溃

  • 类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 但是有对象方法

3、消息查找流程

消息查找阶段:

  1. 首先进入快速流程,拿到isa,通过汇编的手段在缓存中找,找到就返回
  2. 然后进入慢速流程,通过:当前类.方法列表 -> 父类.缓存 -> 父类.方法列表 -> 元类.缓存 -> 元类.方法列表 这个流程,哪一步找到就返回
  3. 最后都没找到,进入``消息转发阶段`