筆記-runtime源碼解析之讓你完全瞭解底層源碼

什麼是runtime

runtime是由CC++彙編一塊兒寫成的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

兩種方式:

  • 快速 緩存找-經過彙編
  • 慢速

objc_msgSend 是用匯編寫的,高效以及C語言不能改經過寫一個函數,保留未知的參數,去跳轉到任意的指針,彙編能夠利用寄存器實現。

下面進入乾貨,源碼查看如何尋找imp,彙編部分:

上面這些彙編語言,主要就是爲了尋找imp,調用_objc_msgSend而後判斷接收者recevier是否爲空,爲空則返回,不爲空,就處理isa,完畢以後就調用CacheLookup NORMAL緩存找impCacheLookup的結果又分三種,若是找到了,則調用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,以下圖

相關文章
相關標籤/搜索