初學 Objective-C(如下簡稱ObjC) 的人很容易忽略一個 ObjC 特性 —— ObjC Runtime。這是由於這門語言很容易上手,幾個小時就能學會怎麼使用,因此程序員們每每會把時間都花在瞭解 Cocoa 框架以及調整本身的程序的表現上。然而 Runtime 應該是每個 ObjC 都應該要了解的東西,至少要理解編譯器會把html
[target doMethodWith:var1];
編譯成:程序員
objc_msgSend(target,@selector(doMethodWith:),var1);
這樣的語句。理解 ObjC Runtime 的工做原理,有助於你更深刻地去理解 ObjC 這門語言,理解你的 App 是怎樣跑起來的。我想全部的 Mac/iPhone 開發者,不管水平如何,都會從中獲益的。數組
ObjC Runtime 的代碼是開源的,能夠從這個站點下載: opensource.apple.com。緩存
這個是全部開源代碼的連接: http://www.opensource.apple.com/source/數據結構
這個是ObjC rumtime 的源代碼: http://www.opensource.apple.com/source/objc4/
4應該表明的是build版本而不是語言版本,如今是ObjC 2.0app
ObjC 是一種面向runtime(運行時)的語言,也就是說,它會盡量地把代碼執行的決策從編譯和連接的時候,推遲到運行時。這給程序員寫代碼帶來很大的靈活性,好比說你能夠把消息轉發給你想要的對象,或者隨意交換一個方法的實現之類的。這就要求 runtime 能檢測一個對象是否能對一個方法進行響應,而後再把這個方法分發到對應的對象去。咱們拿 C 來跟 ObjC 對比一下。在 C 語言裏面,一切從 main 函數開始,程序員寫代碼的時候是自上而下地,一個 C 的結構體或者說類吧,是不能把方法調用轉發給其餘對象的。舉個栗子:框架
#include < stdio.h > int main(int argc, const char **argv[]) { printf("Hello World!"); return 0; }
這段代碼被編譯器解析,優化後,會變成一堆彙編代碼:ide
.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。可是在這以後就不曉得發生什麼了。
ObjC Runtime 實際上是一個 Runtime 庫,基本上用 C 和彙編寫的,這個庫使得 C 語言有了面向對象的能力(腦中浮現當你喬幫主參觀了施樂帕克的 SmallTalk 以後嘴角一抹淺笑)。這個庫作的事前就是加載類的信息,進行方法的分發和轉發之類的。
再往下深談以前咱先介紹幾個術語。
目前說來Runtime有兩種,一個 Modern Runtime 和一個 Legacy Runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是能夠不用管就是了。
一種 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 事實上是一個 C 的結構體,表示的是一個方法。定義是:
typedef struct objc_selector *SEL;
使用起來就是:
SEL aSel = @selector(movieTitle);
這樣能夠直接取一個selector,若是是傳遞消息(相似於C的方法調用)就是:
[target getMovieTitleForObject:obj];
在 ObjC 裏面,用’[]‘括起來的表達式就是一個消息。包括了一個 target,就是要接收消息的對象,一個要被調用的方法還有一些你要傳遞的參數。相似於 C 函數的調用,可是又有所不一樣。事實上上面這個語句你僅僅是傳遞了 ObjC 消息,並不表明它就會必定被執行。target 這個對象會檢測是誰發起的這個請求,而後決策是要執行這個方法仍是其餘方法,或者轉發給其餘的對象。
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。
typedef id (*IMP)(id self,SEL _cmd,...);
一個 IMP 就是一個函數指針,這是由編譯器生成的,當你發起一個 ObjC 消息以後,最終它會執行的那個代碼,就是由這個函數指針指定的。
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 這個類對象執行方法調用。
初學 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 的類咱們不只是得到了不少很好用的屬性,並且也繼承了這種內存分配的方法。
剛剛咱們看到 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
這段代碼是這樣執行的:
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 是對普通數組的封裝,內部實現是複雜的,可是對外隱藏了複雜性。
這個方法作的事情很多,舉個栗子:
[self printMessageWithString:@"Hello World!"];
這句語句被編譯成這樣:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
這個方法先去查找 self 這個對象或者其父類是否響應 @selector(printMessageWithString:),若是從這個類的方法分發表或者 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 很是重要的一個特性,大概的步驟以下:
這就給了程序員一次機會,能夠告訴 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,否則就死循環了=.=
-(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,因此有興趣的讀者能夠自行查閱原文,這裏就不詳談鳥。