瞭解了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
是怎麼調用的
我們看到調用alloc
和init
都調起了objc_msgSend
,接下來跟着符號斷點走
進入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,
由此還會發現對象的本質其實就是一個結構體
下層通訊(通過源碼查看objc_msgSend內部實現)
首先我們到蘋果open source官網下載最新源碼
在方法調用的時候,會發送objc_msgSend
消息,objc_msgSend
會根據sel找到函數實現的指針imp,進而執行函數,那sel是如何找到imp的呢?
objc_msgSend
在發送消息時候根據sel查找imp有兩種方式
- 快速(通過彙編的緩存快速查找)
- 慢速(C配合C++、彙編一起查找)
先看下objc_class
bits中包含各種數據,cache(每個類都有一個)用來存儲方法select和imp,select和imp會以哈希表形式存在
objc_msgSend
在快速查找的時候,就是通過彙編查找objc_class中的cache,如果找到則直接返回,否則通過C的lookup,找到後再存入cache
彙編部分快速查找
首先調用objc_msgSend
會走到ENTRY
先判斷p0檢查是否爲空和tagged pointer(特殊類型)判斷,調用LNilOrTagged
進行isa處理,通過isa找到相應類class,最後調用LGetIsaDone
來執行CacheLookup
在緩存中查找imp,如果查找到直接調起imp否則調起objc_msgSend_uncached,objc_msgSend_uncached有兩種情況
首先,第一個是CacheHit,直接調起imp,第二個是CheckMiss,之後調用objc_msgSend_uncached,第三個就是add,下面是CacheHit和CheckMiss的宏
那如果在緩存中沒有查找到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
,如下圖
__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
進入lookUpImpOrForward
,這裏再次判斷是否存在cache,如果有則直接快速查找,但是這裏是NO,所以不會走。接下來走checkIsKnownClass
判斷是否是已經聲明的類,如果沒有則報錯"Attempt to use unknown class %p.",之後走realizeClass
判斷是否已經實現,如果就相應賦值data。
data賦值後走_class_initialize
初始化cls,接下來開始retry
操作。
前方高能
再次進行cache_getImp,why?併發啊,還有重映射(在初始化init的時候有個remap(class)第一次通過彙編找不到,但是在加載類的時候對當前類進行重映射)
接下來開始先在自己的class_rw_t的methods中根據sel查找方法返回method_t
如果拿到Method後保存到緩存中,保證以後調用可以直接走彙編的CacheHit快速查找,如果拿不到則繼續從父類開始查找,直到找到NSObject(因爲NSObject的父類爲nil),如果找到imp則一樣保存在緩存中,如果到最後還是沒有查找到,則進入動態方法解析。
動態方法解析
如果前面一系列操作還是沒有找到方法,那麼就會進行動態方法解析,動態方法解析只執行一次
首先執行_class_resolveMethod
,這裏會執行+resolveClassMethod
或者 +resolveInstanceMethod
。
先判斷當前cls是否爲元類,如果是元類則執行_class_resolveClassMethod
,再執行_class_resolveInstanceMethod
,如果不是元類則直接執行_class_resolveInstanceMethod
,_class_resolveInstanceMethod
內部調用objc_msgSend實現消息發送,對cls發送了SEL_resolveInstanceMethod
類型的消息,所以在方法中會走到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
沒有了源碼實現,但是我們可以通過instrumentObjcMessageSends
函數來打印調用堆棧信息。可以進入instrumentObjcMessageSends
內部看下具體實現。
先判斷了是否可以寫入日誌信息等,接下來同步日誌文件
所以我們每次運行會在/private/tmp
文件下多一個msgSends-xxx
文件,裏面是所有調用過程
如果還沒有找到的話最後會報錯調用__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增加的一個運行時功能,也就是我們平時所說的運行時。
在運行工程時工程會被裝載到內存,來提供運行時功能。
該文章爲記錄本人的學習路程,希望能夠幫助大家,也歡迎大家點贊留言交流!!!文章地址:https://www.jianshu.com/p/1ddd15e47343