iOS底層-objc_msgSend 慢速查找流程分析

慢速查找-匯編部分

在objc-msg-arm64.s文件中查找__objc_msgSend_uncached的匯編實(shí)現(xiàn)盆顾,其中的核心是MethodTableLookup(即查詢方法列表)佑刷,其源碼如下

.macro MethodTableLookup

// push frame

SignLR

stp fp, lr, [sp,#-16]!

mov fp, sp

// save parameter registers: x0..x8, q0..q7

sub sp, sp,#(10*8 + 8*16)

stp q0, q1, [sp,#(0*16)]

stp q2, q3, [sp,#(2*16)]

stp q4, q5, [sp,#(4*16)]

stp q6, q7, [sp,#(6*16)]

stp x0, x1, [sp,#(8*16+0*8)]

stp x2, x3, [sp,#(8*16+2*8)]

stp x4, x5, [sp,#(8*16+4*8)]

stp x6, x7, [sp,#(8*16+6*8)]

str x8,? ? [sp,#(8*16+8*8)]

// 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 registers and return

ldp q0, q1, [sp,#(0*16)]

ldp q2, q3, [sp,#(2*16)]

ldp q4, q5, [sp,#(4*16)]

ldp q6, q7, [sp,#(6*16)]

ldp x0, x1, [sp,#(8*16+0*8)]

ldp x2, x3, [sp,#(8*16+2*8)]

ldp x4, x5, [sp,#(8*16+4*8)]

ldp x6, x7, [sp,#(8*16+6*8)]

ldr x8,? ? [sp,#(8*16+8*8)]

mov sp, fp

ldp fp, lr, [sp],#16

AuthenticateLR

.endmacro

其中的核心是_lookUpImpOrForward,上述匯編的過(guò)程,可以通過(guò)匯編調(diào)試來(lái)驗(yàn)證

在main中嚣潜,例如[person sayHello]對(duì)象方法調(diào)用處加一個(gè)斷點(diǎn)懒棉,并且開啟匯編調(diào)試【Debug -- Debug worlflow -- 勾選Always show Disassembly】,運(yùn)行程序

圖1

匯編中objc_msgSend加一個(gè)斷點(diǎn)该抒,執(zhí)行斷住慌洪,按住control + stepinto,進(jìn)入objc_msgSend的匯編:

圖2

在_objc_msgSend_uncached加一個(gè)斷點(diǎn)凑保,執(zhí)行斷住冈爹,按住control + stepinto,進(jìn)入?yún)R編

圖3

慢速查找-C/C++部分

根據(jù)匯編部分的提示欧引,全局續(xù)搜索lookUpImpOrForward频伤,最后在objc-runtime-new.mm文件中找到了源碼實(shí)現(xiàn),這是一個(gè)c實(shí)現(xiàn)的函數(shù)

IM PlookUpImpOrForward(id?inst,SEL?sel,Class cls,int?behavior)

{

? ? const IMP forward_imp = (IMP)_objc_msgForward_impcache;

? ? IMP?imp =nil;

? ? ClasscurClass;

? ? runtimeLock.assertUnlocked();

? ? // Optimistic cache lookup

? ? if(fastpath(behavior &LOOKUP_CACHE)) {

? ? ? ? imp =cache_getImp(cls, sel);

? ? ? ? if(imp)gotodone_nolock;

? ? }

? ? // runtimeLock is held during isRealized and isInitialized checking

? ? // to prevent races against concurrent realization.

? ? // runtimeLock is held during method search to make

? ? // method-lookup + cache-fill atomic with respect to method addition.

? ? // Otherwise, a category could be added but ignored indefinitely because

? ? // the cache was re-filled with the old value after the cache flush on

? ? // behalf of the category.

? ? runtimeLock.lock();

? ? // We don't want people to be able to craft a binary blob that looks like

? ? // a class but really isn't one and do a CFI attack.

? ? //

? ? // To make these harder we want to make sure this is a class that was

? ? // either built into the binary or legitimately registered through

? ? // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.

? ? //

? ? // TODO: this check is quite costly during process startup.

? ? checkIsKnownClass(cls);

? ? if(slowpath(!cls->isRealized())) {

? ? ? ? cls =realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);

? ? ? ? // runtimeLock may have been dropped but is now locked again

? ? }

? ? if(slowpath((behavior &LOOKUP_INITIALIZE) && !cls->isInitialized())) {

? ? ? ? cls =initializeAndLeaveLocked(cls, inst, runtimeLock);

? ? ? ? // runtimeLock may have been dropped but is now locked again

? ? ? ? // If sel == initialize, class_initialize will send +initialize and?

? ? ? ? // then the messenger will send +initialize again after this?

? ? ? ? // procedure finishes. Of course, if this is not being called?

? ? ? ? // from the messenger then it won't happen. 2778172

? ? }

? ? runtimeLock.assertLocked();

? ? curClass = cls;

? ? // The code used to lookpu the class's cache again right after

? ? // we take the lock but for the vast majority of the cases

? ? // evidence shows this is a miss most of the time, hence a time loss.

? ? //

? ? // The only codepath calling into this without having performed some

? ? // kind of cache lookup is class_getInstanceMethod().

? ? for(unsignedattempts =unreasonableClassCount();;) {

? ? ? ? // curClass method list.

? ? ? ? Methodmeth =getMethodNoSuper_nolock(curClass, sel);

? ? ? ? if(meth) {

? ? ? ? ? ? imp = meth->imp;

? ? ? ? ? ? gotodone;

? ? ? ? }

? ? ? ? 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.

? ? ? ? ? ? gotodone;

? ? ? ? }

? ? }

? ? // No implementation found. Try method resolver once.

? ? if(slowpath(behavior &LOOKUP_RESOLVER)) {

? ? ? ? behavior ^=LOOKUP_RESOLVER;

? ? ? ? returnresolveMethod_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)) {

? ? ? ? returnnil;

? ? }

? ? returnimp;

}

其整體的慢速查找流程如圖所示:

主要有以下幾步:

1.cache緩存中進(jìn)行查找芝此,即快速查找憋肖,找到則直接返回imp,反之婚苹,則進(jìn)入第2步

2.判斷cls是否是已知類岸更,如果不是,則報(bào)錯(cuò)膊升;類是否實(shí)現(xiàn)坐慰,如果沒有,則需要先實(shí)現(xiàn),確定其父類鏈结胀,此時(shí)實(shí)例化的目的是為了確定父類鏈赞咙,方法后續(xù)的循環(huán),是否初始化糟港,如果沒有攀操,則初始化

3.for循環(huán),按照類繼承鏈 或者 元類繼承鏈的順序查找

當(dāng)前cls的方法列表中使用二分查找算法查找方法秸抚,如果找到速和,則進(jìn)入cache寫入流程并返回imp,如果沒有找到剥汤,則返回nil當(dāng)前cls被賦值為父類颠放,如果父類等于nil,則imp = 消息轉(zhuǎn)發(fā)吭敢,并終止遞歸碰凶,進(jìn)入第4步

如果父類鏈中存在循環(huán),則報(bào)錯(cuò)鹿驼,終止循環(huán)

父類緩存中查找方法

如果未找到欲低,則直接返回nil,繼續(xù)循環(huán)查找

如果找到畜晰,則直接返回imp砾莱,執(zhí)行cache寫入流程

4.判斷是否執(zhí)行過(guò)動(dòng)態(tài)方法解析

,如果沒有凄鼻,執(zhí)行動(dòng)態(tài)方法解析

如果執(zhí)行過(guò)一次動(dòng)態(tài)方法解析腊瑟,則走到消息轉(zhuǎn)發(fā)流程

getMethodNoSuper_nolock方法:二分查找方法列表

查找方法列表的流程如下所示:

其二分查找核心的源碼實(shí)現(xiàn)如下:

ALWAYS_INLINE static method_t *

findMethodInSortedMethodList(SEL key, const method_list_t *list)

{

? ? ASSERT(list);

? ? const method_t * const first = &list->first;

? ? const method_t *base = first;

? ? const method_t *probe;

? ? uintptr_t keyValue = (uintptr_t)key; //key 等于 say666

? ? uint32_t count;

? ? //base相當(dāng)于low,count是max块蚌,probe是middle闰非,這就是二分

? ? for (count = list->count; count != 0; count >>= 1) {

? ? ? ? //從首地址+下標(biāo) --> 移動(dòng)到中間位置(count >> 1 左移1位即 count/2 = 4)

? ? ? ? probe = base + (count >> 1);


? ? ? ? uintptr_t probeValue = (uintptr_t)probe->name;


? ? ? ? //如果查找的key的keyvalue等于中間位置(probe)的probeValue,則直接返回中間位置

? ? ? ? if (keyValue == probeValue) {

? ? ? ? ? ? // -- while 平移 -- 排除分類重名方法

? ? ? ? ? ? while (probe > first && keyValue == (uintptr_t)probe[-1].name) {

? ? ? ? ? ? ? ? //排除分類重名方法(方法的存儲(chǔ)是先存儲(chǔ)類方法匈子,在存儲(chǔ)分類---按照先進(jìn)后出的原則河胎,分類方法最先出,而我們要取的類方法虎敦,所以需要先排除分類方法)

? ? ? ? ? ? ? ? //如果是兩個(gè)分類游岳,就看誰(shuí)先進(jìn)行加載

? ? ? ? ? ? ? ? probe--;

? ? ? ? ? ? }

? ? ? ? ? ? return (method_t *)probe;

? ? ? ? }


? ? ? ? //如果keyValue 大于 probeValue,就往probe即中間位置的右邊查找

? ? ? ? if (keyValue > probeValue) {

? ? ? ? ? ? base = probe + 1;

? ? ? ? ? ? count--;

? ? ? ? }

? ? }

? ? return nil;

}

算法原理簡(jiǎn)述為:從第一次查找開始其徙,每次都取中間位置胚迫,與想查找的key的value值作比較,如果相等唾那,則需要排除分類方法访锻,然后將查詢到的位置的方法實(shí)現(xiàn)返回,如果不相等,則需要繼續(xù)二分查找期犬,如果循環(huán)至count = 0還是沒有找到河哑,則直接返回nil,如下所示:

二分查找方法列表原理

以查找LGPerson類的say666實(shí)例方法為例龟虎,其二分查找過(guò)程如下

舉例說(shuō)明二分查找過(guò)程

cache_getImp方法:父類緩存查找

cache_getImp方法是通過(guò)匯編_cache_getImp實(shí)現(xiàn)璃谨,傳入的$0?是?GETIMP,如下所示:

父類緩存查找流程

如果父類緩存中找到了方法實(shí)現(xiàn)鲤妥,則跳轉(zhuǎn)至CacheHit即命中佳吞,則直接返回imp

如果在父類緩存中,沒有找到方法實(shí)現(xiàn)棉安,則跳轉(zhuǎn)至CheckMiss或者JumpMiss底扳,通過(guò)判斷$0跳轉(zhuǎn)至LGetImpMiss,直接返回nil

總結(jié)

對(duì)于對(duì)象方法(即實(shí)例方法)贡耽,即在類中查找衷模,其慢速查找的父類鏈?zhǔn)牵侯?-父類--根類--nil

對(duì)于類方法,即在元類中查找菇爪,其慢速查找的父類鏈?zhǔn)牵涸?-根元類--根類--nil

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末算芯,一起剝皮案震驚了整個(gè)濱河市柒昏,隨后出現(xiàn)的幾起案子凳宙,更是在濱河造成了極大的恐慌,老刑警劉巖职祷,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氏涩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡有梆,警方通過(guò)查閱死者的電腦和手機(jī)是尖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泥耀,“玉大人饺汹,你說(shuō)我怎么就攤上這事√荡撸” “怎么了兜辞?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)夸溶。 經(jīng)常有香客問我逸吵,道長(zhǎng),這世上最難降的妖魔是什么缝裁? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任扫皱,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘韩脑。我一直安慰自己氢妈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布段多。 她就那樣靜靜地躺著允懂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪衩匣。 梳的紋絲不亂的頭發(fā)上蕾总,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音琅捏,去河邊找鬼生百。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柄延,可吹牛的內(nèi)容都是我干的蚀浆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼搜吧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼市俊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起滤奈,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤摆昧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蜒程,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绅你,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年昭躺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忌锯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡领炫,死狀恐怖偶垮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帝洪,我是刑警寧澤似舵,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站碟狞,受9級(jí)特大地震影響啄枕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜族沃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一频祝、第九天 我趴在偏房一處隱蔽的房頂上張望泌参。 院中可真熱鬧,春花似錦常空、人聲如沸沽一。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)铣缠。三九已至,卻和暖如春昆禽,著一層夾襖步出監(jiān)牢的瞬間蝗蛙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工醉鳖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捡硅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓盗棵,卻偏偏與公主長(zhǎng)得像壮韭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纹因,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345