runtime
是由C
、C++
、彙編
一塊兒寫成的api
,爲OC
提供運行時。c++
運行時:裝載內存,提供運行時功能(依賴於
runtime
)
編譯時:把高級語言(OC、Swift、Java等)源代碼編譯成可以識別的語言(機器語言-->二進制)api
底層庫關係: 緩存
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [[LGPerson alloc] init];
[p run];
}
return 0;
}
複製代碼
clang 編譯,cd到相應的文件下,打開終端,輸入下面命令bash
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++
或
clang -rewrite-objc main.m -o test.c++
複製代碼
打開生成的testMain.c++文件,很長,有幾萬行代碼,咱們看主要的,以下iphone
可有看出,對象的本質是一個結構體,方法的本質是發送消息。任何方法的調用均可以翻譯成是objc_msgSend
這個方法的調用函數
對象調用ui
LGStudent *s = [[LGStudent alloc] init];
objc_msgSend(s, sel_registerName("run"));
複製代碼
類方法的調用spa
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
向父類發消息,第一個參數是結構體指針(父類)3d
向父類發消息(類方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class]; // 當前類
myClassSuper.super_class = class_getSuperclass(object_getClass([s class])); // 當前類的類 = 元類
objc_msgSendSuper(&myClassSuper, @selector(run));
複製代碼
Runtime的三種調用方式:
一、runtime api
--> (class_、objc_、object_)
二、NSObject api
--> (isKindOfClass、isMemberOfClass)
三、OC上層方法 -->(@selector)
注意點:
對象方法存在哪? ==> 類 實例方法
類方法存在哪? ==> 元類 實例方法
類方法在元類裏是什麼形式存在? ==> 實例方法
兩種方式:
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
,以下圖