一、前言
最近我想要研究一下 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)擊此地址可以去下載。

二、IMP 和 objc_msgSend
1、SEL 和 IMP
首先我們先認(rèn)識一下 SEL 和 IMP。

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

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

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

每個方法都調(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,存放著方法的 sel 和 imp,sel 和 imp 最終會組成一張哈希表,這樣通過 sel 可以快速的查找到 imp,所以當(dāng)我們查找一個方法的時候,首先查找的就是這個 cache。

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

這部分屬于匯編部分,涉及了匯編語言的語法,雖然我不熟悉匯編語言,但是還是能夠找到一些關(guān)鍵的地方,理解整個流程的走向。
首先,在下載好的 runtime 源碼中搜索 _objc_msgSend,選擇查看 arm64 下的,也就是 objc-msg_arm64.s 如圖:

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

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

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

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

CacheHit和CheckMiss也分別都是宏,里面有相應(yīng)的操作,再次就不深入細(xì)講了。我們只要知道,如果沒有找到,CheckMiss中執(zhí)行的是__objc_msgSend_uncached方法。
MethodTableLookup又是個宏,這是個方法列表,也是個重點(diǎn)核心。

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

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

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

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

如果父類中也沒有,那么就開始下一個步驟,動態(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ā)流程的簡圖。

在這幾個步驟我們都可以設(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ù) resolver 是 YES,triedResolver 默認(rèn)第一次是NO,可以進(jìn)入判斷,但是只會調(diào)用一次,調(diào)用過后就會把 triedResolver 設(shè)為 YES。

在_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_resolveInstanceMethod的內(nèi)部實現(xiàn),實質(zhì)就是 消息的發(fā)送。

例如我們調(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方法

objc_defaultForwardHandler函數(shù)打印的
以上就是我對 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è)傳播——凡幾多
