在本文我們會看到一個在Objective-C中很陌生的概念——元類。Objective-C中的每個類都有和自己相關聯的元類,但我們幾乎從來不直接使用它,它們依然是那麼神祕。我們將開始學習怎樣在執行時建立一個類。通過建立的“class pair”,我會解釋什麼是元類,然後探討它對於Objective-C中物件和類的意義。
在執行時建立一個類
下面的程式碼在執行時建立了一個NSError的子類,並且新增了一個方法:
1 2 3 4 |
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0); class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:"); objc_registerClassPair(newClass); |
ReportFunction函式就是新增的例項方法,具體實現如下
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void ReportFunction(id self, SEL _cmd) { NSLog(@"This object is %p.", self); NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]); Class currentClass = [self class]; for (int i = 1; i < 5; i++) { NSLog(@"Following the isa pointer %d times gives %p", i, currentClass); currentClass = object_getClass(currentClass); } NSLog(@"NSObject's class is %p", [NSObject class]); NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class])); } |
表面上看來,這相當簡單。在執行時建立一個類只需要3個步驟:o
- 為”class pair”分配記憶體 (使用
objc_allocateClassPair
). - 新增方法或成員變數到有需要的類裡 (我已經使用
class_addMethod新增了一個方法
). - 註冊類以便它能使用 (使用
objc_registerClassPair
).
然而,有一個很迫切的問題:什麼是“class pair”?objc_allocateClassPair函式僅返回了一個值:the class。那另一半pair在哪?
我相信你已經猜到了,另一半pair就是元類(這篇文章的主題)。為了解釋它是什麼和我們為什麼需要它,還需要交代下Objective-C的物件和類的相關背景。
什麼資料結構才能稱之為物件?
每個物件都有類。這是物件導向的基本概念,但是在Objective-C中,它對資料結構也一樣。含有一個指標且該指標可以正確指向類的資料結構,都可以被視作為物件。
在Objective-C中,物件的類是isa指標決定的。isa指標指向物件所屬的類。
實際上,
Objective-C中物件最基本的定義是這樣的:
1 2 3 |
typedef struct objc_object { Class isa; } *id; |
這說的是:任何帶有以指標開始並指向類結構的結構都可以被視作objc_object。
Objective-C中物件最重要的特點是你可以傳送訊息給它們:
1 2 |
[@"stringValue" writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL]; |
這能工作是因為Objective-C物件(這兒是NSCFString)在傳送訊息時,執行時庫會追尋著物件的isa指標得到了物件所屬的類(這兒是NSCFString類)。這個類包含了能應用於這個類的所有例項方法和指向超類的指標以便可以找到父類的例項方法。執行時庫檢查這個類和其超類的方法列表,找到一個匹配這條訊息的方法(在上面的程式碼裡,是NSString類的writeToFile:atomically:encoding:error方法
)。執行時庫基於那個方法呼叫函式(IMP)。
重點就是類要定義這個你傳送給物件的訊息。
什麼是元類
現在,可能你已經知道了,Objective-C的一個類也是一個物件。這意味著你可以傳送訊息給一個類。
1 |
NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding]; |
在這個示例裡,defaultStringEncoding被髮送給了
NSString類。
因為
Objective-C中每個類本身也是一個物件。如上面所展示的,這意味著類結構必須以一個isa指標開始,從而可以和objc_object在二進位制層面相容,然後這個結構的下一欄位必須是一個指向超類的指標(對於基類則為nil)。
正如我上週展示的,類被定義的方式有點不同,依賴於你的執行時庫版本,但是,它們都以isa欄位開始,隨後是superclass欄位。
1 2 3 4 5 6 |
typedef struct objc_class *Class; struct objc_class { Class isa; Class super_class; /* followed by runtime specific details... */ }; |
為了呼叫類裡的方法,類的isa指標必須指向包含這些類方法的類結構體。
這就引出了元類的定義:元類是類物件的類。
簡單說就是:
- 當你給物件傳送訊息時,訊息是在尋找這個物件的類的方法列表。
- 當你給類發訊息時,訊息是在尋找這個類的元類的方法列表。
元類是必不可少的,因為它儲存了類的類方法。每個類都必須有獨一無二的元類,因為每個類都有獨一無二的類方法。
元類的類是什麼?
元類,就像之前的類一樣,它也是一個物件。你也可以呼叫它的方法。自然的,這就意味著他必須也有一個類。
所有的元類都使用根元類(繼承體系中處於頂端的類的元類)作為他們的類。這就意味著所有NSObject的子類(大多數類)的元類都會以NSObject的元類作為他們的類
根據這個規則,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說基類的元類的isa指標指向他自己。
類和元類的繼承
類用 super_class指標指向了超類,同樣的,元類用
super_class指向類的super_class的元類。
說的更拗口一點就是,根元類把它自己的基類設定成了
super_class。
在這樣的繼承體系下,所有例項、類以及元類(meta class)都繼承自一個基類。
這意味著對於繼承於NSObject的所有例項、類和元類,他們可以使用NSObject的所有例項方法,類和元類可以使用NSObject的所有類方法
這些文字看起來莫名其妙難以理解。Greg Parker給出了一份精彩的圖譜來展示這些關係:
實驗證明
為了驗證,讓我們看看我在文章開始寫的ReportFunction
函式的輸出。這個函式的目的是跟隨isa指標並列印出它的路途。
為了執行ReportFunction,我們需要建立一個動態例項來建立類呼叫report
方法。
1 2 3 4 |
id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil]; [instanceOfNewClass performSelector:@selector(report)]; [instanceOfNewClass release]; |
這裡沒有宣告report方法,但我使用
performSelector:呼叫它,所以編譯器不會給出警告。
然後ReportFunction函式會沿著isa進行檢索,來告訴我們class,meta-class以及meta-class的class是什麼樣的情況:
得到物件的類:ReportFunction
函式使用object_getClass跟蹤isa指標,因為isa指標是類的保護成員(你不能直接接收其他物件的isa指標)。ReportFunction不使用類方法,因為在類物件裡呼叫類方法不能返回元類,它會再次返回這個類(因此
[NSString class]會返回NSString
類而不是NSString
元類)
This is the output (minus NSLog
prefixes) when the program runs:
這是程式執行時的輸出(省略了NSlog字首):
1 2 3 4 5 6 7 8 |
This object is 0x10010c810. Class is RuntimeErrorSubclass, and super is NSError. Following the isa pointer 1 times gives 0x10010c600 Following the isa pointer 2 times gives 0x10010c630 Following the isa pointer 3 times gives 0x7fff71038480 Following the isa pointer 4 times gives 0x7fff71038480 NSObject's class is 0x7fff710384a8 NSObject's meta class is 0x7fff71038480 |
觀察isa到達過的地址的值:
- 物件的地址是
0x10010c810
. - 類的地址是
0x10010c600
. 元類的地址是
0x10010c630
.- 根元類(NSObject的元類)的地址是
0x7fff71038480
. - NSObject元類的類是它本身.
這些地址的值並不重要,重要的是它們說明了文中討論的從類到meta-class到NSObject的meta-class的整個流程。
最後
元類是 Class 物件的類。每個類(Class)都有自己獨一無二的元類(每個類都有自己第一無二的方法列表)。這意味著所有的類物件都不同。
元類總是會確保類物件和基類的所有例項和類方法。對於從NSObject繼承下來的類,這意味著所有的NSObject例項和
protocol方法在所有的類(和meta-class)中都可以使用。
所有的meta-class使用基類的meta-class作為自己的基類,對於頂層基類的meta-class也是一樣,只是它指向自己而已。