深刻理解 Objective-C Runtime 機制

注:這篇文章適合對Runtime有必定了解的同窗進一步理解 能夠先看看這篇iOS Runtime(一) Runtime的應用javascript

Objective-C

Objective-C 擴展了 C 語言,並加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。html

Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。理解 Objective-C 的 Runtime 機制能夠幫咱們更好的瞭解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。java

Runtime 原理的概述

Objective-C的是一個運行時面向語言,這意味着當它可能在運行時決定如何實現而不是在編譯期。 這給你很大的靈活性,你能夠根據須要將消息重定向到適當的對象,或者甚至有意交換方法實現等。若是咱們將它與C語言進行對比。git

在不少語言,好比 C ,調用一個方法其實就是跳到內存中的某一點並開始執行一段代碼。沒有任何動態的特性,由於這在編譯時就決定好了。而在 Objective-C 中,[object foo] 語法並不會當即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另外一個對象,或者不予理睬僞裝沒收到這個消息。多條不一樣的消息也能夠對應同一個方法實現。這些都是在程序運行的時候決定的。github

什麼是Objective-C運行時?

Objective-C運行時是一個運行庫,它是一個主要在C&Assembler中編寫的庫,它將面向對象的功能添加到C中以建立Objective-C。 這意味着它加載類信息,全部方法調度,方法轉發等。Objective-C運行時本質上建立全部支持結構,使面向對象的編程與Objective-C可能。objective-c

Objective-C 類和對象

Objective-c類自己也是對象,而運行時經過建立Meta類處理這一點。 當你發送一個消息,如[NSObject alloc],你其實是發送一個消息到類對象,該類對象須要是一個MetaClass的實例,它自己是根元類的實例。 而若是你說NSObject的子類,你的類指向NSObject做爲它的超類。 然而,全部元類都指向根元類做爲它們的超類。 全部的元類都只有它們響應的消息的方法列表的類方法。 因此當你發送消息到類對象,如[NSObject alloc],而後objc_msgSend()實際上經過元類查看它的響應,而後若是它找到一個方法,操做類對象。編程

爲何Objective-C的對象都要繼承 NSObject

最初當你開始Cocoa開發,教程都說作繼承類NSObject,而後開始編碼的東西,你享受不少好處。 有一件事你甚至沒有意識到,發生在你身上的是將對象設置爲使用Objective-C運行時。數組

MyObject *object = [[MyObject alloc] init];複製代碼

執行的第一個消息是+ alloc。 若是你看看文檔,它說「新實例的isa實例變量被初始化爲描述類的數據結構;全部其餘實例變量的內存設置爲0」 因此經過繼承Apples類,咱們不只繼承了一些偉大的屬性,並且咱們繼承了在內存中容易地分配和建立咱們的對象的能力,它匹配運行時指望的結構(使用指向咱們類的isa指針)&是大小 的咱們的類。緩存

那麼什麼是類緩存? (objc_cache * cache)

一個 class 每每只有 20% 的函數會被常常調用,可能佔總調用次數的 80% 。每一個消息都須要遍歷一次 objc_method_list 並不合理。若是把常常被調用的函數緩存下來,那能夠大大提升函數查詢的效率。這也就是 objc_class 中另外一個重要成員 objc_cache 作的事情 - 再找到 foo 以後,把 foo 的 method_name 做爲 key ,method_imp 做爲 value 給存起來。當再次收到 foo 消息的時候,能夠直接在 cache 裏找到,避免去遍歷 objc_method_list.數據結構

當Objective-C運行時經過跟蹤它的isa指針檢查對象時,它能夠找到一個實現許多方法的對象。然而,你可能只調用它們的一小部分,而且每次查找時,搜索全部選擇器的類分派表沒有意義。因此類實現一個緩存,每當你搜索一個類分派表,並找到相應的選擇器,它把它放入它的緩存。因此當objc_msgSend()查找一個類的選擇器,它首先搜索類緩存。這是基於這樣的理論:若是你在類上調用一個消息,你可能之後再次調用該消息。因此若是咱們考慮到這一點,這意味着若是咱們有一個NSObject子類,名爲MyObject並運行如下代碼

MyObject *obj = [[MyObject alloc] init];

@implementation MyObject
-(id)init {
    if(self = [super init]){
        [self setVarA:@」blah」];
    }
    return self;
}
@end複製代碼

發生如下狀況(1)[MyObject alloc]被首先執行。 MyObject類不實現alloc,因此咱們將沒法在類中找到+ alloc,並遵循指向NSObject的超類指針。(2)咱們要求NSObject是否響應+ alloc,而且它。 + alloc檢查接收器類是MyObject,並分配一個內存塊大小的類,並初始化它的isa指向MyObject類的指針,咱們如今有一個實例,最後咱們把+ alloc NSObject的類緩存爲類對象3)到目前爲止,咱們發送了一個類消息,但如今咱們發送一個實例消息,只是調用-init或咱們指定的初始化。固然咱們的類響應這個消息因此 - (id)init get的放入緩存(4)而後self = [super init]被調用。super 是一個指向對象超類的魔術關鍵字,因此咱們去NSObject並調用它的init方法。這是爲了確保OOP繼承工做正常,由於全部的超類都將正確初始化它們的變量,而後你(在子類中)能夠正確初始化你的變量,而後覆蓋超類,若是你真的須要。在NSObject的狀況下,沒有什麼很是重要的,但並不老是這樣。

看這段代碼

#import < Foundation/Foundation.h>

@interface MyObject : NSObject
{
 NSString *aString;
}

@property(retain) NSString *aString;

@end

@implementation MyObject

-(id)init
{
 if (self = [super init]) {
  [self setAString:nil];
 }
 return self;
}

@synthesize aString;

@end


int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

 id obj1 = [NSMutableArray alloc];
 id obj2 = [[NSMutableArray alloc] init];

 id obj3 = [NSArray alloc];
 id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];

 NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
 NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));

 NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
 NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));

 id obj5 = [MyObject alloc];
 id obj6 = [[MyObject alloc] init];

 NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
 NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));

 [pool drain];
    return 0;
}複製代碼

結果是

NSMutableArray
NSMutableArray
NSArray
NSArray
MyObject
MyObject複製代碼

事實上

obj1 class is __NSPlaceholderArray obj2 class is NSCFArray obj3 class is __NSPlaceholderArray obj4 class is NSCFArray obj5 class is MyObject obj6 class is MyObject複製代碼

這是由於在Objective-C中有一個潛在的+ alloc返回一個類的對象,而後-init返回另外一個類的對象。

消息發送

I’m sorry that I long ago coined the term 「objects」 for this topic because it gets many people to focus on the lesser idea. The big idea is 「messaging」 – that is what the kernal[sic] of Smalltalk is all about... The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.

Alan Kay 曾屢次強調 Smalltalk 的核心不是面向對象,面向對象只是 the lesser ideas,消息傳遞 纔是 the big idea。

消息傳遞的關鍵藏於 objc_object 中的 isa 指針和 objc_class 中的 class dispatch table。

在 Objective-C 中,類、對象和方法都是一個 C 的結構體,從 objc/objc.h 頭文件中,咱們能夠找到他們的定義:

id objc_msgSend ( id self, SEL op, ... );複製代碼
typedef struct objc_object *id;複製代碼
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};複製代碼
typedef struct objc_class *Class;複製代碼
struct objc_class : objc_object {
    Class superclass;
    const char *name;
    uint32_t version;
    uint32_t info;
    uint32_t instance_size;
    struct old_ivar_list *ivars;
    struct old_method_list **methodLists;
    Cache cache;
    struct old_protocol_list *protocols;
    // CLS_EXT only
    const uint8_t *ivar_layout;
    struct old_class_ext *ext;
    /.../
}複製代碼

struct objc_ivar_list ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表
struct objc_method_list *
methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表

struct old_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct old_ivar ivar_list[1];
};複製代碼
struct old_method_list {
    void *obsolete;

    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    // 可變長的方法數組
    struct old_method method_list[1];
};複製代碼

objc_method_list 本質是一個有 objc_method 元素的可變長度的數組。一個 objc_method 結構體中有函數名,也就是SEL,有表示函數類型的字符串 (見 Type Encoding) ,以及函數的實現IMP。

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)
#define CACHE_BUCKET_IMP(B)   ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))
#endif
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};複製代碼
struct old_protocol_list {
    struct old_protocol_list *next;
    long count;
    struct old_protocol *list[1];
};複製代碼
struct old_class_ext {
    uint32_t size;
    const uint8_t *weak_ivar_layout;
    struct old_property_list **propertyLists;
};複製代碼

消息發送的步驟

  1. Check for ignored selectors (GC) and short-circuit.若是 selector 是須要被忽略的垃圾回收用到的方法,則將 IMP 結果設爲 _objc_ignored_method,這是個彙編程序入口,能夠理解爲一個標記。(OSX)
  2. Check for nil target.檢查對象是否爲nil
    • If nil & nil receiver handler configured, jump to handler
    • If nil & no handler (default), cleanup and return.
  3. Search the class’s method cache for the method IMP 在cache 中查找IMP
    • If found, jump to it.找到,跳轉到相應的內存地址
    • Not found: lookup the method IMP in the class itself 未找到,在類的method_list中查找
      • If found, jump to it.找到,跳轉
      • If not found, jump to forwarding mechanism.未找到,進入消息分發的步驟

消息分發的步驟

  • 在對象類的 dispatch table 中嘗試找到該消息。若是找到了,跳到相應的函數IMP去執行實現代碼;
  • 若是沒有找到,Runtime 會發送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個消息;
  • 若是 resolve 方法返回 NO,Runtime 就發送 -forwardingTargetForSelector: 容許你把這個消息轉發給另外一個對象;
  • 若是沒有新的目標對象返回, Runtime 就會發送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你能夠發送 -invokeWithTarget: 消息來手動轉發消息或者發送 -doesNotRecognizeSelector: 拋出異常。

objc_msgSend函數

事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用 - objc_msgSend() 。

Hybrid vTable Dispatch

新的 Objc-runtime-new.m 這樣寫到

/*********************************************************************** * vtable dispatch * * Every class gets a vtable pointer. The vtable is an array of IMPs. * The selectors represented in the vtable are the same for all classes * (i.e. no class has a bigger or smaller vtable). * Each vtable index has an associated trampoline which dispatches to * the IMP at that index for the receiver class's vtable (after * checking for NULL). Dispatch fixup uses these trampolines instead * of objc_msgSend. * Fragility: The vtable size and list of selectors is chosen at launch * time. No compiler-generated code depends on any particular vtable * configuration, or even the use of vtable dispatch at all. * Memory size: If a class's vtable is identical to its superclass's * (i.e. the class overrides none of the vtable selectors), then * the class points directly to its superclass's vtable. This means * selectors to be included in the vtable should be chosen so they are * (1) frequently called, but (2) not too frequently overridden. In * particular, -dealloc is a bad choice. * Forwarding: If a class doesn't implement some vtable selector, that * selector's IMP is set to objc_msgSend in that class's vtable. * +initialize: Each class keeps the default vtable (which always * redirects to objc_msgSend) until its +initialize is completed. * Otherwise, the first message to a class could be a vtable dispatch, * and the vtable trampoline doesn't include +initialize checking. * Changes: Categories, addMethod, and setImplementation all force vtable * reconstruction for the class and all of its subclasses, if the * vtable selectors are affected. **********************************************************************/複製代碼
static const char * const defaultVtable[] = {
    "allocWithZone:",
    "alloc",
    "class",
    "self",
    "isKindOfClass:",
    "respondsToSelector:",
    "isFlipped",
    "length",
    "objectForKey:",
    "count",
    "objectAtIndex:",
    "isEqualToString:",
    "isEqual:",
    "retain",
    "release",
    "autorelease",
};
static const char * const defaultVtableGC[] = {
    "allocWithZone:",
    "alloc",
    "class",
    "self",
    "isKindOfClass:",
    "respondsToSelector:",
    "isFlipped",
    "length",
    "objectForKey:",
    "count",
    "objectAtIndex:",
    "isEqualToString:",
    "isEqual:",
    "hash",
    "addObject:",
    "countByEnumeratingWithState:objects:count:",
};複製代碼

Runtime 經過 vTable 的方式 加速調用類的經常使用方法。

Category

可是category則徹底不同,它是在運行期決議的。
就category和extension的區別來看,咱們能夠推導出一個明顯的事實,extension能夠添加實例變量,而category是沒法添加實例變量的(由於在運行期,對象的內存佈局已經肯定,若是添加實例變量就會破壞類的內部佈局,這對編譯型語言來講是災難性的)

-category和+load方法

咱們知道,在類和category中均可以有+load方法,那麼有兩個問題:
1)、在類的+load方法調用的時候,咱們能夠調用category中聲明的方法麼?
2)、這麼些個+load方法,調用順序是咋樣的呢?

1)、能夠調用,由於附加category到類的工做會先於+load方法的執行
2)、+load的執行順序是先類,後category,而category的+load執行順序是根據編譯順序決定的。

部份內容引用和翻譯自
www.friday.com/bbum/2009/1…
cocoasamurai.blogspot.com/2010/01/und…

最近會每日一篇的把博客上的文章遷移到掘金,但願你們關注我。

本文的附贈的Runtime一些用法的Sample
github.com/JunyiXie/XJ…

相關文章
相關標籤/搜索