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

《目录-iOS & OpenGL & OpenGL ES & Metal》缓存

既然知道了方法的本质就是发送消息,那咱们继续研究一下runtime的消息查找markdown

前言

runtime的消息查找分为2步:多线程

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

1、objc_msgSend

objc_msgSend是用汇编写的,那咱们就从汇编开始探索一下objc_msgSend都作了些什么函数

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

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

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

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

个人天啊!这都是些什么鬼?性能

不要紧,我也看不懂,咱们边百度指令,边分析注释,硬读吧~spa

二、GetClassFromIsa_p16

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

2、快速流程

一、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方法

二、CheckMissJumpMiss

cbz比较,若是结果为0就跳转后面
由于咱们是 NORMAL 模式,因此无论进哪一个方法都会来到 __objc_msgSend_uncached方法

三、__objc_msgSend_uncached

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

四、MethodTableLookup

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

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

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

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

3、慢速流程

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

一、lookUpImpOrForward

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

  • 有一步容错,经过cache_getImp方法,若是找到了imp就直接返回

  • 细节点

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

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

  • 若是有父类,直接在父类的缓存中找

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

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

二、方法崩溃的代码

在上面的流程中有一点要提一下,_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);
}
复制代码

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

4、总结

一、对象方法查找流程

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

二、类方法查找流程

  • 类方法 - 本身有

  • 类方法 - 本身没有 - 找父类的

  • 类方法 - 本身没有 - 父类也没有 - 找父类的父类 - NSObject

  • 类方法 - 本身没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 崩溃

  • 类方法 - 本身没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 可是有对象方法

三、消息查找流程

消息查找阶段:

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