iOS 底层探索篇 ——Runtime-objc_msgSend流程分析 - 慢速查找流程 (上)

321 阅读6分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

接着上篇的代码分析

//这里的p12 是第一次要找的index((_cmd ^ (_cmd >> 7)) & mask)
//左移4位相当于*16,p10是buckets的首地址,也就是将p10平移到第一次要查找的地方
//然后储存到p12里面。
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
//相当于之前的bucket--循环当前查找
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
// 找到就去 前面的cachehit
	b.eq	2b				//         goto hit
// p9 不等于0
	cmp	p9, #0				// } while (sel != 0 &&
/// p13 大于 p12,确保不查找之前查过的bucket
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

真机中探寻objc_msgSend

重新创建一个app工程,在真机上跑代码来探索objc_msgSend

在这里插入图片描述

打开always show disassembly后运行。

在这里插入图片描述

按住control然后点击step into,进入到objc_msgSend

在这里插入图片描述

接下来读取寄存器x0,x1。

在这里插入图片描述

这里可以看到x1SELx0就是receiver,再按住control然后点击step into,进入到libobjc.A.dylib`objc_msgSend。看到第2行就是拿x0和0x0对比来判断x0是否存在,而第5行则是通过将receiver的地址于上掩码得到isa。

在这里插入图片描述

输出证明一下确实得到isa。

在这里插入图片描述

_objc_msgSend_uncached

之前说到,如果找到了方法就会cacheHit,没有找到就会进入_objc_msgSend_uncached,那么在源码中搜索_objc_msgSend_uncached,找到entry

在这里插入图片描述

这里看到会进入MethodTableLookup,来搜索一下MethodTableLookup。

在这里插入图片描述

再来看一下TailCallFunctionPointer,好像只是返回。说明了重要信息在MethodTableLookup里面。

接着看MethodTableLookup,看到下面有x0,是寄存器的第一个地址,存放返回值的地方,所以有x0就代表有返回值imp,我们目标需要找imp,所以需要去看_lookUpImpOrForward

在这里插入图片描述

搜索一下_lookUpImpOrForward,发现在objc-msg-arm64.s只有刚才那里有。

在这里插入图片描述

接下来去看c++代码,搜索lookUpImpOrForward,回到objc-runtime-new里面,找到 lookUpImpOrForward的实现。

不是全部代码。

为什么缓存要特意用汇编写而不是c++写呢

  1. 汇编流程更加接近机器语言,执行快,优化查找时间
  2. 安全
  3. 参数未知,c语言无法满足

如果在汇编找不到方法,就会进入到慢速查找流程,就是遍历methodList的过程。

lookUpImpOrForward中,我们的目标是imp,我们需要关注的是返回值imp

在这里插入图片描述

现在开始探寻这个方法。

看到这里有个checkIsKnownClass,故名思义就是检查是不是已知的类

在这里插入图片描述

点进去看看方法实现。

在这里插入图片描述

在看看isKnownClass的实现,大概看出来allocatedClasses是个集合,用来存放所有已经加载的类。

在这里插入图片描述

回到lookUpImpOrForward往下走来到这里,这里主要是进行类的实现。

在这里插入图片描述

在看这里

在这里插入图片描述

继续点进去

在这里插入图片描述

在点这里

在这里插入图片描述

看到了熟悉的class_rw_t,supercls,metaclas,这个方法对ro,rw进行了一些处理

在这里插入图片描述

接下来看到这里,发现对父类以及元类也进行了实现

在这里插入图片描述

往下看,看到了这里对cls的父类以及元类进行了赋值。到这里可以看到,这里对类以及他的父类以及元类 -> 根类根元类进行了初始化。这是为了如果在当前类中找不到方法,可以在父类或者元类中进行查找。

在这里插入图片描述

回到lookUpImpOrForward方法继续往下走

在这里插入图片描述

来到一个死循环里面,会先到共享缓存找一遍,以免到这里的时候,刚好之前方法缓存进去了

在这里插入图片描述

如果没有缓存走到getMethodNoSuper_nolock

在这里插入图片描述

点进去看看getMethodNoSuper_nolock的实现。这里的cls->data()->methods()就比较熟悉了,是获取方法列表。然后获取方法列表中的第一个和最后一个方法,遍历查找。

在这里插入图片描述

接下来就会进入search_method_list_inline,点进去看一下。

在这里插入图片描述

再来看findMethodInSortedMethodList。这里一般都是走else,其他如m1会走isSmallList

在这里插入图片描述

点开findMethodInSortedMethodList查看。这里运用了二分查找法。假设count是8也就是二进制的1000,要查找的为7,1000 >> 1 = 0100 = 4,那么第一次进循环时,probe就是4,而probeValue就是通过getName方法得到的SEL,如果和要查找的SEL匹配就返回probeprobe--是在返回是有分类的情况,先调用分类的方法,这里不展开。如果不等于,则继续往下走,如果7大于4,那么base = probe + 1 = 5,count -- 就为7. 再进一次循环,count >>= 1, 7 >>= 1,那么就是0011等于3。probe = base + (count >> 1) = 5+1 = 6,继续查找,没有找到,那么就是base = probe + 1 = 7,count -- 等于 2. 在进一次循环。count >>=1 也就是0010 >>= 1等于1,probe = base + (count >>1 ) = 7 + 0 = 7,这次keyValue 就等于probeValue,找到了我们要找的probe。

在这里插入图片描述

在回到lookUpImpOrForward往下走,看到这里如果找到了imp,那么就会goto done

在这里插入图片描述

看一下done是什么,发现这里有log_and_fill_cache,进行缓存填充了。

在这里插入图片描述

log_and_fill_cache点进去看,发现这里插入缓存cache.insert.

在这里插入图片描述

到这里,msgSend已经形成了一个闭环。 第一次loopUpCache寻找如果没有找到的话,会进入慢速查找,如果rw,ro里面没有,就会报错。如果有,就会insert,下一次进来的时候就进行快速查找loopUpCache。

在回到lookUpImpOrForward往下走。这里把curClass换成了父类。如果所有的父类都找完了,那么imp就会设为forward_imp,退出循环,具体流程下回分析。

在这里插入图片描述

继续往下走发现如果找不到的话就会进入cache_getImp。

在这里插入图片描述

点进去cache_getImp发现找不到实现。

在这里插入图片描述

就去汇编中寻找,发现实现,又是熟悉的GetClassFromIsa_p16 以及CacheLookup,说明这里是快查找流程。这里找不到的话,就会返回一个空。

在这里插入图片描述

所以上面的 imp = cache_getImp(curClass, sel); imp就会为空。然后在回到lookUpImpOrForward往下走 就会开始重新循环,而这时候的curClass 就是父类。这个循环会一直下去,直到找到方法,或者父类为空也没有找到。

lookUpImpOrForward流程总结:

  1. 检测类是否注册过, 如果没有注册过就报错
  2. 检测是否实现/初始化superclass链上的类和元类, 没有的话就去实现,这样的话在本类没有找到就会去父类中寻找。
  3. 进入for循环查找
  • 判断是否有共享缓存,以免到这里的时候,刚好之前方法缓存进去了
  • findMethodInSortedMethodList中二分查找当前类的sel对应的Method, 找到就退出循环,插入缓存。
  • 如果没有找到就检测父类, 父类如果为nil, 则退出循环, imp = forward_imp
  • 如果没有找到且父类不为nil的话就进行父类的慢速查找流程。
  • 父类中找到imp, 则退出循环, 跳转到goto done