在快速查找流程中征懈,如果緩存命中了還好說(shuō),那么如果命不中呢,就會(huì)到我們的objc_msgSend
慢速查找流程烘苹,這篇文章就好好來(lái)分析是怎么進(jìn)行慢速查找的
快速查找中如果沒(méi)有找到方法實(shí)現(xiàn)嘶是,發(fā)現(xiàn)無(wú)論是CheckMiss
還是JumpMiss
闻丑,最終都會(huì)走到__objc_msgSend_uncached
匯編函數(shù)关顷,如圖
然后我們command+F 搜索一下__objc_msgSend_uncached
這玩意干了啥
找到之后邢享,我們發(fā)現(xiàn)它做了一件事件洽腺,就是進(jìn)行了方法列表的查找脚粟。然后我們接著搜索MethodTableLookup
,找到其源碼實(shí)現(xiàn)
前面的準(zhǔn)備工作匯編我們不需要管蘸朋,關(guān)鍵是bl _lookUpImpOrForward
這里跳轉(zhuǎn)到_lookUpImpOrForward
核无,然后搜索_lookUpImpOrForward
發(fā)現(xiàn)找不到了,那么去哪里了呢藕坯?
接下來(lái)我把項(xiàng)目跑起來(lái)团南,通過(guò)斷點(diǎn)來(lái)找到它
首先,在main函數(shù)中[person sayHello]處打一個(gè)斷點(diǎn)炼彪,然后我們打開(kāi)匯編調(diào)試吐根,在頂部狀態(tài)欄選擇Debug -- Debug worlflow -- 勾選Always show Disassembly,
運(yùn)行程序
運(yùn)行完了之后辐马,我們進(jìn)到objc_msgSend
里面去拷橘,按住control
,點(diǎn)中間調(diào)試按鈕(stepinto)就進(jìn)去了
來(lái)到objc_msgSend里面后我們發(fā)現(xiàn)下面有一個(gè)_objc_msgSend_uncached
,看到這里喜爷,果斷打個(gè)斷點(diǎn)膜楷,然后接著往里面進(jìn)
走進(jìn)來(lái)終于找到了我們的lookUpImpOrForward
在objc-runtime-new.mm
里面第6099行
,這就很爽了贞奋,我們直接找過(guò)去就行了。這里補(bǔ)充一點(diǎn)穷绵,細(xì)心的朋友可能會(huì)發(fā)現(xiàn)下劃線去哪里了轿塔, 這是因?yàn)閺奈覀兊?code>匯編到C++會(huì)少一個(gè)下劃線,從C++到C會(huì)再少一個(gè)下劃線
,所以我們?cè)趨R編中去查找C/C++方法時(shí)要把下劃線給去掉
明白了這一點(diǎn)之后我們接下來(lái)就全局搜索lookUpImpOrForward
勾缭,就來(lái)到了objc-runtime-new.mm
里面的lookUpImpOrForward
揍障, 這里面是通過(guò)C/C++
寫(xiě)的,看起來(lái)還是要比匯編舒服一點(diǎn)
還是老規(guī)矩俩由,我把這里面的代碼大致翻譯一下毒嫡,看我分析之前,大家也可以邊打斷點(diǎn)邊看我注釋?zhuān)@樣流程更加的清晰
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// 定義的消息轉(zhuǎn)發(fā)
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// 快速查找幻梯,如果找到則直接返回imp
// 這個(gè)地方為什么又進(jìn)行了快速查找兜畸? 其目的是為了防止多線程操作時(shí),剛好調(diào)用函數(shù)碘梢,就可以有緩存了
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
// 快速查找在緩存里面找到了imp咬摇,直接返回imp
if (imp) goto done_nolock;
}
// 加鎖,目的是保證讀取的線程安全
runtimeLock.lock();
// TODO: this check is quite costly during process startup.
// 判斷是否是一個(gè)已知的類(lèi):判斷當(dāng)前類(lèi)是否是已經(jīng)被認(rèn)可的類(lèi)煞躬,即已經(jīng)加載的類(lèi)
checkIsKnownClass(cls);
// 判斷類(lèi)是否實(shí)現(xiàn)肛鹏,如果沒(méi)有,需要先實(shí)現(xiàn)恩沛,此時(shí)的目的是為了確定父類(lèi)鏈在扰,方法后續(xù)的循環(huán)
// 這個(gè)地方?jīng)]有imp,不是重點(diǎn)
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 判斷類(lèi)是否初始化雷客,如果沒(méi)有芒珠,需要先初始化
// 這個(gè)地方?jīng)]有imp,不是重點(diǎn)
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
// ------------重點(diǎn)在for循環(huán)里面----------------
// unreasonableClassCount -- 表示類(lèi)的迭代的上限
//(猜測(cè)這里遞歸的原因是attempts在第一次循環(huán)時(shí)作了減一操作佛纫,然后再次循環(huán)時(shí),仍在上限的范圍內(nèi)妓局,所以可以繼續(xù)遞歸)
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
// 去當(dāng)前類(lèi)的方法列表查找(采用二分查找算法),如果找到呈宇,則goto done好爬,將方法緩存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
// -------- 如果二分查找找自己沒(méi)有找到,那么就開(kāi)始找父類(lèi)的緩存了 ----------
// 注意甥啄, 這里將curClass = superclass 把父類(lèi)賦值給了當(dāng)前類(lèi)了存炮,并判斷父類(lèi)是否為nil
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 父類(lèi)全部找完了之后,父類(lèi)為nil了蜈漓, 把imp賦值為forward_imp
imp = forward_imp;
// 賦值完了之后會(huì)退出本次循環(huán)穆桂,說(shuō)明父類(lèi)也沒(méi)有這個(gè)方法
break;
}
// Halt if there is a cycle in the superclass chain.
// 如果父類(lèi)鏈中存在循環(huán),則停止
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 找父類(lèi)的緩存融虽。
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// 如果在父類(lèi)中找到了forward享完,則退出循環(huán),調(diào)用此類(lèi)的方法解析器
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
// 如果在父類(lèi)中有额,找到了此方法般又,將其存儲(chǔ)到cache中
goto done;
}
}
// No implementation found. Try method resolver once.
// 在自己和父類(lèi)都沒(méi)有找到方法實(shí)現(xiàn)彼绷,則會(huì)來(lái)到動(dòng)態(tài)方法決議
if (slowpath(behavior & LOOKUP_RESOLVER)) {
// 這里是此次動(dòng)態(tài)方法決議的控制條件
behavior ^= LOOKUP_RESOLVER;
// 動(dòng)態(tài)方法決議,給一次處理的機(jī)會(huì)
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
// 存儲(chǔ)到緩存
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;
}
這個(gè)代碼流程分析的話我覺(jué)得要比上一篇文章的匯編要容易看懂一些茴迁。不過(guò)這里還是把整個(gè)慢速查找的流程總結(jié)一下
總結(jié):
1寄悯、 首先cache
緩存中快速查找沒(méi)有找到,則進(jìn)入慢速查找lookUpImpOrForward
里面堕义。來(lái)到這里之后還會(huì)進(jìn)行快速查找一次猜旬,防止多線程操作時(shí),剛好調(diào)用了此函數(shù)倦卖,這樣就可以直接從緩存中快速查找洒擦, 不用再進(jìn)行慢速查找了
2、判斷cls
糖耸, 是否是已知類(lèi)
秘遏。 然后該類(lèi)是否實(shí)現(xiàn)
,如果沒(méi)有嘉竟,則需要先實(shí)現(xiàn)
邦危,此時(shí)實(shí)例化的目的是為了確定父類(lèi)鏈、ro舍扰、以及rw等
倦蚪,為后面的數(shù)據(jù)讀取查找做好準(zhǔn)備。
3边苹、 來(lái)到for循環(huán)陵且,慢速查找的重點(diǎn)流程 :
3.1: 去當(dāng)前類(lèi)的方法列表查找
(采用二分查找算法),如果找到个束,則goto done慕购,將方法緩存到cache中,如果沒(méi)找到則開(kāi)始找父類(lèi)
3.2: 如果父類(lèi)找到了imp
茬底,則直接返回imp
沪悲,執(zhí)行cache寫(xiě)入流程
,如果循環(huán)完所有的父類(lèi)還沒(méi)找到阱表,最終會(huì)找到nil殿如,父類(lèi)為nil的話就會(huì)走到imp = forward_imp
,跳出當(dāng)前循環(huán)
4最爬、 當(dāng)前類(lèi)和父類(lèi)都沒(méi)找到imp
涉馁, 就會(huì)來(lái)到動(dòng)態(tài)方法決議,給一次機(jī)會(huì)重新進(jìn)行查詢爱致,如果進(jìn)行處理了烤送,能拿到imp
,就不會(huì)崩潰糠悯。
看完我這個(gè)總結(jié)帮坚,再去看我源碼里的注釋?zhuān)矣X(jué)得應(yīng)該非常清晰了牢裳,這就是整個(gè)方法的慢速查找流程。
在分析完objc_msgSend慢速查找之后叶沛,我再補(bǔ)充兩個(gè)知識(shí)點(diǎn)
二分法算法
首先第一個(gè)知識(shí)點(diǎn)是查找方法列表的時(shí)候用的二分法查找,我們先來(lái)找到二分法算法的源碼
這個(gè)代碼我覺(jué)得非常的簡(jiǎn)單忘朝, 沒(méi)有必要再一個(gè)個(gè)注釋了灰署,如果有看不懂的可以斷點(diǎn)一試就明白了,也可以給我留言局嘁,我收到后會(huì)及時(shí)回復(fù)大家溉箕,我說(shuō)一下大致的算法流程:
1、 拿到方法列表的第一個(gè)first和總數(shù)悦昵,然后開(kāi)始遍歷循環(huán)肴茄。注意,方法列表里面的數(shù)值是遞增的但指,有序的
2寡痰、進(jìn)到循環(huán)之后,probe = base + (count >> 1);
這句代碼意思是首地址 位移 (count >> 1)
相當(dāng)于從第一個(gè)元素移動(dòng)到了中間
3棋凳、 移動(dòng)到了中間之后拦坠,拿keyValue == probeValue
進(jìn)行對(duì)比,如果等于剩岳,則返回method_t
贞滨。注意,這里有一個(gè) probe--
拍棕,意思是排除分類(lèi)里面名字一樣的方法晓铆。
4、如果keyValue
大于 probeValue
绰播,即在(probe +1) 到 (count--)
之間查找骄噪,同時(shí)count
每循環(huán)一次,右移一位幅垮,雙數(shù)減半腰池,單數(shù)減半再減一,比如8右移一位是 8/2 = 4忙芒,7右移一位是 7/2 - 1 = 3示弓。然后再回到第二個(gè)步驟進(jìn)行查找
5、如果keyValue
小于 probeValue
呵萨,即在1 - probe
之間繼續(xù)取中間位置進(jìn)行查找奏属,同時(shí)count
每循環(huán)一次,右移一位潮峦。然后再回到第二個(gè)步驟進(jìn)行查找
6囱皿、 一直循環(huán)到count
為0
勇婴,probeValue
為1
還沒(méi)找到,就返回nil
了
消息轉(zhuǎn)發(fā)
明白了二分法算法之后嘱腥,我們最后再補(bǔ)充一個(gè)知識(shí)點(diǎn)耕渴,就是動(dòng)態(tài)方法決議之后,會(huì)來(lái)到消息轉(zhuǎn)發(fā)_objc_msgForward_impcache
這里齿兔,這個(gè)是匯編實(shí)現(xiàn)的橱脸。
然后會(huì)走到__objc_msgForward
,走到這里之后往下走分苇,會(huì)來(lái)到__objc_forward_handler
添诉,我們搜索__objc_forward_handler
找不到,因?yàn)閰R編加了下劃線医寿,我們?nèi)サ粢粋€(gè)下劃線試試栏赴,搜索_objc_forward_handler
,就找到了C++
文件里面靖秩,然后最終走到了objc_defaultForwardHandler
里面须眷。這就是一直都沒(méi)有找到實(shí)現(xiàn)的方法,崩潰時(shí)報(bào)的錯(cuò)誤提示盆偿。
最后柒爸,來(lái)一張消息轉(zhuǎn)發(fā)機(jī)制流程圖,為下一篇文章做準(zhǔn)備