初學 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
這段代碼是這樣執行的:
- [MyObject alloc] 先被執行。但是由於 MyObject 這個類沒有 +alloc 這個方法,於是去父類 NSObject 查找。
- 檢測 NSObject 是否響應 +alloc 方法,發現響應,於是檢測 MyObject 類,根據其所需的內存空間大小開始分配內存空間,然後把 isa 指針指向 MyObject 類。那麼 +alloc 就被加進 cache 列表裏面了。
- 完了執行 -init 方法,因爲 MyObject 響應該方法,直接加入 cache。
- 執行 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 裏面找到了,就調用它對應的函數指針。如果找不到,那就會執行一些其他的東西。步驟如下:
- 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
- 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因爲會被忽略掉。
- 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 裏面找,完了找得到就跳到對應的函數去執行。
- 如果 cache 找不到就找一下方法分發表。
- 如果還找不到就要開始消息轉發邏輯了。
在編譯的時候,你定義的方法比如:
-(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 非常重要的一個特性,大概的步驟如下:
- 查找該類及其父類的 cahce 和方法分發表,在找不到的情況下執行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。
-
接下來 Runtime 會調用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
這就給了程序員第二次機會,如果你沒辦法在自己的類裏面找到替代方法,你就重載這個方法,然後把消息轉給其他的Object。- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
這樣你就可以把消息轉給別人了。當然這裏你不能 return self,不然就死循環了=.=
- 最後,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,所以有興趣的讀者可以自行查閱原文,這裏就不詳談鳥。