在OC端,所有的方法调用,在编译的时候都转成了消息的转发objc_msgSend
或者objc_msgSendSuper
。那么问题来了,objc_msgSend
是怎么找到方法并调用的能??
带着这个问题,我们找到objc-818.2
的源码,全局搜索一下,经过一番查找发现它的实现是在objc-msg-arm64.s
汇编文件中,从ENTRY
位置入手。
一、方法快速查找流程
汇编imp快速查找流程:
7.1可看
cmp p0, #0
// 首先是查看消息 receiver 接收者是否存在,如果不存在,再判断是否支持SUPPORT_TAGGED_POINTERS
,支持的话就跳到LNilOrTagged
执行(),不支持就跳到LReturnZero
执行
ldr p13, [x0]
// p13拿到isa
GetClassFromIsa_p16 p13, 1, x0
// 通过isa拿到class放入p16寄存器
GetClassFromIsa_p16 里面的ExtractISA
拿到class
.macro ExtractISA
and 1, #ISA_MASK // $0 = isa & ISA_MASK = class
.endmacro
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
LGetIsaDone
开始calls IMP或者调用objc_msgSend_uncached
mov x15 , x16
// 保存class
- 调用函数LLookupStart\Function,根据架构选型,arm64的真机是走
CACHE_MASK_STORAGE = CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE]
// 获取到cache_t存在p11寄存器
- CACHE 是在类结构体中的偏移 是 (2 * SIZEOF_POINTER) = 16字节,p11 = x16 + 0x10 得到 => cache_t
- CONFIG_USE_PREOPT_CACHES = 1
- __has_feature(ptrauth_calls) A12之后,也就是iPhone X之后
tbnz p11, #0, LLookupPreopt\Function
// 判断cache_t是否存在
and p10, p11, #0x0000ffffffffffff
// 因为arm64真机的buckets是存在低48位,所以p11 (也就是cache_t) & 掩码 (#0x0000ffffffffffff) = buckets- buckets是存bucket_t结构体的一段连续的内存空间,但是不是数组
eor p12, p1, p1, LSR #7
- p1存的是_cmd,LSR表示:右移7位,eor表示:异或
代码意思就是: p12 = p1 ^ (p1 >> 7) ==> p12 = _cmd ^ (_cmd >> 7),意思就是对sel进行hash,将哈希之后的值存在p12寄存器中
and p12, p12, p11, LSR #48
- p11存cache_t,p12存sel的哈希值,cache_t在arm64中低48位是buckets,高16位是mask
- 代码的意思是: p12 & (p11 >> 48),p11 >> 48得到高16位的mask,转化一下就是
sel的hash值 & mask
==(_cmd ^ (_cmd >> 7)) & mask
存在p12寄存器中,其实最终就会得到一个index下标在p12中
add p13, p10, p12, LSL #(1+PTRSHIFT)
- 此时p10存 buckets(这个其实是那个连续内存的首地址,但是不是数组),p12是index下标,在
__LP64__
和arm64中PTRSHIFT = 3。- 代码的意思是:p13 = buckets + (index << (1+PTRSHIFT)) ==> buckets + (index << 4) 相当于首地址加上一个下标n(可以用数组的首地址加一个下标n来理解),就是指向buckets容器中的一个内存块。
进入遍历do.....while循环
7.11: ldp p17, p9, [x13], #-BUCKET_SIZE
- BUCKET_SIZE是一个bucket的大小,x13指向了中间的某一个bucket,-BUCKET_SIZE,就是向前挪了一个位置,{imp, sel} = *bucket--,读取imp到p17和sel到p9寄存器中
7.2
cmp p9, p1
b.ne 3f
- 如果p9(也就是sel)不等于p1(也就是_cmd),也就是没有找到imp,则跳到处执行
7.3
2: CacheHit \Mode
- 如果p9的sel与 p1的_cmd相等,也就是找到imp,那就调取CacheHit函数
7.4
3: cbz p9, \MissLabelDynamic
cmp p13, p10
b.hs 1b
- 如果p9 == 0,也就是sel == 0,则跳转到 MissLabelDynamic函数,这个形参函数传进来的值是__objc_msgSend_unCache。否则如果p13 > p10,也就是往前遍历,如果还大于首地址,则继续跳到 ,继续bucket--,继续找sel和_cmd比较。
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
- 如果上面的循环没有找到,也就是前一半遍历没有找到,则定位到后一半继续遍历。p10存 buckets,即首地址,p13 = buckets + (mask << 1+PTRSHIFT),p13定位到最后一个bucket_t
add p12, p10, p12, LSL #(1+PTRSHIFT)
- 在前一半遍历的时候,p12存的一个index位置的bucket_t,所以后一半遍历的时候,以p12往后一个作为起始点,以first_probed表示,防止重复遍历前面的部分。
4: ldp p17, p9, [x13], #-BUCKET_SIZE
- p13已经定位到最后一个,然后循环取出bucket_t {imp, sel}存在p17(imp),和p9(sel)中。
cmp p9, p1
// if (sel == _cmd)
b.eq 2b
// goto cacheHit
cmp p9, #0
// } while (sel != 0 &&
ccmp p13, p12, #0, ne
// bucket > first_probed)
b.hi 4b
- 然后依次遍历对比,如果sel == _cmd ,则跳到cacheHit执行,否则只要sel != 0 且p13与起始位置没有碰面(bucket > first_probed)继续循环遍历。
image.png image.png
小结:
简单总结一下,方法快速查找流程是用汇编实现的,其原因个人认为:
其一
、汇编实现能够直接操作寄存器快速,而这部分是调用最多的更能提高速度。
其二
、是为了应对不同的调用转换,因为所有的方法最后都转化成objc_msgSend消息,其参数数量,参数类型,返回类型都不一样,使用C、C++、Objective-C是不能做到,就算能做那也要写庞大的代码量才能实现,就谈不上快速查找了,而使用汇编加上类型转换,就可以实现了一套简单的代码完成所有的调用类型。
获取到class和isa --->
通过偏移拿到cache_t--->
根据架构与上掩码得到buckets和mask--->
定位index使用do...while遍历前一半和后一半找sel与_cmd对比来查找imp--->
找到则CacheHit--->
找不到MissLabelDynamic,也就是__objc_msgSend_uncached(漫长的C/C++方法查找)
二、方法慢速查找初探
我们注意到前面第7.4
步,如果找遍了所有的buckets都没有找到imp,会调用MissLabelDynamic,在第4
步调起CacheLookup NORMAL, _objc_msgSend, __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
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
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
然后bl _lookUpImpOrForward,这个就是方法的慢速查找流程,进入C/C++的方法查找流程和转发。
小结:
方法的慢速查找在另一个篇章iOS底层探索--方法慢速查找进行详细讲解,这里是为了能了解到底层是如何从汇编的快速查找无缝衔接到漫长的C/C++查找的。
汇编的代码分析是一个枯燥的过程,需要耐心,耐心,耐心,结合源码的底层数据结构才能更好的理解,读者可结合思路,自己自己的推敲一遍,你会有一种神清气爽的感觉。
微风拂面,都仿佛对面走来的女孩在向你示爱!!!
网友评论