iOS Runtime 底層原理:動態方法解析、訊息轉發原始碼分析

Runtime底層原理
瞭解了Runtime函式含義,我們就可以直接使用Runtime的API了,那接下來繼續探究Runtime的原始碼,經過原始碼分析來更加深刻的瞭解Runtime原理。
開發應用
都知道Runtime很重要,但是有很多小夥伴根本不瞭解,或者只是知道可以替換方法啊、可以交換兩個方法的呼叫,專案中也用不到,
從進入iOS開始,寫了無數個 [[objc alloc] init]
,這個到底在幹嘛?初始化和init?alloc和init到底做了什麼?
通過彙編檢視方法呼叫
Person *person = [Person alloc]; Person *person1 = [person init]; Person *person2 = [person init]; NSLog(@"%p-----%p------%p", person, person1, person2);
這裡會輸出什麼呢?
0x10102e1a0-----0x10102e1a0------0x10102e1a0
來,讓我們斷點看下, alloc
和 init
是怎麼呼叫的

objc_msgSend
我們看到呼叫 alloc
和 init
都調起了 objc_msgSend
,接下來跟著符號斷點走

libobjc

callAlloc
進入 libobjc
庫的dylib之後走 +[NSObject alloc]
方法,指標調起 _objc_rootAlloc
,進入 _objc_rootAlloc
方法,繼續調起 callAlloc
,通過暫存器,可以看到alloc已經通過類建立例項物件

類物件
init
按照同樣方法 依然可以通過彙編看出方法呼叫順序,可以用真機進行測試並列印
通過編譯C++
當新的物件被建立時,其記憶體同時被分配,例項變數也同時被初始化。物件的第一個例項變數是一個指向該物件的類結構的指標,叫做 isa。通過該指標,物件可以訪問它對應的類以及相應的父類。在 Objective-C 執行時系統中物件需要有 isa 指標,我們一般建立的從 NSObject 或者 NSProxy 繼承的物件都自動包括 isa 變數。接下來看下物件被建立的過程
首先,我們通過clang命令
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++
也可以用 clang -rewrite-objc main.m -o test.c++
命令,只不過會有很多警告、程式碼會更長(大概9萬多行)。
編譯main函式中的OC程式碼為C++程式碼
int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; [p run]; } return 0; }
編譯後多一個testMain.c++檔案,開啟後在程式碼最後面會發現我們的main函式
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run")); } return 0; }
可以看出,我們的方法呼叫會編譯成objc_msgSend,

person物件
由此還會發現物件的本質其實就是一個結構體
下層通訊(通過原始碼檢視objc_msgSend內部實現)
首先我們到蘋果open source官網下載最新原始碼

原始碼
在 方法呼叫的時候,會發送 objc_msgSend
訊息, objc_msgSend
會根據sel找到函式實現的指標imp ,進而執行函式,那sel是如何找到imp的呢?
objc_msgSend
在傳送訊息時候根據sel查詢imp有兩種方式
- 快速(通過彙編的快取快速查詢)
- 慢速(C配合C++、彙編一起查詢)
先看下objc_class

objc_class
bits中包含各種資料,cache(每個類都有一個)用來儲存方法select和imp,select和imp會以雜湊表形式存在
objc_msgSend
在快速查詢的時候,就是通過彙編查詢objc_class中的cache,如果找到則直接返回,否則通過C的lookup,找到後再存入cache
彙編部分快速查詢
首先呼叫 objc_msgSend
會走到ENTRY

ENTRY
先判斷p0檢查是否為空和tagged pointer(特殊型別)判斷,呼叫 LNilOrTagged
進行isa處理,通過isa找到相應類class,最後呼叫 LGetIsaDone
來執行 CacheLookup
在快取中查詢imp,如果查詢到直接調起imp否則調起objc_msgSend_uncached,objc_msgSend_uncached有兩種情況

CacheLookup
首先,第一個是CacheHit,直接調起imp,第二個是CheckMiss,之後呼叫objc_msgSend_uncached,第三個就是add,下面是CacheHit和CheckMiss的巨集

CacheLookup macro
那如果在快取中沒有查詢到imp,調起 objc_msgSend_uncached
,在方法列表中找到imp之後再 TailCallFunctionPointer
調起imp
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// 方法列表中找到imp TailCallFunctionPointer x17
重點:MethodTableLookup是怎麼操作的
小知識點:通過method list查詢method,下面是method_t的結構,method其實是一個雜湊表,sel和imp是鍵值對
struct method_t { SEL name; const char *types;// 引數型別 MethodListIMP imp; struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; };
進入 MethodTableLookup
之後,調起了 __class_lookupMethodAndLoadCache3
,如下圖

MethodTableLookup
__class_lookupMethodAndLoadCache3
是C方法,再次進入 _class_lookupMethodAndLoadCache3
方法, 注意,因為這裡由彙編跳轉到C,所以要全域性搜尋 _class_lookupMethodAndLoadCache3
,要刪去一個 "_"
,下面是 _class_lookupMethodAndLoadCache3
函式
/*********************************************************************** * _class_lookupMethodAndLoadCache. * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). * This lookup avoids optimistic cache scan because the dispatcher * already tried that. **********************************************************************/ IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
C/C++部分查詢
調起 lookUpImpOrForward
,因為當前cls物件已經經過彙編編譯到結構,有了isa,並且在cache中沒有找到,所以這裡的initialize為YES,cache為NO,resolver為YES

image.png
進入 lookUpImpOrForward
,這裡再次判斷是否存在cache,如果有則直接快速查詢,但是這裡是NO,所以不會走。接下來走 checkIsKnownClass
判斷是否是已經宣告的類,如果沒有則報錯"Attempt to use unknown class %p.",之後走 realizeClass
判斷是否已經實現,如果就相應賦值data。

realizeClass
data賦值後走 _class_initialize
初始化cls,接下來開始 retry
操作。
前方高能
再次進行cache_getImp,why?併發啊,還有重對映(在初始化init的時候有個remap(class)第一次通過彙編找不到,但是在載入類的時候對當前類進行重對映)

cache_getImp
接下來開始先在自己的class_rw_t的methods中根據sel查詢方法返回method_t

method_t
如果拿到Method後儲存到快取中,保證以後呼叫可以直接走彙編的CacheHit快速查詢,如果拿不到則繼續從父類開始查詢,直到找到NSObject(因為NSObject的父類為nil),如果找到imp則一樣儲存在快取中,如果到最後還是沒有查詢到,則進入動態方法解析。

父類查詢方法
動態方法解析
如果前面一系列操作還是沒有找到方法,那麼就會進行動態方法解析,動態方法解析只執行一次

動態方法解析
首先執行 _class_resolveMethod
,這裡會執行 +resolveClassMethod
或者 +resolveInstanceMethod
。

class resolveMethod
先判斷當前cls是否為元類,如果是元類則執行 _class_resolveClassMethod
,再執行 _class_resolveInstanceMethod
,如果不是元類則直接執行 _class_resolveInstanceMethod
, _class_resolveInstanceMethod
內部呼叫objc_msgSend實現訊息傳送,對cls傳送了 SEL_resolveInstanceMethod
型別的訊息,所以在方法中會走到 resolveInstanceMethod
方法。

class resolveInstanceMethod
為什麼元類最後也執行了 _class_resolveInstanceMethod
方法呢?因為類方法以例項物件的形態存在元類裡面,比如類方法中沒有找到方法,會去元類中查詢,元類中沒有再繼續去根元類中查詢,最後會查到NSObject。
程式碼示例:
.h實現
- (void)run; + (void)eat;
.m實現(沒有實現-run方法和+eat方法)
- (void)walk { NSLog(@"%s",__func__); } + (void)drink { NSLog(@"%s",__func__); } // .m沒有實現,並且父類也沒有,那麼我們就開啟動態方法解析 //- (void)walk{ //NSLog(@"%s",__func__); //} //+ (void)drink{ //NSLog(@"%s",__func__); //} #pragma mark - 動態方法解析 + (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(run)) { // 我們動態解析我們的 物件方法 NSLog(@"物件方法解析走這裡"); SEL walkSEL = @selector(walk); Method readM= class_getInstanceMethod(self, walkSEL); 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(eat)) { // 我們動態解析我們的 物件方法 NSLog(@"類方法解析走這裡"); SEL drinkSEL = @selector(drink); // 類方法就存在我們的元類的方法列表 // 類 類犯法 // 元類 物件例項方法 //Method hellowordM1= class_getClassMethod(self, hellowordSEL); Method drinkM= class_getInstanceMethod(object_getClass(self), drinkSEL); IMP drinkImp = method_getImplementation(drinkM); const char *type = method_getTypeEncoding(drinkM); NSLog(@"%s",type); return class_addMethod(object_getClass(self), sel, drinkImp, type); } return [super resolveClassMethod:sel]; }
訊息轉發
經歷了動態方法決議還沒有找到,會進入蘋果尚未開源的訊息轉發,繼續查詢方法, _objc_msgForward_impcache
再次跨域到彙編。

訊息轉發
走到 __objc_msgForward_impcache
後執行 __objc_msgForward

__objc_msgForward_impcache
沒有了原始碼實現,但是我們可以通過 instrumentObjcMessageSends
函式來列印呼叫堆疊資訊。可以進入 instrumentObjcMessageSends
內部看下具體實現。

instrumentObjcMessageSends
先判斷了是否可以寫入日誌資訊等,接下來同步日誌檔案

logMessageSend
所以我們每次執行會在 /private/tmp
檔案下多一個 msgSends-xxx
檔案,裡面是所有呼叫過程

堆疊呼叫資訊
如果還沒有找到的話最後會報錯呼叫 __objc_forward_handler

__objc_forward_handler
這也是我們在方法報錯的時候會報 unrecognized selector sent to instance %p " "(no message forward handler is installed)"
錯誤的原因,會提示出元類資訊, +
或者 -
方法,方法的名字還有SEL方法編號
程式碼示例:
#pragma mark - 例項物件訊息轉發 - (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); //if (aSelector == @selector(run)) { //// 轉發給Student物件 //return [Student new]; //} return [super forwardingTargetForSelector:aSelector]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); if (aSelector == @selector(run)) { // forwardingTargetForSelector 沒有實現,就只能方法簽名了 return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"%s",__func__); NSLog(@"------%@-----",anInvocation); anInvocation.selector = @selector(walk); [anInvocation invoke]; } #pragma mark - 類訊息轉發 + (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); return [super forwardingTargetForSelector:aSelector]; } // + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); if (aSelector == @selector(walk)) { return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } return [super methodSignatureForSelector:aSelector]; } + (void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"%s",__func__); NSString *sto = @"奔跑吧"; anInvocation.target = [Student class]; [anInvocation setArgument:&sto atIndex:2]; NSLog(@"%@",anInvocation.methodSignature); anInvocation.selector = @selector(run:); [anInvocation invoke]; }
現在我們應該也知道了為什麼 objc_msgSend
的原始碼用的彙編,因為彙編可以通過暫存器x0-x31來保留未知引數來跳轉到任意的指標,還有彙編更高效一點,而C滿足不了。
言而總之,總而言之
Runtime就是C、C++、彙編實現的一套API,給OC增加的一個執行時功能,也就是我們平時所說的執行時。
在執行工程時工程會被裝載到記憶體,來提供執行時功能。