[iOS開發]一篇文章帶你深入理解runtime

一. runtime簡介

runtime簡稱運行時,是一套底層的 C 語言 API。OC就是運行時機制,運行時機制中最主要的是消息機制。而消息機制就是開發者在編碼過程中,可以給任意一個對象發送消息,在編譯階段只是確定了要向接收者發送這條消息,而接受者將要如何響應和處理這條消息,那就要看運行時來決定了。

  • 對于C語言,函數的調用在編譯的時候會決定調用哪個函數。
  • 對于OC的函數,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。OC作為動態語言,它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。
  • 事實證明:
    在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要聲明過就不會報錯。
    在編譯階段,C語言調用未實現的函數就會報錯。

二. runtime的數據結構

源碼:

typedef struct objc_class *Class;
typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

通過以上源碼可以總結出以下幾點:

  • objc_class被typedef成了Class類型,objc_object被typedef成了id類型。
  • object類和NSObject類里面分別都包含一個objc_class類型的isa。
  • objc_class和objc_object都是結構體,且objc_class繼承于objc_object,因此Objective-C 中類也是一個對象,叫類對象。
  • objc_object包含一個isa_t類型的結構體,因此objc_class也會包含isa_t類型的結構體isa。
  • 在objc_class中,除了isa之外,還有3個成員變量,一個是父類的指針,一個是方法緩存,最后一個這個類的實例方法鏈表。

isa

isa可分為兩類:

  • 指針型isa:其值代表Class地址。
  • 非指針型isa:其值的部分代表Class的地址,64位的其他部分留作他用,為了節省內存。

isa指向:

  • 關于對象,其isa指向類對象,實例對象調用方法就是通過isa找到類對象,到類對象中找到方法進行調用。
  • 關于類對象,其isa指向元類對象,調用類方法就是通過isa找到元類對象,到元類對象中找到方法進行調用。

cache_t

源碼:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

typedef unsigned int uint32_t;
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long  uintptr_t;
typedef uintptr_t cache_key_t;

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

cache_t的數據結構如下:

它包括:

  • _buckets,一個散列表,用來存儲Method的鏈表。
  • mask: 分配用來緩存bucket的總數。
  • occupied:表明目前實際占用的緩存bucket的個數。

而bucket_t的數據結構是:

  • key: cache_key_t。
  • IMP:無類型的函數指針, 指向了一個方法的具體實現。

cache_t的意義在于:

  • 用于快速查找方法執行函數,比如在調用方法時,如果方法有緩存,那就不用到方法列表中逐一遍歷去查找方法的具體實現了。
  • 是可以增量擴展的哈希表結構,可增量擴展意思是當存儲的數據量大時可以擴展內存空間來支持更多的緩存。
  • 是局部性原理的最佳應用:局部性原理體現在將調用頻次最高的方法放到緩存當中,那么下次的命中率會更高,優化方法調用的性能。往往當一個實例對象調用方法時,首先根據對象的isa指針查找到它對應的類對象,然后在類對象方法列表中搜索方法,如果沒有找到,就使用super_class指針到父類對象的方法列表中查找,一旦找到就調用方法,如果沒有找到,有可能消息轉發,也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%。所以使用Cache來緩存經常調用的方法,當調用方法時,優先在Cache查找,如果沒有找到,再到方法列表中查找,這樣就能優化方法調用的性能。

class_data_bits_t

class_data_bits_t數據結構:

總結:

  • class_data_bits_t結構體是對class_rw_t的封裝,代表著類或分類中變量,屬性,方法鏈表。
  • class_rw_t結構體又是對class_ro_t的封裝,它的結構包括:class_ro_t,protocols,properties,methods,后三者是二維數組,比如methods的元素是一個類或者它的分類的方法列表methodList,而methodList的元素是method_t。
  • class_ro_t結構體是一個指向常量的指針,ro代表它是只讀的,
    存儲編譯器決定了的成員變量,屬性,方法和遵守協議,而它們都是一維數組。

method_t

源碼:

struct method_t {
    SEL name;
    const char *types;
    IMP 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; }
    };
};

從源碼可以看出method_t結構體有三個成員變量:

  • SEL name:代表方法名稱。
  • const char* types:返回值和參數組合,不可變的字符類型指針。
  • IMP imp:無類型的函數指針,對應的是函數體。

其中types的數據結構如下:

我們知道runtime的messageSend方法頭兩個參數是默認固定的參數,第一個參數默認是self,第二個參數默認是方法名稱SEL,所以types值中代表參數的兩個固定的值是@:,比如方法- (void)aMethod;,它的types值就是:v@:v代表返回值是void,@代表第一個參數是id類型的self,:代表第二個參數SEL。

總結

runtime整個數據結構如下:

三. 消息傳遞

實例對象、類對象、元類對象

先看一張經典的圖,它能展示出三者之間的關系:

總結:

  • 類對象存儲實例方法列表等信息。
  • 元類對象存儲類方法列表等信息。
  • 類對象和元類對象都是objc_class數據結構類型的,都繼承于objc_object, 所以都有isa指針,所以實例對象可以通過isa找到類對象,進而訪問類對象存儲的實例方法列表,類對象也可以通過isa找到元類對象,進而訪問元類對象存儲的類方法列表。

實例對象調用實例方法過程

圖解過程:

在此過程中會經歷三種查找:

  • 緩存查找:根據給定的SEL,通過哈希函數的算法得到bucket_t對應cache_t數組中的索引位置,哈希函數表達式就是SEL選擇器因子和對應的mask作與操作,mask是cache_t的成員變量,查找到bucket_t后,就能提取到IMP指針,返回給調用方。
  • 在當前類中查找:
  1. 對于已排序好的方法列表,采用二分查找算法查找方法對應執行函數。
  2. 對于沒有排序的方法列表,采用一般遍歷查找方法對應執行函數。
  • 父類逐級查找:通過當前類的superClass指針去訪問父類,先判斷父類是否為空,是空就結束查找,否就看能否根據SEL在父類緩存中找到相應的方法實現,如果找到就結束流程,如果父類緩存中沒有,就遍歷父類方法列表,看看能否查找到SEL對應的方法實現,如果還沒有,就根據父類的superClass找到父類的父類,繼續此逐級查找過程。


所以整個消息傳遞流程可以作如下總結:

  1. 當實例對象調用一個方法時,系統會根據實例對象的isa指針找到它的類對象,查找類對象的緩存中是否有對應SEL的IMP方法實現,如果沒有,則在當前類對象的實例方法列表,二分查找或遍歷查找同名的方法實現IMP,如果找到,填充到緩存中,并返回selector,如果沒有找到,根據類對象的superClass指針到父類對象的方法列表中進行父類逐級查找,直到根類對象還沒找到,則進入消息轉發流程。
  2. 當調用一個類方法時,類對象根據isa指針找到元類對象,也是先到元類對象的緩存中查找,沒找到則在當前元類對象的類方法列表,二分查找或遍歷查找同名的方法實現IMP,如果找到,填充到緩存中,并返回selector,如果沒有找到,根據元類對象的superClass指針到父元類對象的方法列表中進行父類逐級查找,直到根元類對象,如果還沒找到,就到根元類對象的superClass指向的根類對象(也就是NSObject)中找同名的方法,如果還沒找到就走消息轉發流程。

四. 消息轉發機制

我們定義的方法如果沒有實現,系統會依次調用以下方法:

- (void)testMethod;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        NSLog(@"resolveInstanceMethod:");
        
        return NO;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector:(");
    return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testMethod)) {
        NSLog(@"methodSignatureForSelector:");
        return [NSMethodSignature signatureWithObjCTypes: "v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"forwardInvocation:");
}

整個實例方法的消息轉發流程:

  • resolveInstanceMethod方法如果返回YES,那么根據SEL就找到了對應的函數實現,代表消息已處理;如果返回NO,系統會給出第二次機會去處理消息。
    比如,我們給原來定義的方法動態添加一個實現:
void testImp (void) {
    NSLog(@"hhh");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        NSLog(@"resolveInstanceMethod:");
        
        //動態添加方法實現
        class_addMethod(self, @selector(testMethod), testImp, "v@:");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

那么根據testMethod方法的SEL就找到了testImp的實現,因此就不會再調用下面的方法,消息轉發結束。

  • forwardingTargetForSelector方法用于指定一個轉發目標,系統會把消息轉發給此目標。如果我們在此函數中返回一個nil,也就是沒有指定轉發目標的話,系統會給出第三次處理這條消息的機會。
  • methodSignatureForSelector方法的返回值是個NSMethodSignature對象,它是對方法的返回值,返回值類型,參數,參數類型,參數個數的封裝,如果此函數返回了方法簽名,則系統會接著調用forwardInvocation方法,如果該方法能處理消息,則消息轉發流程就此結束,如果不能處理或methodSignatureForSelector沒有返回方法簽名,則此消息就被標記為無法處理。

消息轉發流程圖解:

五. method-Swizzling

將兩個方法對應的方法實現進行交換:


使用場景:比如統計頁面進入次數或時長,那么可以交換系統的viewWillAppear方法,在新方法中插入統計邏輯即可,而不用在每個頁面的viewWillAppear方法中都作統計。

#import "UIViewController+runtime.h"
#import <objc/runtime.h>
@implementation UIViewController (runtime)

+ (void)load {
    Method testMethod = class_getInstanceMethod([UIViewController class], @selector(viewWillAppear:));
    Method testOtherMethod = class_getInstanceMethod(self, @selector(testOtherMethod));
    method_exchangeImplementations(testMethod, testOtherMethod);
}

- (void)testOtherMethod {
    //插入邏輯
    NSLog(@"testOtherMethod");
}

六. 動態方法解析

動態方法解析總結:

  • 用@dynamic標記屬性,則屬性的get,set方法就是運行時添加的而不是編譯時聲明好的。
  • 編譯時語言和動態運行時語言的區別是:
  1. 動態運行時語言將函數決議推遲到運行時,為方法添加具體的實現。比如當我們把一個屬性標示為@dynamic時,代表著不需要編譯器在編譯時為屬性生成get,set方法實現,而是在運行時具體調用屬性的get或set方法時,再添加相應的實現。
  2. 編譯時語言在編譯期進行函數決議,也就是在編譯期間就確定了一個方法名稱對應的具體的方法實現,而在具體的運行中是不能修改的。

方法緩存

方法緩存的目的:優化方法查找性能,因為當一個方法在比較“上層”的類中,用比較“下層”(繼承關系上的上下層)對象去調用的時候,如果沒有緩存,那么整個查找鏈是相當長的。就算方法是在這個類里面,當方法比較多的時候,每次都查找也是費事費力的一件事情。所以將調用頻率高的方法緩存下來,提高命中率,也是上面文章所說的局部性原理的最佳應用。

推薦美團技術團隊的這篇文章: 深入理解 Objective-C:方法緩存

關于runtime的面試題

1. [self class] 和 [super class]。

 #import "Animal.h"

 @interface Dog: Animal

 @end

 @implementation Dog

 - (id)init
 {
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
 }
 @end

self和super的區別:

  • self是類的一個隱藏參數,每個方法的實現的第一個參數是self。
  • super是一個”編譯器標示符”,而不是隱藏參數,它負責告訴編譯器,當調用方法時,去調用父類的方法,而不是本類中的方法。

在調用[super class]的時候,runtime會去調用objc_msgSendSuper方法。

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

從源碼可以看出:

  • objc_msgSendSuper方法中,第一個參數是一個objc_super結構體。
  • objc_super結構體有兩個變量,一個是接收消息的receiver,一個是當前類的父類super_class。

調用objc_msgSendSuper的流程是:
從objc_super結構體中的superClass指向的父類的方法列表開始查找selector,找到后以objc_super->receiver去調用父類的這個selector。所以最后的調用者是objc->receiver也就是self,即objc_super->receiver = self。

最后結論:
無論是self還是super,接收者都是當前對象,區別是self調用class時,是從該實例對象的類對象順次向上查找,而super調用class時,越過了該實例對象的類對象,是從其類對象的父類對象順次向上查找,不過最終都找到了根類對象NSObject中的class方法,從而返回相同的結果。

2. isKindOfClass 與 isMemberOfClass。

    BOOL result1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL result2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL result3 = [(id)[Dog class] isKindOfClass:[Dog class]];
    BOOL result4 = [(id)[Dog class] isMemberOfClass:[Dog class]];
    NSLog(@"%d %d %d %d", result1, result2, result3, result4);

在isKindOfClass中有一個循環,先判斷class是否等于meta class,不等就繼續循環判斷是否等于super class,不等再繼續取super class,如此循環下去。

[NSObject class]執行完之后調用isKindOfClass,第一次判斷先判斷NSObject 和 NSObject的meta class是否相等,很明顯不等。接著第二次循環判斷NSObject與meta class的superclass是否相等,而Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身,所以相等,result1為YES。

同理,[Dog class]執行完之后調用isKindOfClass,第一次for循環,Dog的meta Class與Dog不等,第二次循環,Dog meta Class的super class 指向的是 NSObject meta Class, 和 Dog也不相等。第三次循環,NSObject Meta Class的super class指向的是NSObject Class,和 Dog 不相等。第四次循環,NSObject Class 的super class 指向 nil, 和 Dog不相等,最終result3為NO。

如果是Dog的實例對象,[dog isKindOfClass:[Dog class],那么此時就應該輸出YES了。因為在isKindOfClass函數中,判斷Dog的isa指向是否是自己的類Dog,第一次for循環就能輸出YES了。

isMemberOfClass是拿到自己的isa指針和自己比較,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject不相等。第四行,isa指向Dog的Meta Class,和Dog也不等,所以第二行result2和第四行result4都為NO。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • runtime 常見作用 該文件的目的是為讀者誰可能有興趣學習的Objective-C的運行時。因為這不是一個關于...
    陽明先生_X自主閱讀 905評論 1 13
  • 一.Runtime的概念 runtime是oc底層的一套C語言編寫的API,將OC代碼轉換成運行時代碼。其中最主要...
    wps_pro閱讀 1,159評論 1 1
  • 你要知道的runtime都在這里 轉載請注明出處 http://www.lxweimin.com/p/eac6ed1...
    WWWWDotPNG閱讀 19,441評論 6 84
  • 到現在為止已經寫了61篇簡書了,就是說堅持寫了兩個月的簡書了,這就是習慣的力量,每天抽出一點時間寫簡書,沒有感覺花...
    RogueQ閱讀 308評論 0 0
  • 《和善和堅定》 與孩子相處是一門藝術,也是一門技術 今天與大家分享表揚孩子的10種方法、希望大家能在生活中運用起來...
    未泯_ddec閱讀 298評論 0 0