筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

佐籩發表於2019-03-28

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

什麼是runtime

runtime是由CC++彙編一起寫成的api,為OC提供執行時。

執行時:裝載記憶體,提供執行時功能(依賴於runtime
編譯時:把高階語言(OC、Swift、Java等)原始碼編譯成能夠識別的語言(機器語言-->二進位制)

底層庫關係:

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

物件和方法的本質

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++檔案,很長,有幾萬行程式碼,我們看主要的,如下

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

可有看出,物件的本質是一個結構體方法的本質是傳送訊息。任何方法的呼叫都可以翻譯成是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,彙編部分:

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

上面這些組合語言,主要就是為了尋找imp,呼叫_objc_msgSend然後判斷接收者recevier是否為空,為空則返回,不為空,就處理isa,完畢之後就呼叫CacheLookup NORMAL快取找impCacheLookup的結果又分三種,如果找到了,則呼叫CacheHit進行call or return imp;如果是第二種CheckMiss,則進行下一步的函式呼叫__objc_msgSend_uncached;第三種是如果在別的地方找到了這imp,那麼就在這裡進行add操作,為了方便下一次快速的查詢。

著重檢視一下方法__objc_msgSend_uncached的呼叫:

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
玩過原始碼的小夥伴,走到這裡方法__class_lookupMethodAndLoadCache3就會發現,在彙編層次,已經走不下去了,其實從這個方法開始,就會從彙編轉到C++或者C層次的程式碼上了,後面繼續看。
筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

從上面程式碼可以看出,這是一個漫長的查詢過程,先從自己的方法列表裡查詢,如果找到,就呼叫,同時把該imp存放在快取中;如果沒有找到,就到自己的父類裡查詢,接著後面是一個往復的過程,遞迴查詢父類,直到找到NSObject這個類。

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
如果這個過程方法還沒有查詢到,那就進入動態解析的過程。

動態解析

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

變數triedResolver使得動態解析只走一次。重點關注_class_resolveMethod方法:

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

上面程式碼判斷是否是元類,不是元類走_class_resolveInstanceMethod方法,是元類走_class_resolveClassMethod方法。

當我們重寫+resolveClassMethod+resolveInstanceMethod方法的時候,是如何走到那裡的呢,可以通過下面原始碼看出

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

下面通過程式碼瞭解一下動態解析:

@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:方法。執行程式碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
可以發現,+ (BOOL)resolveInstanceMethod:(SEL)sel明顯走了兩次,在上面原始碼中,我們分析了,變數triedResolver使得動態解析只走一次,這裡又是什麼原因呢?

下面通過bt尋找原因,在方法+ (BOOL)resolveInstanceMethod:(SEL)sel加一個斷點,看下圖

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
這是第一次來到這個方法裡,看一下紅色框裡的內容,先走方法_objc_msgSend_uncached,然後走方法lookUpImpOrForward,在走到方法_class_resolveInstanceMethod裡,從這個大致的流程可以知道,這個流程,就是上面所分析的流程,尋找imp的過程,沒有找到,就走到裡動態解析這一步;

下面跳過斷點,第二次走到+ (BOOL)resolveInstanceMethod:(SEL)sel方法裡

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
熟悉訊息轉發流程的小夥伴們或許已經看的很明白了,第二次走到這裡,是在訊息轉發的過程中走過來的,走到這裡的前提就是動態解析失敗了。具體的流程會在訊息轉發的過程中說到。

如果我們在這一步進行重定向,可以使用下面的方式

+ (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的過程就多了一步,如果有疑問,可以通過下面程式碼驗證

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
從上面的程式碼可以看出,獲取類的類方法得到的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;
}

複製程式碼

按照上述描述,編譯執行結果如下

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
看到沒有,我們呼叫的明明是類方法run,為什麼在這裡卻走到了一個例項方法裡面。希望小夥伴們能夠好好的去體會前面說過的一句話,類方法在元類中的儲存方式是以例項方法去儲存的

那麼如果開啟類方法run的註釋呢?看下面結果

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
為什麼只呼叫了類方法run,沒有呼叫例項方法呢?因為這個過程,只要找到了imp就會立即呼叫,後面的過程也就不用在走了。

記住下面這張圖,理清楚isa的走位,以及superclass的走位(如果圖中標註有錯誤,還希望指出,謝謝)

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

訊息轉發

當動態解析並沒有獲取到我們想要的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:,不然將返回物件重新傳送訊息。

配合下面的圖,以上就是完整的訊息轉發

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

很多的應用也在這一層去實現的,不過現在不討論這個,我們主要看這三個方法是如何來的,那麼就繼續去檢視我們的原始碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼
在原始碼中查詢方法_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,如下圖

筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼

相關文章