iOS 底層探索 - 方法

iOS 底層探索 - 方法.png

iOS 底層探索系列

我們?cè)谇懊嫣剿髁藢?duì)象和類的底層原理,接下來(lái)我們要探索一下方法的本質(zhì),而在探索之前包晰,我們先簡(jiǎn)單過(guò)一遍 Runtime 的知識(shí)點(diǎn)整吆,如果讀者對(duì)這塊內(nèi)容已經(jīng)很熟悉了的話可以直接跳過(guò)第一章。

PS: 由于筆者對(duì)匯編暫時(shí)還是摸索的階段猫妙,關(guān)于匯編源碼的部分如有錯(cuò)誤入宦,歡迎指正。

一倦畅、Runtime 簡(jiǎn)介

眾所周知,Objective-C 是一門動(dòng)態(tài)語(yǔ)言绣的,而承載整個(gè) OC 動(dòng)態(tài)特性的就是 Runtime叠赐。關(guān)于 Runtime 更多內(nèi)容可以直接進(jìn)入官網(wǎng)文檔查看。

Runtime 是以 C/C++和匯編編寫而成的屡江,為什么不用 OC 呢芭概,這是因?yàn)閷?duì)我們編譯器來(lái)說(shuō),OC 屬于更高級(jí)的語(yǔ)言惩嘉,相比于 CC++ 以及匯編罢洲,執(zhí)行效率更慢,而在運(yùn)行時(shí)系統(tǒng)需要盡可能快的執(zhí)行效率文黎。

1.1 Runtime 的前世今生

Runtime 分為兩個(gè)版本惹苗,legacymodern,分別對(duì)標(biāo) OC 1.0OC 2.0耸峭。我們通常只需要專注于 modern 版本即可桩蓉,在 libObjc 源碼中體現(xiàn)在 new 后綴的文件上。

1.2 Runtime 三種交互方式

我們與 Runtime 打交道有三種方式:

  • 直接在 OC 層進(jìn)行交互:比如 @selector
  • NSObject 的方法:NSSelectorFromName
  • Runtime 的函數(shù): sel_registerName

二劳闹、方法的本質(zhì)探索

2.1 方法初探

image.png

我們可以看到院究,通過(guò) clang 重寫之后洽瞬,sayNB 在底層其實(shí)是一個(gè)消息的發(fā)送。

我們把右側(cè)的發(fā)送消息的代碼簡(jiǎn)化一下:

LGPerson *person = objc_msgSend((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("sayNB"));

由此可見(jiàn)业汰,真正發(fā)送消息的地方是 objc_msgSend伙窃,這個(gè)方法有兩個(gè)參數(shù),一個(gè)是消息的接受者為 id 類型蔬胯,第二個(gè)個(gè)是方法編號(hào) sel对供。

作為對(duì)比,run 方法就直接執(zhí)行了氛濒,并沒(méi)有通過(guò) objc_msgSend 進(jìn)行消息發(fā)送:

image.png

2.2 方法發(fā)送的幾種情況

LGStudent *s = [LGStudent new];
[s sayCode];        

objc_msgSend(s, sel_registerName("sayCode"));

上述代碼表示的是向?qū)ο?s 發(fā)送 sayCode 消息产场。


id cls = [LGStudent class];
void *pointA = &cls;
[(__bridge id)pointA sayNB];

objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));

上述代碼表示向 LGStudent 這個(gè)類發(fā)送 sayNB 消息。


// 向父類發(fā)消息(對(duì)象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));

上述代碼表示向父類發(fā)送 sayHello 消息舞竿。


//向父類發(fā)消息(類方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元類
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

上述代碼表示向父類的類京景,也就是元類發(fā)送 sayNB 消息。

我們?cè)?OC 中使用 objc_msgSend 的時(shí)候骗奖,要注意需要將 Enbale Strict of Checking of objc_msgSend Calls 設(shè)置為 NO确徙。這樣才不會(huì)報(bào)警告。

image.png

三执桌、探索 objc_msgSend

objc_msgSend 之所以采用匯編來(lái)實(shí)現(xiàn)鄙皇,是因?yàn)?/p>

  • 匯編更容易能被機(jī)器識(shí)別
  • 參數(shù)未知、類型未知對(duì)于 CC++ 來(lái)說(shuō)不如匯編更得心應(yīng)手

3.1 消息查找機(jī)制

  • 快速流程
  • 慢速流程

3.2 定位 objc_msgSend 匯編源碼

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check

判斷 p0 仰挣,也就是我們 objc_msgSend 的第一個(gè)參數(shù) id 消息的接收者是否為空伴逸。

ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class

讀取 x0 然后賦值到 p13 ,這里 p13 拿到的是 isa膘壶。為什么要拿 isa 呢错蝴,因?yàn)椴徽撌菍?duì)象方法還是類方法,我們都需要在類或者元類的緩存或方法列表中去查找颓芭,所以 isa 是必需的顷锰。

3.3 GetClassFromIsa_p16

通過(guò) GetClassFromIsa_p16,將獲取到的 class 存在 p16 上面亡问。

GetClassFromIsa_p16 源碼如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

這個(gè)方法的目的就是通過(guò)位移操作獲取 isashiftcls 然后進(jìn)行位運(yùn)算與操作得到真正的類信息官紫。

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

3.4 CacheLookup

獲取完 isa 之后,接下來(lái)就要進(jìn)行 CacheLookup 玛界,查找方法緩存万矾,我們?cè)賮?lái)到 CacheLookup 的源碼處:

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x16 = class to be searched
 *
 * Kills:
 *   x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    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:  // double wrap
    JumpMiss $0
    
.endmacro

通過(guò)上述代碼可知 CacheLookup 有三種模式:NORMALGETIMP慎框, LOOKUP

ldp p10, p11, [x16, #CACHE]
  • CacheLookup 需要讀取上一步拿到的類的 cache 緩存,而根據(jù)我們前面對(duì)類結(jié)構(gòu)的學(xué)習(xí)后添,這里顯然進(jìn)行 16 字節(jié)地址平移操作笨枯,然后把拿到的 cache_t 中的 bucketsoccupiedmask 賦值給 p10, p11
and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
  • 這里是將 w1w11 進(jìn)行與操作馅精,其實(shí)本質(zhì)就是 _cmd & mask严嗜。這一步和我們探索 cache_t 時(shí)遇到的
    image.png

    是一模模一樣樣的道理。目的就是拿到下標(biāo)洲敢。然后經(jīng)過(guò)哈希運(yùn)算之后漫玄,得到了 bucket 結(jié)構(gòu)體指針,然后將這個(gè)結(jié)構(gòu)體指針中的 imp压彭,sel 分別存在 p17睦优,p9 中。
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
  • 接著我們將上一步獲取到的 sel 和我們要查找的 sel(在這里也就是所謂的 _cmd)進(jìn)行比較壮不,如果匹配了汗盘,就通過(guò) CacheHitimp 返回;如果沒(méi)有匹配询一,就走下一步流程隐孽。
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
  • 由于上一步的 sel 沒(méi)有匹配上,我們需要接著進(jìn)行搜索健蕊。

3.5 CheckMiss

我們來(lái)到 CheckMiss 的源碼:

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

這里由于我們是 NORMAL 模式菱阵,所以會(huì)來(lái)到 __objc_msgSend_uncached:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

__objc_msgSend_uncached 中最核心的邏輯就是 MethodTableLookup,意為查找方法列表缩功。

3.6 MethodTableLookup

我們?cè)賮?lái)到 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)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // 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

我們觀察 MethodTableLookup 內(nèi)容之后會(huì)定位到 __class_lookupMethodAndLoadCache3晴及。在 __class_lookupMethodAndLoadCache3 之前會(huì)做一些準(zhǔn)備工作,真正的方法查找流程核心邏輯是位于 __class_lookupMethodAndLoadCache3 里面的掂之。 但是我們?nèi)炙阉?__class_lookupMethodAndLoadCache3 會(huì)發(fā)現(xiàn)找不到抗俄,這是因?yàn)榇藭r(shí)我們會(huì)從匯編跳入到 C/C++。所以去掉一個(gè)下劃線就能找到:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

四世舰、總結(jié)

  • 方法的本質(zhì)就是消息發(fā)送动雹,消息發(fā)送是通過(guò) objc_msgSend 以及其派生函數(shù)來(lái)實(shí)現(xiàn)的。
  • objc_msgSend 為了執(zhí)行效率以及 C/C++ 不能支持參數(shù)未知跟压,類型未知的代碼胰蝠,所以采用匯編來(lái)實(shí)現(xiàn) objc_msgSend
  • 消息查找或者說(shuō)方法查找震蒋,會(huì)優(yōu)先去從類中查找緩存茸塞,找到了就返回,找不到就需要去類的方法列表中查找查剖。
  • 由匯編過(guò)渡到 C/C++钾虐,在類的方法列表中查找失敗之后,會(huì)進(jìn)行轉(zhuǎn)發(fā)笋庄。核心邏輯位于 lookUpImpOrForward效扫。

我們下一章將會(huì)從 lookUpImpOrForward 開(kāi)始探索倔监,探索底層的方法查找的具體流程到底是怎么樣的,敬請(qǐng)期待~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菌仁,一起剝皮案震驚了整個(gè)濱河市浩习,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌济丘,老刑警劉巖谱秽,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異摹迷,居然都是意外死亡疟赊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門泪掀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)听绳,“玉大人,你說(shuō)我怎么就攤上這事异赫∫握酰” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵塔拳,是天一觀的道長(zhǎng)鼠证。 經(jīng)常有香客問(wèn)我,道長(zhǎng)靠抑,這世上最難降的妖魔是什么量九? 我笑而不...
    開(kāi)封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮颂碧,結(jié)果婚禮上荠列,老公的妹妹穿的比我還像新娘。我一直安慰自己载城,他們只是感情好肌似,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著诉瓦,像睡著了一般川队。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上睬澡,一...
    開(kāi)封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天固额,我揣著相機(jī)與錄音,去河邊找鬼煞聪。 笑死斗躏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昔脯。 我是一名探鬼主播瑟捣,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼馋艺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼栅干!你這毒婦竟也來(lái)了迈套?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤碱鳞,失蹤者是張志新(化名)和其女友劉穎桑李,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體窿给,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贵白,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崩泡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禁荒。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖角撞,靈堂內(nèi)的尸體忽然破棺而出呛伴,到底是詐尸還是另有隱情,我是刑警寧澤谒所,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布热康,位于F島的核電站,受9級(jí)特大地震影響劣领,放射性物質(zhì)發(fā)生泄漏姐军。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一尖淘、第九天 我趴在偏房一處隱蔽的房頂上張望奕锌。 院中可真熱鬧,春花似錦村生、人聲如沸惊暴。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缴守。三九已至,卻和暖如春镇辉,著一層夾襖步出監(jiān)牢的瞬間屡穗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工忽肛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留村砂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓屹逛,卻偏偏與公主長(zhǎng)得像础废,于是被迫代替她去往敵國(guó)和親汛骂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344