Objective-C 中的 Meta-class 是什麼?

cocoabit發表於2013-12-24

  在這篇文章中,我關注的是 Objective-C 中的一個陌生的概念—— meta-class。在 Objective-C 中的每個類都有一個相關聯的 meta-class,但是你很少會直接使用 meta-class,他們仍舊保持著神祕的面紗。我們從在執行時建立一個類開始。通過檢視 “class pair”,我會解釋 meta-class 是什麼,同時也會談談在 Objective-C 中的物件或者類相關的一些一般主題。

 在執行時建立一個類

  下面的程式碼在執行時建立了一個 NSError 的子類同時為它新增了一個方法:

Class newClass =
    objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

  新增的方法使用叫 ReportFunction 的函式作為實現,定義如下:

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]));
}

  表面上來看,非常簡單。在執行時建立一個類只需要這三步:

  1、為 “class pair” 建立儲存空間(使用 objc_allocateClassPair)。

  2、為這個類新增所需的 methods 和 ivars(我已經使用 class_addMethod 新增過一個方法了)。

  3、註冊這個類,然後就可以使用了(使用 objc_registerClassPair)。

  然後,中級問題是:“class pair” 是什麼?函式 objc_allocateClassPair 只返回了一個值:這個 class。這一對中的另一個在哪?(譯註:pair 有 “一對,一雙” 的意思)

  我敢肯定你已經猜到了另一半就是 meta-class(就是這篇文章的標題),但是要解釋那是什麼和你為什麼需要它,我需要介紹一些在 Objective-C 中的關於物件和類的背景知識。

 把一個資料結構變為物件需要什麼?

  每個物件都有一個類。這是面相物件概念的基礎知識,但在 Objective-C 中不是這樣,它(譯註:class)同樣是這個資料的一部分。每個可以被當成物件的資料結構都在恰當的位置有一個指向一個類的指標。

  在 Objective-C,一個物件的類由它的 isa 指標決定。isa 指標指向這個物件的 Class。

  事實上,在 Objective-C 中的物件的定義看起來像這樣:

typedef struct objc_object {
    Class isa;
} *id;

  這就是說:任何結構體只要以一個指向 Class 結構的指標開始的就可以被當成是 objc_object。

  在 Objective-C 中的物件的一個重要的特性是,你可以向它們傳送訊息:

[@"stringValue"
    writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

  你可以這麼做是因為,當你向一個 Objective-C 的物件(像這裡的 NSCFString)傳送訊息的時候,runtime 沿著物件的 isa 指標找到了這個物件的 Class(這裡是 NSCFString 的類)結構體。 Class 結構體中包含了一個這個類的方法列表和一個指向父類的指標,用於查詢繼承的方法。

  關鍵點是 Class 結構體中定義了你可以向一個物件傳送的訊息。

 meta-class 是什麼?

  現在,你可能已經知道,在 Objective-C 中一個 Class 也是一個物件。這就意味著你也可以向一個 Class 傳送訊息。

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

  這裡,向 NSString 類傳送了 defaultStringEncoding。

  可以這麼做是因為在 Objective-C 中每個 Class 它自己同樣也是個物件。也就是說 Class 結構體必須以 isa 指標開始,然後就可以在二進位制相容(binary compatible)我上面介紹的 objc_object 結構了,接著下一個欄位必須是一個指向它的父類的指標(要是類就是基類就是 nil)。

  我上週已經介紹過,定義一個類有好幾種方法,主要依賴於你正在執行的 runtime 的版本。但,是的,都是由一個 isa 欄位開始然後是 superclass 欄位。

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    /* 以下依賴於 runtime 的具體實現 …… */
};

  然而,為了讓我們在 Class 上呼叫一個方法,Class 的 isa 指標必須指向一個 Class 結構體,並且那個 Class 結構體必須包含我們可以在那個 Class 上呼叫的方法的列表。

  這就引出了 meta-class 的定義:meta-class 是 Class 物件的類(the meta-class is the class for a Class object)。

  簡單來說:

  • 當你向一個物件傳送訊息,就在那個物件的方法列表中查詢那個訊息。
  • 當你想一個類傳送訊息,就再那個類的 meta-class 中查詢那個訊息。

  meta-class 是必須的,因為它為一個 Class 儲存類方法。每個類都必須有一個唯一的 meta-class,因為每個 Class 都有一個可能不一樣的類方法。

 meta-class 的類是什麼?

   meta-class,如之前的 Class,同樣是個物件。這就意味著你也可以在它上面呼叫方法。自然的,這就意味著它也必須有一個類(譯註:isa 指標)。

  所有的 meta-class 使用它們基類的 meta-class (繼承層次中最頂層的 Class 的 meta-class)作為它們自己的類。這就是說所有繼承自 NSObject 的類(大部分的類),以 NSObject 的 meta-class 作為自己的 meta-class 的類。

  遵循這個規則,所有的 meta-class 使用基類的 meta-class 作為他們的類,任何基類的 meta-class 將會是他們自己(它們的 isa 指向他們自己)。這就是說 NSObject 的 meta-class 的 isa 指標指向它們自己(是自己的一個例項)。

 class 和 meta-class 的繼承

  和 Class 以 super_class 指標指向它的父類的方法一樣,meta-class 以 super_class 指標指向 Class 的 super_class 的 meta-class。(譯註:這句話有點繞,就是 super-class 一個指向 Class 的父類,一個指向 meta-class 的父類。Class 是一般物件的型別,meta-class 是 Class 的型別。)

  進一步來講,基類的 meta-class 設定 super_class 指標指向基類自己。

  這個繼承層次的結果就是,所有在這個繼承層次中的的例項,類和 meta-class 都繼承了基類的層次。

  對於所有在 NSObject 層次中的例項,類和 meta-class,這就意味著所有 NSObject 的例項方法都是有效的。對於類和 meta-class,所有 NSObject 的類方法也同樣是有效的。

  所有這些在字面上相當讓人困惑。Greg Parker 已經把例項,類,meta-class 還有他們的超類以非常棒的圖解的方式聚合在一起,展示他們是如何在一起工作的。

 用實驗驗證這點

  為了驗證這些,讓我們看看在我文章開頭提供的 ReportFunction 的輸出。這個函式的目的是順著 isa 指標打引出它找到的。

  要執行 ReportFunction,我們需要為這個動態建立的類建立一個例項,然後在上面呼叫這個方法。

id instanceOfNewClass =
    [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

  因為沒有這個方法的宣告,所以我使用 performSelector: 呼叫這個方法,這樣編譯器就不會輸出警告了。

  現在 ReportFunction 會沿著 isa 指標告訴我們這個物件使用了哪些類,meta-class 和 meta-class 的類。

  獲取一個物件的類:ReportFunction 使用 object_getClass 跟隨 isa 指標,因為 isa 指標是一個類中一個受保護的成員變數(你不能直接訪問其他物件的 isa 指標)。ReportFunction 沒有以類方法的形式這樣呼叫,因為在 Class 物件上呼叫類方法不會返回 meta-class,而是再次返回 Class 物件(所以 [NSString class] 會返回 NSString 的類而不是 NSString 的 meta-class)。

  這個是程式執行後的結果(省去了 NSLog 的字首)。

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 的地址:

  • the object is address 0x10010c810.
  • the class is address 0x10010c600.
  • the meta-class is address 0x10010c630.
  • the meta-class’s class (i.e. the NSObject meta-class) is address 0x7fff71038480.
  • the NSObject meta-class’ class is itself.

  地址的值不是很重要,只是演示了上面討論的從類到 meta-class 到 NSObject 的 meta-class 的過程。

 結論

  meta-class 是 Class 物件的類。每個 Class 都有個不同的自己的 meta-class(因此每個 Class 都可以有一個自己不同的方法列表)。也就是說每個類的 Class 不完全相同。

  meta-class 總是會保證 Class 物件會有從基類繼承的所有的的例項和類方法,加上之後繼承的類方法。如從 NSObject 繼承的類,就意味著在所有的 Class(和 meta-class)物件中定義了所有從 NSObject 繼承的例項和協議方法。

  所有的 meta-class 使用基類的 meta-class(NSObject 的 meta-class 用於繼承自 NSObject 的類)作為他們自己的類,包括在執行時自己定義的基礎的 meta-class。

  原文出處: cocoawithlove

相關文章