Runtime底層原理分析之objc_msgSend

一、前言

最近我想要研究一下 Runtime 的底層原理,于是下載了一份 runtime 的源碼,學(xué)習(xí)的過程中也查閱了很多資料,詢問了很多大?!,F(xiàn)在總結(jié)一下我的收獲。
Runtime 是一套由 C、C++、匯編寫成的為 OC 提供運(yùn)行時機(jī)制的東西。Runtime 的源碼可以在蘋果的官網(wǎng) opensource 下載到,我下載的是當(dāng)時的最新版本 objc4-750,點(diǎn)擊此地址可以去下載

objc4-750.png

二、IMP 和 objc_msgSend

1、SEL 和 IMP

首先我們先認(rèn)識一下 SELIMP

IMP.png

從下載的 runtime 源碼中我們可以看到 IMP 的定義:
“IMP是指向函數(shù)具體實現(xiàn)的指針”。
這個函數(shù)體前兩個參數(shù)是 id(消息接收者,也就是對象),以及 SEL(方法的名字)。
類比:書的目錄(SEL)——頁碼(IMP:指向函數(shù)具體實現(xiàn)的指針)——具體內(nèi)容(函數(shù)實現(xiàn))
那么 SEL 是如何找到 IMP 的呢?

例如:有一個 Person 類,初始化一個 xiaoming 對象。

Person.png

然后在 study 方法執(zhí)行之前打一個斷點(diǎn),并運(yùn)行,當(dāng)運(yùn)行到斷點(diǎn)處,我們打開匯編調(diào)試模式,步驟如下 Debug—> Debug Workflow—>Always Show Disassembly。

debug匯編模式.png

我們就來到了一個匯編的界面,如下圖,觀察可看到在alloc、initstudy后面都有 objc_msgSend 函數(shù)的調(diào)用。
注意:在模擬器上運(yùn)行才能看到右側(cè)那些alloc、init、study函數(shù)名的打印,真機(jī)上是看不到的)

objc_msgSend函數(shù).png

每個方法都調(diào)用了objc_msgSend 函數(shù),很有意思不是嗎?但當(dāng)我們在工程里想搜索objc_msgSend看一下里面什么樣時卻發(fā)現(xiàn)沒有,這時我們下載的 runtime 源碼就派上用場了。

2、objc_msgSend

說到 Runtime ,就不能不提 objc_msgSend 消息轉(zhuǎn)發(fā)。
Objective-C 中,消息是直到運(yùn)行的時候才和方法實現(xiàn)綁定的。編譯器會把一個消息表達(dá)式,

[receiver message]

轉(zhuǎn)換成一個對消息函數(shù) objc_msgSend 的調(diào)用。
該函數(shù)有兩個主要參數(shù):消息接收者 receiver 和消息對應(yīng)的方法名字 selector ——也就是方法選標(biāo):

objc_msgSend(receiver, selector)

可同時接收消息中的任意數(shù)目的參數(shù):

objc_msgSend(receiver, selector, arg1, arg2, ...)

該消息函數(shù)做了動態(tài)綁定所需要的一切:

  • 它首先找到選標(biāo)所對應(yīng)的方法實現(xiàn)。因為不同的類對同一方法可能會有不同的實現(xiàn),所以找到的 方法實現(xiàn)依賴于消息接收者的類型。
  • 然后將消息接收者對象(指向消息接收者對象的指針)以及方法中指定的參數(shù)傳給找到的方法實現(xiàn)。
  • 最后,將方法實現(xiàn)的返回值作為該函數(shù)的返回值返回

三、objc_msgSend 源碼分析流程

接下來我們在 Runtime 中跟蹤一下 objc_msgSend 的整個流程。
objc_msgSend 查找分為兩種方式:

  • 1、快速查找:在緩存找,屬于匯編部分,cache_t、imp、哈希表。
  • 2、慢速查找:屬于 C/C++ 部分。

從源碼中我們可以找到 objc_class,可以得知,每個類都有一個緩存 cache,存放著方法的 selimp,selimp 最終會組成一張哈希表,這樣通過 sel 可以快速的查找到 imp,所以當(dāng)我們查找一個方法的時候,首先查找的就是這個 cache

cache.png

1、快速查找部分(匯編部分)

匯編部分快速查找.png

這部分屬于匯編部分,涉及了匯編語言的語法,雖然我不熟悉匯編語言,但是還是能夠找到一些關(guān)鍵的地方,理解整個流程的走向。

首先,在下載好的 runtime 源碼中搜索 _objc_msgSend,選擇查看 arm64 下的,也就是 objc-msg_arm64.s 如圖:

_objc_msgSend.png

通過sel找到imp:
然后在 objc-msg_arm64.s 中搜索查看 ENTRY _objc_msgSend
流程首先執(zhí)行的是 ENTRY _objc_msgSend

_objc_msgSend流程.png

然后再進(jìn)行 LNilOrTagged 判斷,
判斷是否為 nil 或者是否支持 tagged_pointers 類型,
nil 或者不支持就走 LRetrunZero,執(zhí)行 END_ENTRY _objc_msgSend 結(jié)束。

LReturnZero.png

如果不為 nil 并且支持,就走 LGetIsaDone 執(zhí)行完畢

LGetIsaDone.png

接下來在 LGetIsaDone 里執(zhí)行 CacheLookup NORMAL(從緩存里找imp)
如果有緩存,則直接 calls imp,否則執(zhí)行 objc_msgSend_uncached。
下面我們來看看CacheLookup,它是一個宏,如果在緩存里找到了,則執(zhí)行CacheHit,沒找到執(zhí)行CheckMiss,沒找到但是在其他地方已經(jīng)找到了,可以add添加進(jìn)緩存里去。

CacheLookup.png
CacheHitCheckMiss也分別都是宏,里面有相應(yīng)的操作,再次就不深入細(xì)講了。我們只要知道,如果沒有找到,CheckMiss中執(zhí)行的是__objc_msgSend_uncached方法。
__objc_msgSend_uncached詳細(xì).png

MethodTableLookup又是個宏,這是個方法列表,也是個重點(diǎn)核心。

MethodTableLookup.png

我們至此,發(fā)現(xiàn)搜索不到__class_lookupMethodAndLoadCache3,此時可能會失去信心,但不要擔(dān)心,我們能搜索到_class_lookupMethodAndLoadCache3,
_class_lookupMethodAndLoadCache3C++ 中的函數(shù),從而從匯編開始到 C++ 中了。

接下來看 objc-runtime-new.mm 中的_class_lookupMethodAndLoadCache3

_class_lookupMethodAndLoadCache3.png

快速查找(匯編階段)到此完畢!
因為快速查找沒找到,所以 慢速查找部分(C/C++部分)開始。

2、慢速查找部分(C/C++部分)

慢速查找部分.png

現(xiàn)在首先看 當(dāng)前類 的緩存里現(xiàn)在有沒有了,如果有,則返回 imp 。如果沒有,則執(zhí)行getMethodNoSuper_nolock,在當(dāng)前類查找,如果找到了,則執(zhí)行 log_and_fill_cache,把 imp 存到緩存中去,并返回要查找的 imp。這樣下次再找的時候,就會直接進(jìn)行匯編快速查找,直接CacheHit了。

tryThisClassCache.png

如果當(dāng)前類也沒有,則查找當(dāng)前類的父類,對父類進(jìn)行 for 循環(huán),因為最終的父類都是 NSObject ,NSObject 的父類則是 nil 了,所以我們只遍歷到 NSObject。如果父類里有緩存,那么通用把 imp 存到緩存中去,并返回要查找的 imp。如果沒有緩存,就執(zhí)行getMethodNoSuper_nolock

findsuper.jpg

如果父類中也沒有,那么就開始下一個步驟,動態(tài)方法解析。

3、動態(tài)方法解析 和 消息轉(zhuǎn)發(fā)

1)消息轉(zhuǎn)發(fā)流程簡述

當(dāng)一個方法沒有找到的時候,會經(jīng)歷幾個步驟才會崩潰,先是經(jīng)過動態(tài)方法解析步驟,如果消息還未得到處理,則進(jìn)入forwardingTargetForSelector:,還未處理則進(jìn)入methodSignatureForSelector:forwardInvocation
下面是一個消息轉(zhuǎn)發(fā)流程的簡圖。

消息轉(zhuǎn)發(fā)流程.png

在這幾個步驟我們都可以設(shè)法攔截崩潰信息,處理未處理的消息。我們可以對 crash 進(jìn)行自定義處理,防止崩潰的發(fā)生。也可以把 crash 收集起來發(fā)給服務(wù)器。

2)動態(tài)方法解析

下面詳細(xì)看一下動態(tài)方法解析的具體流程,首先如果父類中沒有找到 imp,那么開始進(jìn)行動態(tài)方法解析,執(zhí)行_class_resolveMethod。由于傳進(jìn)來的參數(shù) resolverYES,triedResolver 默認(rèn)第一次是NO,可以進(jìn)入判斷,但是只會調(diào)用一次,調(diào)用過后就會把 triedResolver 設(shè)為 YES

動態(tài)方法解析.png

_class_resolveMethod方法中,判斷如果是 元類,則執(zhí)行_class_resolveInstanceMethod,否則執(zhí)行_class_resolveClassMethod之后再執(zhí)行_class_resolveInstanceMethod。

問:
為什么執(zhí)行完_class_resolveClassMethod之后會再次執(zhí)行一次_class_resolveInstanceMethod?
答:
比如有一個類 Person,我們查找 Person 的一個類方法,如果沒找到,會繼續(xù)找他的第一個 元類,再找不到,會繼續(xù)找 根元類 ,最終會找到 NSObject
Person(類方法) 找——> 元類(實例方法) 找——> 根元類(實例方法) 找——> NSObject(實例方法)
實例方法存在類對象里面,類方法存在元類里面。

屏幕快照 2019-05-22 下午6.38.39.png

所以最終,還會執(zhí)行一次_class_resolveInstanceMethod

_class_resolveMethod.png

_class_resolveInstanceMethod的內(nèi)部實現(xiàn),實質(zhì)就是 消息的發(fā)送。

_class_resolveInstanceMethod.png

例如我們調(diào)用了 Person 類的對象方法 run,但 Person.m 沒有實現(xiàn) run 方法,并且父類也沒有,那么我們就開啟下面的動態(tài)方法解析,
重寫resolveInstanceMethod來動態(tài)解析對象方法,
重寫resolveClassMethod來動態(tài)解析類方法。

// .m沒有實現(xiàn),并且父類也沒有,那么我們就開啟下面的動態(tài)方法解析
#pragma mark - 動態(tài)方法解析

#import "LGPerson.h"
#include <objc/runtime.h>

@implementation Person

//- (void)run{
//    NSLog(@"%s",__func__);
//}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        // 我們動態(tài)解析我們的 對象方法
        NSLog(@"對象方法解析走這里");
        SEL readSEL = @selector(readBook);
        Method readM= class_getInstanceMethod(self, readSEL);
        IMP readImp = method_getImplementation(readM);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    return [super resolveInstanceMethod:sel];
}


+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(walk)) {
        // 我們動態(tài)解析我們的 類方法
        NSLog(@"類方法解析走這里");
        SEL hellowordSEL = @selector(helloWord);
        // 類方法就存在我們的元類的方法列表
        // 類 類犯法
        // 元類 對象實例方法
        Method hellowordM1= class_getClassMethod(self, hellowordSEL);
        Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
        IMP hellowordImp = method_getImplementation(hellowordM);
        const char *type = method_getTypeEncoding(hellowordM);
        NSLog(@"%s",type);
        return class_addMethod(object_getClass(self), sel, hellowordImp, type);
    }
    return [super resolveClassMethod:sel];
}

如果動態(tài)解析步驟也沒有找到解決辦法,那么再進(jìn)行到下一步驟。消息轉(zhuǎn)發(fā)。

3)消息轉(zhuǎn)發(fā)

<1>、forwardingTargetForSelector方法:
比如我們除了 Person 類,還有一個 Dog 類,這個類里才有 run 方法,那么當(dāng)我們判斷到是 run 方法找不到時,就可以把消息轉(zhuǎn)發(fā)給 Dog 類。

#pragma mark - 消息轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // 轉(zhuǎn)發(fā)給我們的 Dog 對象
        return [Dog new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

<2>、methodSignatureForSelector方法:
如果forwardingTargetForSelector沒有攔截住,那么只能用最后一道關(guān)卡methodSignatureForSelector了,獲取方法簽名,然后移交給消息轉(zhuǎn)發(fā)forwardInvocation。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // 獲取方法簽名
        Method method    = class_getInstanceMethod(object_getClass(self), @selector(readBook));
        const char *type = method_getTypeEncoding(method);
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];// 移交給消息轉(zhuǎn)發(fā)
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"------%@-----",anInvocation);
    anInvocation.selector = @selector(readBook);
    [anInvocation invoke];
}

如果以上所有步驟都沒有把消息成功處理,那么就會崩潰了。比如沒有找到study方法

unrecognized.png
這個我們常見的 unrecognized selector 錯誤信息其實是由這個objc_defaultForwardHandler函數(shù)打印的
objc_defaultForwardHandler.png

以上就是我對 Runtime 的消息轉(zhuǎn)發(fā)objc_megSend 的一個主要的底層原理分析總結(jié)。如果有寫錯的地方,還請幫忙指出,多謝,互相進(jìn)步。

以上的總結(jié)參考了并部分摘抄了以下文章,非常感謝以下作者的分享?。?br> 1、《Objective-C 2.0運(yùn)行時系統(tǒng)編程指南》
2、作者黃文臣的《iOS Runtime詳解之SEL,Class,id,IMP,_cmd,isa,method,Ivar》

轉(zhuǎn)載請備注原文出處,不得用于商業(yè)傳播——凡幾多

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容