一纹因,前言
在iOS開發(fā)過程中,我們都知道不管是什么方法的執(zhí)行
圾结,對(duì)象的創(chuàng)建
鸭轮,以及代理
和Bloc
k的實(shí)現(xiàn)都離不開runtime
,所以runtime
可以說是iOS開發(fā)過程中的生命存在歪沃, 運(yùn)行時(shí) 存在 動(dòng)態(tài)決議
的作用,例如我們?cè)谝粋€(gè)類的聲明中聲明了相關(guān)的方法嫌松,但是并沒有進(jìn)行實(shí)現(xiàn)時(shí)沪曙,進(jìn)行編譯是不會(huì)有任何問題的,但是運(yùn)行時(shí)就會(huì)報(bào)錯(cuò)豆瘫,告知我們沒有實(shí)現(xiàn)該方法。接下來我們就重點(diǎn)研究一下運(yùn)行時(shí)為什么會(huì)只能的告訴我們沒有實(shí)現(xiàn)該方法菊值。
二外驱,環(huán)境配置
首先我們?cè)趍ain.m 中聲明一個(gè)類LGTeacher集成自NSObject,聲明一個(gè)方法sayHello,再次聲明一個(gè)類LGPerson繼承自LGTeacher腻窒,在LGPerson中重寫了父類的sayHello昵宇,以及從新聲明了sayNB,
代碼如下
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
此時(shí)我們?cè)趍ain.m 中進(jìn)行創(chuàng)建相應(yīng)的對(duì)象儿子;并調(diào)用相應(yīng)的方法:
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
[person sayNB];
此時(shí)我們對(duì)該文件進(jìn)行相應(yīng)的clang
命令瓦哎,生成應(yīng)的.cpp文件,對(duì)該文件進(jìn)行查看柔逼,我們能發(fā)現(xiàn)在運(yùn)行時(shí)runtime
對(duì)我們的所有方法進(jìn)行相關(guān)的處理蒋譬;這就是runtime的作用;
以上的代碼在運(yùn)行時(shí)編譯成
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
也就是說我們?cè)趧?chuàng)建對(duì)象的時(shí)候存在兩種對(duì)應(yīng)的關(guān)系愉适;
- alloc 對(duì)應(yīng)的是
sel_registerName("alloc")
- 方法調(diào)用 對(duì)應(yīng)是
sel_registerName("方法名"));
我們?cè)诳刂婆_(tái)打印的結(jié)果是
2020-09-20 23:01:12.225822+0800 001-運(yùn)行時(shí)感受[13893:304936] 666
2020-09-20 23:01:12.226333+0800 001-運(yùn)行時(shí)感受[13893:304936] 666
然后我們?cè)谟胷untime的形式進(jìn)行創(chuàng)建方法犯助,改寫原來的方法調(diào)用過程;
[teacher sayHello];
objc_msgSend(teacher, sel_registerName("sayHello"));
[person sayNB];
objc_msgSend(person, sel_registerName("sayNB"));
我們能看到這兩組打印結(jié)果完全是一樣的
所以我們得出的結(jié)論是
[teacher sayHello]
和 objc_msgSend(teacher, sel_registerName("sayHello"));
完全等價(jià)维咸;那么為什么會(huì)是這樣的剂买,這就是我們接下來重點(diǎn)研究的對(duì)象objc_msgSend
的查找流程
三,查找流程
在objc_msgSend
的方法查找過程中存在兩種查找流程癌蓖,一種是帶緩存的瞬哼,一種是不帶緩存,也即是快速和慢速的兩種情況租副,因?yàn)槲覀兩弦还?jié)學(xué)習(xí)了一個(gè)類的中的相關(guān)的cache_t
的部分坐慰,并做了相關(guān)的詳細(xì)的介紹;所以里邊涉及了很多方法的存儲(chǔ)和對(duì)應(yīng)的imp的存儲(chǔ)過程用僧。那么接下來我們分別對(duì)兩個(gè)查找流程進(jìn)行一個(gè)學(xué)習(xí)和分析讨越。
1、快速查找
我們知道不管我們是何種語言實(shí)現(xiàn)的代碼永毅,在底層都會(huì)編譯為計(jì)算機(jī)能識(shí)別的語言把跨,也就是二進(jìn)制的代碼;那么計(jì)算機(jī)為什么能將我們寫的代碼轉(zhuǎn)換為二進(jìn)制語言了沼死,這就是計(jì)算機(jī)最高效的一種語言匯編
着逐,這就是計(jì)算機(jī)能快速識(shí)別我們代碼的根本原因,那么我們就對(duì)匯編查找流程進(jìn)行一個(gè)分析和學(xué)習(xí)吧;
首先引入一個(gè)相關(guān)匯編指令介紹
我們打開開源代碼0bjc781
,進(jìn)行編譯過后進(jìn)入相關(guān)的匯編代碼耸别;進(jìn)入?yún)R編文件
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
匯編解析:
- 1
ENTRY _objc_msgSend
進(jìn)入?yún)R編進(jìn)行方法查找入口健芭; - 2
NoFrame
進(jìn)入無窗口調(diào)試模式; - 3
cmp p0, #0
進(jìn)行判斷類是否是空秀姐;如果是空這進(jìn)入LReturnZero
,否則進(jìn)入LNilOrTagged
- 4
ldr p13, [x0]
將[x0]
中的信息讀取到p13
即將isa賦值給 p13, p13= 該類的isa - 5 獲取isa 進(jìn)行關(guān)聯(lián)類對(duì)象慈迈,相當(dāng)于
alloc
流程中的initWithIsa
- 6
CacheLookup NORMAL, _objc_msgSend
獲取完isa 后,進(jìn)行正常的消息轉(zhuǎn)發(fā)過程省有;
CacheLookup
的定義是什么痒留? 所以帶著刨根問底的理念全局搜索這個(gè)關(guān)鍵字,
CacheLookup
的查找有三種格式
- 1 NORMAL
- 2 GETIMP
- 3 LOOKUP
接下來我們著重分析一下 NORMAL 其他兩種情況剩余時(shí)間再去學(xué)習(xí)總結(jié)蠢沿,也算是一個(gè)學(xué)習(xí)的過程
我們找到相關(guān)的CacheLookup
定義 伸头;代碼如下
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
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
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)
.endmacro
匯編分析
- 1
ldr p11, [x16, #CACHE]
我們找到#CACHE
的定義是
#define CACHE (2 * __SIZEOF_POINTER__)
而 __SIZEOF_POINTER__
我們都知道是8
,所以 #CACHE
是16;
根據(jù)匯編語句也就是 ldr p11, [x16, 16]
也就是 x16
平移16位也就是順著ISA平移16位到Cache_t的位置舷蟀,也就是正如備注說的 p11 = mask|buckets
也就相當(dāng)于上次文章中介紹的找到相關(guān)的buckets的索引位置恤磷;index
的內(nèi)部結(jié)構(gòu)
- 2
p10, p11, #0x0000ffffffffffff
通過掩碼找出取出緩存中的buckets 也就是p11 = mask|buckets
; - 3
and p12, p1, p11, LSR #48
因?yàn)?p11, LSR #48
就相當(dāng)于
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) -
取出mask 在和p12
進(jìn)行按位與操作
也就是x12 = _cmd & mask
- 4
add p12, p10, p12, LSL #(1+PTRSHIFT)
因?yàn)?p12, LSL #(1+PTRSHIFT)
我們找到PTRSHIFT
的定義如下
#define PTRSHIFT 3
也就是p12, LSL #4
也就是 p12
左移4位也就是 左移16; p12 = *(buckets + index) *16 也就是找到相應(yīng)位置的bucket;
5
ldp p17, p9, [x12] // {imp, sel} = *bucket
取出第四步驟中取出的buckect中的sel
和imp
6 拿到相應(yīng)的Sel 和IMP 和我們調(diào)用的方法進(jìn)行對(duì)比野宜,如果沒有找到則跳轉(zhuǎn)到
2
找到那么就進(jìn)入CacheHit
定義
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == 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.
AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
- 7 再次沒查找到的時(shí)候向前查找所有的buckets扫步,如果都沒找到,則進(jìn)入
CheckMiss
:定義
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
這就是整個(gè)快速查找過程匈子,
2锌妻、慢速查找
在此過程中我們知道調(diào)用方法就是獲取某個(gè)方法的IMP
.所以我們找到項(xiàng)目的 class_getMethodImplementation
方法,在詳細(xì)研究相關(guān)的流程情況旬牲,代碼定義如下
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
再次尋根究底的探索相關(guān)的獲取imp
的重點(diǎn)方法lookUpImpOrNil
;再次進(jìn)入
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}
再次順騰摸瓜進(jìn)入 lookUpImpOrForward
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
if (slowpath((curClass = curClass->superclass) == 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)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
代碼解析過程仿粹;
- 1 首先從緩存中取出相應(yīng)的
imp
,我們知道快速查找流程就是沒找到相應(yīng)的緩存;所以此處不可能存在imp
.所以直接將IMP設(shè)置為nil
; - 2 再次檢查是否是我們當(dāng)前的類對(duì)象原茅;
- 3 如果當(dāng)前類是否是運(yùn)行相應(yīng)的查找行為權(quán)限吭历,如果有就繼續(xù)查找;
- 4 如果當(dāng)前類中沒找到相應(yīng)的方法擂橘,繼續(xù)從父類方法列表中去查找晌区,直到找到NSObject 為止,
- 5 如果找到通贞;那么就將該方法列表緩存起來朗若,為了下次能快速的查找,
- 6 如果慢速都沒找到昌罩,直到返回nil的時(shí)候哭懈,那么就要進(jìn)行動(dòng)態(tài)方法解析。
這就是所有慢速查找流程的核心茎用,通過上述流程就能體現(xiàn)objec_msgSend的流程執(zhí)行遣总。
四睬罗,總結(jié);
慢速查找的原理就是在c/c++層面去進(jìn)行旭斥,只是做的事情是找到就進(jìn)行緩存操作容达,也是反復(fù)的遞歸找出我們所需的方法實(shí)現(xiàn)imp ,如果沒找到包括快速和慢速都沒有,那么后期將會(huì)繼續(xù)判斷是否允許動(dòng)態(tài)解析垂券,方法決議等花盐,如果不允許,則程序就會(huì)報(bào)錯(cuò)菇爪,告知我們這個(gè)方法沒有實(shí)現(xiàn)算芯,這就是這個(gè)objc_msgSend的流程,