Object, Class and Meta Class in Objective-C

寫了一陣子的 Objective-C/iOS app,這次讓我們回頭來看點基礎的東西 :)

什麼是 id?

在 Objective-C 裡,變數宣告通常得告知編譯器這個變數的型別,例如這樣:

NSArray* myArray = @[@"eddie", @"kao"];

而在 Objective-C 裡有個特別的型別叫做 id,它可以指向任何物件,像這樣:

id myObject = @[@"eddie", @"kao"];

那這個 id 是什麼? 我們直接來從原始程式碼來找這個 id 的定義。大家可以打開 XCode,然後選擇「File」→「Open Quickly..」,應該可以找到 objc.h 這個檔案,往下捲一點,應該可以找到像下面的這幾行程式碼:

/// 檔案名稱:objc.h
/// A pointer to an instance of a class.
typedef struct objc_object *id;

就照上面這段註解說的,所謂的 id 就是一個「指向一個實例 (instance) 的指針」,而且 id 這個 Struct 的定義本身就有帶一個 *,這也是為什麼你在宣告其它一般物件型別的變數需要加註一個 * 來表示這是一個指標變數,而使用 id 卻不需要的原因。

什麼是一個物件(Object)?

讓我們再從剛剛那個檔案,順蔓摸瓜的繼續看下去..

/// 檔案名稱:objc.h
/// Represents an instance of a class.
struct objc_object {
    Class isa;
};

從這段程式碼可以知道,其實所謂的「物件」,說穿了就只是一個 C 的結構(C Struct),而在這個 Struct 裡面只有一個 isa 的指針,指向自己所屬的類別。

再來我們來看看在 Objective-C 裡,到底什麼是一個類別(Class)

什麼是類別(Class)?

繼續來看剛剛的那個檔案:

/// 檔案:objc.h
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

看得出來其實 Class 本身也是一個 C Struct,再往下看一下這個 objc_class 的定義 (runtime.h):

/// 檔案:runtime.h
struct objc_class {
    Class isa;

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

從上面這段程式碼可以看到,所謂的類別也是一個 C Struct,裡面第一個指針 isa 指向它的所屬類別,第二個指針 super_class 則是指向它的父類別。除此之外還有一些其它的指針,指向例如 instance variables、method list、cache、protocol 等 Struct。

訊息傳遞(Message Passing)

大家在其它 Objective-C 的書上應該常看到,在 Objective-C 的世界裡,方法不像其它程式語言一樣的被直接呼叫,而是透過「訊息傳遞(Message Passing)」方式,像是這樣:

[someObject saySomething:@"hello"]

這裡的 someObject 稱作 receiver,就是接收「訊息」的傢伙,而 saySomething: 就是「訊息」,@"hello" 則是參數。

事實上,在訊息傳遞的過程,當 someObject 這傢伙收到訊息之後,會順著這個物件的 isa 指針找到自己的類別,然後再依照收到的「訊息」去類別的 method list 找出對應的方法。如果在這個類別裡面找到了,就會直接執行它,如果找不到,會再往上一層類別(super class)找。以上這個流程都是在程式執行過程中(Rumtime)才動態決定的,而不是在編譯(Compile)時期就決定好的。

而這個訊息傳遞的過程畢竟是多做了幾件事,相對感覺會比較耗時,但實際上在程式的執行過程中,一但執行過的方法就會被暫存(cache)下來,下次再收到一樣的訊息的時候就會快得多了。

其實,在 Objective-C 的世界裡,類別本身也是物件,所以你也可以對它「發送訊息」,像是這樣:

[NSArray arrayWithObjects:@"eddie", @"kao", nil];

看起來就跟在其它程式語言的「類別方法」差不多,但事實上它就是對 NSArray 這個類別發送了 arrayWithObjects: 這個訊息。

接下來我們直接寫一小段程式會比較容易想像:

// Animal Class
@interface Animal : NSObject
@end

@implementation Animal
@end

// Fox Class
@interface Fox : Animal
- (void) say;
@end

@implementation Fox
- (void) say
{
    NSLog(@"What does the fox say!?");
}
@end

上面這段程式碼建立了兩個類別,分別是 AnimalFox ,而且 Fox 繼承自 Animal,而 Animal 類別則是繼承自 NSObject 類別。

不管是「物件」或是「類別」,因為一樣都是物件,所以他們在收到訊息時的反應流程也是一樣的:

什麼是 Meta Class?

Meta 這個字我不太懂中文該怎麼翻譯比較好,有人翻譯成「元」,有人翻譯成「後設」,但我個人還是喜歡直接使用 Meta 這個詞就好。

剛剛在看類別的 objc_class 的定義的時候有提到類別的 Struct 裡也有一個 isa 指針,指向它所屬的類別,你可以想像成是它是「類別的類別」,也就是所謂的 Meta Class

而 Meta Class 其實也是一種類別,所以也跟一般的類別一樣有 isasuper_class 指針... 所以就可以把這整個的關係用一張圖表來解釋:

Object Model

看圖說故事:

  1. 每個 Class (Fox, Animal, NSObject) 的 isa 指針都指向一個唯一的 Class,這個 Class 稱之為 Meta Class
  2. 每個 Meta Class 的 isa 指針都是指向最上層的 Meta Class (在我們上面那段範例裡,就是 NSObject 的 Meta Class),而最上層的 Meta Class 的 isa 則是指向自己,形成一個迴路。
  3. 每個 Meta Class 的 super_class 指針都是指向它原本類別的 Super Class 的 Meta Class,但最上層的 Meta Class 的 super_class 則是指向 NSObject 類別本身。
  4. 最上層的 Class (NSObject),它的 Super Class 指向 nil

PS:在 Objective-C 裡有兩種 Root Class(NSObject 跟 NSProxy),但因為在 Objective-C 裡「大部份」的類別都是 NSObject 的子類別,所以舉例常會說最上層的類別就是 NSObject

一堆 Class、Super Class、Meta Class 的,有種快打結的感覺了嗎? 其實只要理解上面那張圖片,基本上就不用記這些繞舌的規則了。

但口說無憑,讓我們來寫幾行程式碼驗證一下:

Fox* lucky = [[Fox alloc] init];
[lucky say];

// about fox instance
NSLog(@"the class of lucky is %@, address = %p", [lucky class], [lucky class]);

// about Fox class
NSLog(@"Fox = %@, address = %p", [Fox class], [Fox class]);
NSLog(@"Fox's Super Class = %@, address = %p", [Fox superclass], [Fox superclass]);
Class metaClassOfFox = object_getClass([Fox class]);
NSLog(@"Fox's Meta Class = %p", metaClassOfFox);
NSLog(@"Fox's Meta Class's Super Class = %p", [metaClassOfFox superclass]);
Class metaMetaClassOfFox = object_getClass(metaClassOfFox);
NSLog(@"Fox's Meta Class's Meta Class = %p", metaMetaClassOfFox);

// about Animal class
NSLog(@"Animal = %@, address = %p", [Animal class], [Animal class]);
NSLog(@"Animal's Super Class = %@, address = %p", [Animal superclass], [Animal superclass]);
Class metaClassOfAnimal = object_getClass([Animal class]);
NSLog(@"Animal's Meta Class = %p", metaClassOfAnimal);
NSLog(@"Animal's Meta Class's Super Class = %p", [metaClassOfAnimal superclass]);
Class metaMetaClassOfAnimal = object_getClass(metaClassOfAnimal);
NSLog(@"Animal's Meta Class's Meta Class = %p", metaMetaClassOfAnimal);

// about NSObject class
NSLog(@"NSObject = %@, address = %p", [NSObject class], [NSObject class]);
NSLog(@"NSObject's Super Class = %@, address = %p", [NSObject superclass], [NSObject superclass]);
Class metaClassOfNSObject = object_getClass([NSObject class]);
NSLog(@"NSObject's Meta Class = %p", metaClassOfNSObject);
NSLog(@"NSObject's Meta Class's Super Class = %p", [metaClassOfNSObject superclass]);
Class metaMetaClassOfNSObject = object_getClass(metaClassOfNSObject);
NSLog(@"NSObject's Meta Class's Meta Class = %p", metaMetaClassOfNSObject);

輸出結果如下:

What does the fox say!?

the class of lucky is Fox, address = 0x100002260

Fox = Fox, address = 0x100002260
Fox's Super Class = Animal, address = 0x100002210
Fox's Meta Class = 0x100002238
Fox's Meta Class's Super Class = 0x1000021e8
Fox's Meta Class's Meta Class = 0x7fff77cce838

Animal = Animal, address = 0x100002210
Animal's Super Class = NSObject, address = 0x7fff77cce810
Animal's Meta Class = 0x1000021e8
Animal's Meta Class's Super Class = 0x7fff77cce838
Animal's Meta Class's Meta Class = 0x7fff77cce838

NSObject = NSObject, address = 0x7fff77cce810
NSObject's Super Class = (null), address = 0x0
NSObject's Meta Class = 0x7fff77cce838
NSObject's Meta Class's Super Class = 0x7fff77cce810
NSObject's Meta Class's Meta Class = 0x7fff77cce838

輸出結果可能在每個人的電腦上都不一樣,但從輸出的結果就能證明上面那張圖片的每個類別之間的關係。

要特別注意的是,你可能會覺得直接對類別發送 class 訊息就可以得到該類別的 Meta Class,事實上直接對類別發送 class 訊息只會得到該類別自己。要取得類別的 Meta Class,可以透過 object_getClass 來取得。

以上,希望這篇文章能讓大家對 Objective-C 基本的物件跟類別有更進一步的了解。

個人認為,雖然了解這些知識不見得對 iOS app 的開發有直接的幫助,但至少當在寫一個類別或使用一個物件的時候,會更清楚到底是怎麼一回事。如果有哪邊寫錯,還請前輩先進不吝指點。