Objc-Runtime理解

初學 Objective-C(以下簡稱ObjC) 的人很容易忽略一個 ObjC 特性 —— ObjC Runtime。這是因爲這門語言很容易上手,幾個小時就能學會怎麼使用,所以程序員們往往會把時間都花在瞭解 Cocoa 框架以及調整自己的程序的表現上。然而 Runtime 應該是每一個 ObjC 都應該要了解的東西,至少要理解編譯器會把

[target doMethodWith:var1];

編譯成:

objc_msgSend(target,@selector(doMethodWith:),var1);

這樣的語句。理解 ObjC Runtime 的工作原理,有助於你更深入地去理解 ObjC 這門語言,理解你的 App 是怎樣跑起來的。我想所有的 Mac/iPhone 開發者,無論水平如何,都會從中獲益的。

ObjC Runtime 是開源的

ObjC Runtime 的代碼是開源的,可以從這個站點下載: opensource.apple.com

這個是所有開源代碼的鏈接: http://www.opensource.apple.com/source/

這個是ObjC rumtime 的源代碼: http://www.opensource.apple.com/source/objc4/
4應該代表的是build版本而不是語言版本,現在是ObjC 2.0

動態 vs 靜態語言

ObjC 是一種面向runtime(運行時)的語言,也就是說,它會儘可能地把代碼執行的決策從編譯和鏈接的時候,推遲到運行時。這給程序員寫代碼帶來很大的靈活性,比如說你可以把消息轉發給你想要的對象,或者隨意交換一個方法的實現之類的。這就要求 runtime 能檢測一個對象是否能對一個方法進行響應,然後再把這個方法分發到對應的對象去。我們拿 C 來跟 ObjC 對比一下。在 C 語言裏面,一切從 main 函數開始,程序員寫代碼的時候是自上而下地,一個 C 的結構體或者說類吧,是不能把方法調用轉發給其他對象的。舉個栗子:

#include < stdio.h >

int main(int argc, const char **argv[])
{
        printf("Hello World!");
        return 0;
} 

這段代碼被編譯器解析,優化後,會變成一堆彙編代碼:

.text
 .align 4,0x90
 .globl _main
_main:
Leh_func_begin1:
 pushq %rbp
Llabel1:
 movq %rsp, %rbp
Llabel2:
 subq $16, %rsp
Llabel3:
 movq %rsi, %rax
 movl %edi, %ecx
 movl %ecx, -8(%rbp)
 movq %rax, -16(%rbp)
 xorb %al, %al
 leaq LC(%rip), %rcx
 movq %rcx, %rdi
 call _printf
 movl $0, -4(%rbp)
 movl -4(%rbp), %eax
 addq $16, %rsp
 popq %rbp
 ret
Leh_func_end1:
 .cstring
LC:
 .asciz "Hello World!"

然後,再鏈接 include 的庫,完了生成可執行代碼。對比一下 ObjC,當我們初學這門語言的時候教程是這麼說滴:用中括號括起來的語句,

[self doSomethingWithVar:var1];

被編譯器編譯之後會變成:

objc_msgSend(self,@selector(doSomethingWithVar:),var1);

一個 C 方法,傳入了三個變量,self指針,要執行的方法 @selector(doSomethingWithVar:) 還有一個參數 var1。但是在這之後就不曉得發生什麼了。

什麼是 Objective-C Runtime?

ObjC Runtime 其實是一個 Runtime 庫,基本上用 C 和彙編寫的,這個庫使得 C 語言有了面向對象的能力(腦中浮現當你喬幫主參觀了施樂帕克的 SmallTalk 之後嘴角一抹淺笑)。這個庫做的事前就是加載類的信息,進行方法的分發和轉發之類的。

Objective-C Runtime 術語

再往下深談之前咱先介紹幾個術語。

  • 2 Runtimes

    目前說來Runtime有兩種,一個 Modern Runtime 和一個 Legacy Runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是可以不用管就是了。

  • 2 Basic types of Methods

    一種 Instance Method,還有 Class Method。instance method 就是帶“-”號的,需要實例化才能用的,如 :

    -(void)doFoo; 
    
    [aObj doFoot];
    

    Class Method 就是帶“+”號的,類似於靜態方法可以直接調用:

    +(id)alloc;
    
    [ClassName alloc];
    

    這些方法跟 C 函數一樣,就是一組代碼,完成一個比較小的任務。

    -(NSString *)movieTitle
    {
        return @"Futurama: Into the Wild Green Yonder";
    }
    
  • Selector

    一個 Selector 事實上是一個 C 的結構體,表示的是一個方法。定義是:

    typedef struct objc_selector  *SEL; 
    

    使用起來就是:

    SEL aSel = @selector(movieTitle); 
    

    這樣可以直接取一個selector,如果是傳遞消息(類似於C的方法調用)就是:

    [target getMovieTitleForObject:obj];
    

    在 ObjC 裏面,用’[]‘括起來的表達式就是一個消息。包括了一個 target,就是要接收消息的對象,一個要被調用的方法還有一些你要傳遞的參數。類似於 C 函數的調用,但是又有所不同。事實上上面這個語句你僅僅是傳遞了 ObjC 消息,並不代表它就會一定被執行。target 這個對象會檢測是誰發起的這個請求,然後決策是要執行這個方法還是其他方法,或者轉發給其他的對象。

  • Class

    Class 的定義是這樣的:

    typedef struct objc_class *Class;
    typedef struct objc_object {
        Class isa;
    } *id; 
    

    我們可以看到這裏這裏有兩個結構體,一個類結構體一個對象結構體。所有的 objc_object 對象結構體都有一個 isa 指針,這個 isa 指向它所屬的類,在運行時就靠這個指針來檢測這個對象是否可以響應一個 selector。完了我們看到最後有一個 id 指針。這個指針其實就只是用來代表一個 ObjC 對象,有點類似於 C++ 的泛型。當你拿到一個 id 指針之後,就可以獲取這個對象的類,並且可以檢測其是否響應一個 selector。這就是對一個 delegate 常用的調用方式啦。這樣說還有點抽象,我們看看 LLVM/Clang 的文檔對 Blocks 的定義:

     struct Block_literal_1 {
        void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
        int flags;
        int reserved; 
        void (*invoke)(void *, ...);
        struct Block_descriptor_1 {
     unsigned long int reserved; // NULL
         unsigned long int size;  // sizeof(struct Block_literal_1)
     // optional helper functions
         void (*copy_helper)(void *dst, void *src);
         void (*dispose_helper)(void *src); 
        } *descriptor;
        // imported variables
    };
    

    可以看到一個 block 是被設計成一個對象的,擁有一個 isa 指針,所以你可以對一個 block 使用 retain, release, copy 這些方法。

  • IMP (Method Implementations)

    接下來看看啥是IMP。

    typedef id (*IMP)(id self,SEL _cmd,...); 
    

    一個 IMP 就是一個函數指針,這是由編譯器生成的,當你發起一個 ObjC 消息之後,最終它會執行的那個代碼,就是由這個函數指針指定的。

  • Objective-C Classes

    OK,回過頭來看看一個 ObjC 的類。舉一個栗子:

    @interface MyClass : NSObject {
    //vars
    NSInteger counter;
    }
    //methods
    -(void)doFoo;
    @end
    

    定義一個類我們可以寫成如上代碼,而在運行時,一個類就不僅僅是上面看到的這些東西了:

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

    可以看到運行時一個類還關聯了它的父類指針,類名,成員變量,方法,cache 還有附屬的 protocol。

那麼類定義了對象並且自己也是個對象?這是咋整滴?

上面我提到過一個 ObjC 類同時也是一個對象,爲了處理類和對象的關係,runtime 庫創建了一種叫做 標籤類 元類(Meta Class)的東西。當你發出一個消息的時候,比方說

[NSObject alloc];

你事實上是把這個消息發給了一個類對象(Class Object),這個類對象必須是一個 Meta Class 的實例,而這個 Meta Class 同時也是一個根 MetaClass 的實例。當你繼承了 NSObject 成爲其子類的時候,你的類指針就會指向 NSObject 爲其父類。但是 Meta Class 不太一樣,所有的 Meta Class 都指向根 Meta Class 爲其父類。一個 Meta Class 持有所有能響應的方法。所以當 [NSObject alloc] 這條消息發出的時候,objc_msgSend() 這個方法會去 NSObject 它的 Meta Class 裏面去查找是否有響應這個 selector 的方法,然後對 NSObject 這個類對象執行方法調用。

爲啥我們要繼承 Apple Classes

初學 Cocoa 開發的時候,多數教程都要我們繼承一個類比方 NSObject,然後我們就開始 Coding 了。比方說:

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

這個語句用來初始化一個實例,類似於 C++ 的 new 關鍵字。這個語句首先會執行 MyObject 這個類的 +alloc 方法,Apple 的官方文檔是這樣說的:

The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.

新建的實例中,isa 成員變量會變初始化成一個數據結構體,用來描述所指向的類。其他的成員變量的內存會被置爲0.

所以繼承 Apple 的類我們不僅是獲得了很多很好用的屬性,而且也繼承了這種內存分配的方法。

那麼啥是 Class Cache(objc_cache *cache)

剛剛我們看到 runtime 裏面有一個指針叫 objc_cache *cache,這是用來緩存方法調用的。現在我們知道一個實例對象被傳遞一個消息的時候,它會根據 isa 指針去查找能夠響應這個消息的對象。但是實際上我們在用的時候,只有一部分方法是常用的,很多方法其實很少用或者根本用不到。比如一個object你可能從來都不用copy方法,那我要是每次調用的時候還去遍歷一遍所有的方法那就太笨了。於是 cache 就應運而生了,每次你調用過一個方法,之後,這個方法就會被存到這個 cache 列表裏面去,下次調用的時候 runtime 會優先去 cache 裏面查找,提高了調用的效率。舉一個栗子:

MyObject *obj = [[MyObject alloc] init]; // MyObject 的父類是 NSObject

@implementation MyObject
-(id)init {
    if(self = [super init]){
        [self setVarA:@”blah”];
    }
    return self;
}
@end

這段代碼是這樣執行的:

  1. [MyObject alloc] 先被執行。但是由於 MyObject 這個類沒有 +alloc 這個方法,於是去父類 NSObject 查找。
  2. 檢測 NSObject 是否響應 +alloc 方法,發現響應,於是檢測 MyObject 類,根據其所需的內存空間大小開始分配內存空間,然後把 isa 指針指向 MyObject 類。那麼 +alloc 就被加進 cache 列表裏面了。
  3. 完了執行 -init 方法,因爲 MyObject 響應該方法,直接加入 cache。
  4. 執行 self = [super init] 語句。這裏直接通過 super 關鍵字調用父類的 init 方法,確保父類初始化成功,然後再執行自己的初始化邏輯。

OK,這就是一個很簡單的初始化過程,在 NSObject 類裏面,alloc 和 init 沒做什麼特別重大的事情,但是,ObjC 特性允許你的 alloc 和 init 返回的值不同,也就是說,你可以在你的 init 函數裏面做一些很複雜的初始化操作,但是返回出去一個簡單的對象,這就隱藏了類的複雜性。再舉個栗子:

#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;
}

如果你是ObjC的初學者,那麼你很可能會認爲這段代碼執的輸出會是:

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

這是因爲 ObjC 是允許運行 +alloc 返回一個特定的類,而 init 方法又返回一個不同的類的。可以看到 NSMutableArray 是對普通數組的封裝,內部實現是複雜的,但是對外隱藏了複雜性。

OK說回 objc_msgSend 這個方法

這個方法做的事情不少,舉個栗子:

[self printMessageWithString:@"Hello World!"];

這句語句被編譯成這樣:

objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");

這個方法先去查找 self 這個對象或者其父類是否響應 @selector(printMessageWithString:),如果從這個類的方法分發表或者 cache 裏面找到了,就調用它對應的函數指針。如果找不到,那就會執行一些其他的東西。步驟如下:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因爲會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 裏面找,完了找得到就跳到對應的函數去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果還找不到就要開始消息轉發邏輯了。

在編譯的時候,你定義的方法比如:

-(int)doComputeWithNum:(int)aNum 

會編譯成:

int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum) 

然後由 runtime 去調用指向你的這個方法的函數指針。那麼之前我們說你發起消息其實不是對方法的直接調用,其實 Cocoa 還是提供了可以直接調用的方法的:

// 首先定義一個 C 語言的函數指針
int (computeNum *)(id,SEL,int);

// 使用 methodForSelector 方法獲取對應與該 selector 的杉樹指針,跟 objc_msgSend 方法拿到的是一樣的
// **methodForSelector 這個方法是 Cocoa 提供的,不是 ObjC runtime 庫提供的**
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];

// 現在可以直接調用該函數了,跟調用 C 函數是一樣的
computeNum(obj,@selector(doComputeWithNum:),aNum); 

如果你需要的話,你可以通過這種方式你來確保這個方法一定會被調用。

消息轉發機制

在 ObjC 這門語言中,發送消息給一個並不響應這個方法的對象,是合法的,應該也是故意這麼設計的。換句話說,我可以對任意一個對象傳遞任意一個消息(看起來有點像對任意一個類調用任意一個方法,當然事實上不是),當然如果最後找不到能調用的方法就會 Crash 掉。

Apple 設計這種機制的原因之一就是——用來模擬多重繼承(ObjC 原生是不支持多重繼承的)。或者你希望把你的複雜設計隱藏起來。這種轉發機制是 Runtime 非常重要的一個特性,大概的步驟如下:

  1. 查找該類及其父類的 cahce 和方法分發表,在找不到的情況下執行2。
  2. 執行 + (BOOL) resolveInstanceMethod:(SEL)aSEL 方法。

    這就給了程序員一次機會,可以告訴 runtime 在找不到改方法的情況下執行什麼方法。舉個栗子,先定義一個函數:

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

    完了重載 resolveInstanceMethod 方法:

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

    其中 “v@:” 表示返回值和參數,這個符號涉及 Type Encoding,可以參考Apple的文檔 ObjC Runtime Guide

  3. 接下來 Runtime 會調用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
    這就給了程序員第二次機會,如果你沒辦法在自己的類裏面找到替代方法,你就重載這個方法,然後把消息轉給其他的Object。

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

    這樣你就可以把消息轉給別人了。當然這裏你不能 return self,不然就死循環了=.=

  4. 最後,Runtime 會調用 – (void)forwardInvocation:(NSInvocation *)anInvocation 這個方法。NSInvocation 其實就是一條消息的封裝。如果你能拿到 NSInvocation,那你就能修改這條消息的 target, selector 和 arguments。舉個栗子:
    -(void)forwardInvocation:(NSInvocation *)invocation
    {
        SEL invSEL = invocation.selector;
    
        if([altObject respondsToSelector:invSEL]) {
            [invocation invokeWithTarget:altObject];
        } else {
            [self doesNotRecognizeSelector:invSEL];
        }
    }
    

    默認情況下 NSObject 對 forwardInvocation 的實現就是簡單地執行 -doesNotRecognizeSelector: 這個方法,所以如果你想真正的在最後關頭去轉發消息你可以重載這個方法(好折騰-.-)。

    原文後面介紹了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鑑於一是底層的可以不用理會,一是早司空見慣的不用詳談,還有一個是很簡單的,就是一個建立在方法分發表裏面填入默認常用的 method,所以有興趣的讀者可以自行查閱原文,這裏就不詳談鳥。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章