深入理解 Objective-C Runtime 機制

Junyiii發表於2017-04-20

注:這篇文章適合對Runtime有一定了解的同學進一步理解 可以先看看這篇iOS Runtime(一) Runtime的應用

Objective-C

Objective-C 擴充套件了 C 語言,並加入了物件導向特性和 Smalltalk 式的訊息傳遞機制。而這個擴充套件的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 物件導向和動態機制的基石。

Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。理解 Objective-C 的 Runtime 機制可以幫我們更好的瞭解這個語言,適當的時候還能對語言進行擴充套件,從系統層面解決專案中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 訊息傳遞 (Messaging)。

Runtime 原理的概述

Objective-C的是一個執行時面向語言,這意味著當它可能在執行時決定如何實現而不是在編譯期。 這給你很大的靈活性,你可以根據需要將訊息重定向到適當的物件,或者甚至有意交換方法實現等。如果我們將它與C語言進行對比。

在很多語言,比如 C ,呼叫一個方法其實就是跳到記憶體中的某一點並開始執行一段程式碼。沒有任何動態的特性,因為這在編譯時就決定好了。而在 Objective-C 中,[object foo] 語法並不會立即執行 foo 這個方法的程式碼。它是在執行時給 object 傳送一條叫 foo 的訊息。這個訊息,也許會由 object 來處理,也許會被轉發給另一個物件,或者不予理睬假裝沒收到這個訊息。多條不同的訊息也可以對應同一個方法實現。這些都是在程式執行的時候決定的。

什麼是Objective-C執行時?

Objective-C執行時是一個執行庫,它是一個主要在C&Assembler中編寫的庫,它將物件導向的功能新增到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…

相關文章