什麼是runtime
runtime
是由C
、C++
、彙編
一起寫成的api
,為OC
提供執行時。
執行時:裝載記憶體,提供執行時功能(依賴於
runtime
)
編譯時:把高階語言(OC、Swift、Java等)原始碼編譯成能夠識別的語言(機器語言-->二進位制)
底層庫關係:
物件和方法的本質
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [[LGPerson alloc] init];
[p run];
}
return 0;
}
複製程式碼
clang 編譯,cd到相應的檔案下,開啟終端,輸入下面命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++
或
clang -rewrite-objc main.m -o test.c++
複製程式碼
開啟生成的testMain.c++檔案,很長,有幾萬行程式碼,我們看主要的,如下
可有看出,物件的本質是一個結構體,方法的本質是傳送訊息。任何方法的呼叫都可以翻譯成是objc_msgSend
這個方法的呼叫
類方法和例項方法
物件呼叫
LGStudent *s = [[LGStudent alloc] init];
objc_msgSend(s, sel_registerName("run"));
複製程式碼
類方法的呼叫
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("run"));
複製程式碼
向父類發訊息(物件方法)
struct objc_super mySuper;
mySuper.receiver = s;
mySuper.super_class = class_getSuperclass([s class]);
objc_msgSendSuper(&mySuper, @selector(run));
複製程式碼
通過objc_msgSendSuper
向父類發訊息,第一個引數是結構體指標(父類)
向父類發訊息(類方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class]; // 當前類
myClassSuper.super_class = class_getSuperclass(object_getClass([s class])); // 當前類的類 = 元類
objc_msgSendSuper(&myClassSuper, @selector(run));
複製程式碼
Runtime的三種呼叫方式:
1、runtime api
--> (class_、objc_、object_)
2、NSObject api
--> (isKindOfClass、isMemberOfClass)
3、OC上層方法 -->(@selector)
注意點:
物件方法存在哪? ==> 類 例項方法
類方法存在哪? ==> 元類 例項方法
類方法在元類裡是什麼形式存在? ==> 例項方法
訊息的傳送Objc_msgSend
兩種方式:
- 快速 快取找-通過彙編
- 慢速
objc_msgSend 是用匯編寫的,高效以及C語言不能改通過寫一個函式,保留未知的引數,去跳轉到任意的指標,彙編可以利用暫存器實現。
下面進入乾貨,原始碼檢視如何尋找imp
,彙編部分:
上面這些組合語言,主要就是為了尋找imp
,呼叫_objc_msgSend
然後判斷接收者recevier
是否為空,為空則返回,不為空,就處理isa
,完畢之後就呼叫CacheLookup NORMAL
快取找imp
,CacheLookup
的結果又分三種,如果找到了,則呼叫CacheHit
進行call or return imp
;如果是第二種CheckMiss
,則進行下一步的函式呼叫__objc_msgSend_uncached
;第三種是如果在別的地方找到了這imp
,那麼就在這裡進行add
操作,為了方便下一次快速的查詢。
著重檢視一下方法__objc_msgSend_uncached
的呼叫:
__class_lookupMethodAndLoadCache3
就會發現,在彙編層次,已經走不下去了,其實從這個方法開始,就會從彙編轉到C++或者C層次的程式碼上了,後面繼續看。
從上面程式碼可以看出,這是一個漫長的查詢過程,先從自己的方法列表裡查詢,如果找到,就呼叫,同時把該imp
存放在快取中;如果沒有找到,就到自己的父類裡查詢,接著後面是一個往復的過程,遞迴查詢父類,直到找到NSObject
這個類。
動態解析
變數triedResolver
使得動態解析只走一次。重點關注_class_resolveMethod
方法:
上面程式碼判斷是否是元類,不是元類走_class_resolveInstanceMethod
方法,是元類走_class_resolveClassMethod
方法。
當我們重寫+resolveClassMethod
和+resolveInstanceMethod
方法的時候,是如何走到那裡的呢,可以通過下面原始碼看出
下面通過程式碼瞭解一下動態解析:
@interface LGPerson : NSObject
- (void)run;
@end
@implementation LGPerson
#pragma mark - 動態方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"來了 老弟");
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[LGPerson alloc] run];
}
return 0;
}
複製程式碼
注意,上面程式碼中,類LGPerson
沒有實現run
這個例項方法,同時父類以及分類裡都沒有實現,在.m
檔案裡重寫裡resolveInstanceMethod:
方法。執行程式碼
+ (BOOL)resolveInstanceMethod:(SEL)sel
明顯走了兩次,在上面原始碼中,我們分析了,變數triedResolver
使得動態解析只走一次,這裡又是什麼原因呢?
下面通過bt
尋找原因,在方法+ (BOOL)resolveInstanceMethod:(SEL)sel
加一個斷點,看下圖
_objc_msgSend_uncached
,然後走方法lookUpImpOrForward
,在走到方法_class_resolveInstanceMethod
裡,從這個大致的流程可以知道,這個流程,就是上面所分析的流程,尋找imp
的過程,沒有找到,就走到裡動態解析這一步;
下面跳過斷點,第二次走到+ (BOOL)resolveInstanceMethod:(SEL)sel
方法裡
如果我們在這一步進行重定向,可以使用下面的方式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
// 動態解析我們的 物件方法
NSLog(@"物件方法解析走這裡");
SEL readSEL = @selector(readBook);
Method readM= class_getInstanceMethod(self, readSEL);
IMP readImp = method_getImplementation(readM); // 獲取重定向方法的imp
const char *type = method_getTypeEncoding(readM);
return class_addMethod(self, sel, readImp, type); // 新增方法的實現
}
return [super resolveInstanceMethod:sel];
}
複製程式碼
上面說的都是例項方法,下面看看類方法,通過原始碼可以知道,在呼叫方法_class_resolveClassMethod
之後,還會在呼叫方法_class_resolveInstanceMethod
,呼叫方法_class_resolveClassMethod
我們可以理解,因為是動態解析類方法,但是為什麼會去呼叫方法_class_resolveInstanceMethod
,大家知道,這個方法是去動態解析例項方法所用的。
還記得前面說過的類方法的存放位置麼?第一它是類的類方法,第二它是元類的例項方法。所以在尋找類方法的imp
的過程就多了一步,如果有疑問,可以通過下面程式碼驗證
ip
和從元類裡獲取到的例項方法的ip
是一樣的。如果你還是感覺不可靠,那麼也可以通過下面的方式去驗證:
// NSObject的分類 驗證上述問題的時候,可以先後註釋掉例項方法和類方法
#import "NSObject+ZB.h"
#import <objc/runtime.h>
@implementation NSObject (ZB)
+ (void)run {
NSLog(@"NSObject === + run");
}
- (void)run {
NSLog(@"NSObject === - run");
}
@end
// ZBPerson繼承NSObject,只在.h檔案中宣告裡類方法run,並未去實現
@interface ZBPerson : NSObject
+ (void)run;
@end
// 直接呼叫類方法,同時註釋掉NSObject分類裡的類方法run
int main(int argc, const char * argv[]) {
@autoreleasepool {
[ZBPerson run];
}
return 0;
}
複製程式碼
按照上述描述,編譯執行結果如下
看到沒有,我們呼叫的明明是類方法run
,為什麼在這裡卻走到了一個例項方法裡面。希望小夥伴們能夠好好的去體會前面說過的一句話,類方法在元類中的儲存方式是以例項方法去儲存的
那麼如果開啟類方法run
的註釋呢?看下面結果
run
,沒有呼叫例項方法呢?因為這個過程,只要找到了imp
就會立即呼叫,後面的過程也就不用在走了。
記住下面這張圖,理清楚isa的走位,以及superclass的走位(如果圖中標註有錯誤,還希望指出,謝謝)
訊息轉發
當動態解析並沒有獲取到我們想要的imp
時,它返回一個NO
,接下來會走到訊息轉發。
下面給出了訊息轉發中的三個方法的使用
#pragma mark - 訊息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(run)) {
// 轉發給我們的ZBStudent 物件
return [ZBStudent new];
}
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(run)) {
// forwardingTargetForSelector 沒有實現 就只能方法簽名了
Method method = class_getInstanceMethod(object_getClass(self), @selector(readBook));
const char *type = method_getTypeEncoding(method);
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
NSLog(@"------%@-----",anInvocation);
anInvocation.selector = @selector(readBook);
[anInvocation invoke];
}
複製程式碼
這三個方法,相信大家已經很熟悉了,方法forwardingTargetForSelector:
允許我們替換訊息的接收者為其他物件,如果這個方法返回nil
或者self
,則會向物件傳送methodSignatureForSelector:
訊息,獲取到方法的簽名用於生成NSInvocation
物件,最後會進入訊息轉發機制forwardInvocation:
,不然將返回物件重新傳送訊息。
配合下面的圖,以上就是完整的訊息轉發
很多的應用也在這一層去實現的,不過現在不討論這個,我們主要看這三個方法是如何來的,那麼就繼續去檢視我們的原始碼
在原始碼中查詢方法_objc_msgForward_impcache
的實現會發現,它又走到了彙編裡,然而這部分只有彙編呼叫,沒有原始碼實現,也就是沒有開源。
那麼又是如何知道,訊息轉發的過程中呼叫了上面所說的三個方法呢?
介紹一個方法instrumentObjcMessageSends
extern void instrumentObjcMessageSends(BOOL);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
[ZBPerson run];
instrumentObjcMessageSends(NO);
}
return 0;
}
複製程式碼
方法instrumentObjcMessageSends
就是列印當前呼叫方法的呼叫過程,編譯完成後可以在路徑Macintosh HD/private/tmp/msgSends-xxxxx
下檢視檔案msgSends-xxxxx
,如下圖