Objective-C

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

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

訊息傳遞(Messaging)

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

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

事實上,在編譯時你寫的 Objective-C 函式呼叫的語法都會被翻譯成一個 C 的函式呼叫 - objc_msgSend() 。比如,下面兩行程式碼就是等價的:

[array insertObject:foo atIndex:5];

objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

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

objc_object, objc_class 以及 Ojbc_method

在 Objective-C 中,類、物件和方法都是一個 C 的結構體,從 objc/objc.h 標頭檔案中,我們可以找到他們的定義:

struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    **struct objc_method_list **methodLists**;
    **struct objc_cache *cache**;
    struct objc_protocol_list *protocols;
#endif
};

struct objc_method_list {  
    struct objc_method_list *obsolete;
    int method_count;

#ifdef __LP64__
    int space;
#endif

    /* variable length structure */
    struct objc_method method_list[1];
};

struct objc_method {  
    SEL method_name;
    char *method_types;    /* a string representing argument/return types */
    IMP method_imp;
};

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

從這些定義中可以看出傳送一條訊息也就 objc_msgSend 做了什麼事。舉 objc_msgSend(obj, foo) 這個例子來說:

  1. 首先,通過 obj 的 isa 指標找到它的 class ;
  2. 在 class 的 method list 找 foo ;
  3. 如果 class 中沒到 foo,繼續往它的 superclass 中找 ;
  4. 一旦找到 foo 這個函式,就去執行它的實現IMP .

但這種實現有個問題,效率低。但一個 class 往往只有 20% 的函式會被經常呼叫,可能佔總呼叫次數的 80% 。每個訊息都需要遍歷一次 objc_method_list 並不合理。如果把經常被呼叫的函式快取下來,那可以大大提高函式查詢的效率。這也就是 objc_class 中另一個重要成員 objc_cache 做的事情 - 再找到 foo 之後,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來。當再次收到 foo 訊息的時候,可以直接在 cache 裡找到,避免去遍歷 objc_method_list.

動態方法解析和轉發

在上面的例子中,如果 foo 沒有找到會發生什麼?通常情況下,程式會在執行時掛掉並丟擲 unrecognized selector sent to … 的異常。但在異常丟擲前,Objective-C 的執行時會給你三次拯救程式的機會:

  1. Method resolution
  2. Fast forwarding
  3. Normal forwarding

Method Resolution

首先,Objective-C 執行時會呼叫 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函式實現。如果你添加了函式並返回 YES, 那執行時系統就會重新啟動一次訊息傳送的過程。還是以 foo 為例,你可以這麼實現:

void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "[email protected]:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

Core Data 有用到這個方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在執行時動態新增的。

如果 resolve 方法返回 NO ,執行時就會移到下一步:訊息轉發(Message Forwarding)

PS:iOS 4.3 加入很多新的 runtime 方法,主要都是以 imp 為字首的方法,比如 imp_implementationWithBlock() 用 block 快速建立一個 imp 。
上面的例子可以重寫成:

IMP fooIMP = imp_implementationWithBlock(^(id _self) {  
    NSLog(@"Doing foo");
});

class_addMethod([self class], aSEL, fooIMP, "[email protected]:");  

Fast forwarding

如果目標物件實現了 -forwardingTargetForSelector: ,Runtime 這時就會呼叫這個方法,給你把這個訊息轉發給其他物件的機會。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(foo:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

只要這個方法返回的不是 nil 和 self,整個訊息傳送的過程就會被重啟,當然傳送的物件會變成你返回的那個物件。否則,就會繼續 Normal Fowarding 。

這裡叫 Fast ,只是為了區別下一步的轉發機制。因為這一步不會建立任何新的物件,但下一步轉發會建立一個 NSInvocation 物件,所以相對更快點。

Normal forwarding

這一步是 Runtime 最後一次給你挽救的機會。首先它會發送 -methodSignatureForSelector: 訊息獲得函式的引數和返回值型別。如果 -methodSignatureForSelector: 返回 nil ,Runtime 則會發出 -doesNotRecognizeSelector: 訊息,程式這時也就掛掉了。如果返回了一個函式簽名,Runtime 就會建立一個 NSInvocation 物件併發送 -forwardInvocation: 訊息給目標物件。

NSInvocation 實際上就是對一個訊息的描述,包括selector 以及引數等資訊。所以你可以在 -forwardInvocation: 裡修改傳進來的 NSInvocation 物件,然後傳送 -invokeWithTarget: 訊息給它,傳進去一個新的目標:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL sel = invocation.selector;

    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } 
    else {
        [self doesNotRecognizeSelector:sel];
    }
}

Cocoa 裡很多地方都利用到了訊息傳遞機制來對語言進行擴充套件,如 Proxies、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來作為代理轉發訊息的;NSUndoManager 擷取一個訊息之後再發送;而 Responder Chain 保證一個訊息轉發給合適的響應者。

總結

Objective-C 中給一個物件傳送訊息會經過以下幾個步驟:

  1. 在物件類的 dispatch table 中嘗試找到該訊息。如果找到了,跳到相應的函式IMP去執行實現程式碼;
  2. 如果沒有找到,Runtime 會發送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個訊息;
  3. 如果 resolve 方法返回 NO,Runtime 就傳送 -forwardingTargetForSelector: 允許你把這個訊息轉發給另一個物件;
  4. 如果沒有新的目標物件返回, Runtime 就會發送 -methodSignatureForSelector:-forwardInvocation: 訊息。你可以傳送 -invokeWithTarget: 訊息來手動轉發訊息或者傳送 -doesNotRecognizeSelector: 丟擲異常。

利用 Objective-C 的 runtime 特性,我們可以自己來對語言進行擴充套件,解決專案開發中的一些設計和技術問題。下一篇文章,我會介紹 Method Swizzling 技術以及如何利用 Method Swizzling 做 Logging。

Reference