Why is MetaClass in Objective-C?

這篇文章源于美團面試官問的我一個問題,為什么Objective-C中有Class和MetaClass這種設計?去掉是否可以?當時的我并沒有深入思考過這個問題,而網上搜索的結果都是在闡述有MetaClass而簡略的解釋了原因。我認為這個問題是個很關鍵的問題,花了大概兩周時間查閱資料,查看源碼。這篇文章試圖展開探討一個問題,為什么Objective-C中有MetaClass這個設計?

前置知識

首先簡單分析下在Objective-C中,對象是什么。下面源碼基于Runtime-709分析。

typedef struct objc_object *id;//id其實是一個object結構體的指針,所以id不用加*
typedef struct objc_class *Class;//Class是class結構體的指針

struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;     // 用來緩存指針和虛函數表  
    class_data_bits_t bits;  //方法列表等
    //...
}

可以看到,對象最基本的就是有一個isa指針,指向他的class,而Class本身是繼承自object。isa指針的理解誒就是英文is a,代表“xxx is a (class)”。那么也就是說,一個對象的isa指向哪個class,代表它是那個類的對象。那么對于class來說,它也是一個對象,它的isa指針指向什么呢?

對于Class來說,也就需要一個描述他的類,也就是“類的類”,而meta正是“關于某事自身的某事”的解釋,所以MetaClass就因此而生了。

而從runtime動態生成一個類的Api的方法中,我們也可以發現metaClass的蹤跡。

Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
{
    Class cls, meta;

    rwlock_writer_t lock(runtimeLock);

    // 如果 Class 名字已存在或父類沒有通過認證則創建失敗
    if (getClass(name)  ||  !verifySuperclass(superclass, true/*rootOK*/)) {
        return nil;
    }

    //分配空間
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);

    //構建meta和class的關系
    objc_initializeClassPair_internal(superclass, name, cls, meta);

    return cls;
}

通過這個方法生成后,就成了大家熟悉的那張圖。

v2-ce4b3fa6d104a632f4f34dec0d50f71f_r

從這張圖上,我們可以看到通過這么一層繼承關系,Objective-C的對象原型繼承鏈就完整了。

同時,實例的實例方法函數存在類結構體中,類方法函數存在metaclass結構體中,而Objective-C的方法調用(消息)就會根據對象去找isa指針指向的Class對象中的方法列表找到對應的方法。

Python中的MetaClass

再講Objective-C之前,先講講別的語言的設計,通過各種語言的比較,可以從更廣的層面去理解語言的設計思想。而之所以先講起Python,是因為我在搜索MetaClass時,搜索結果中大部分其實是講Python中MetaClass的。

先看看Python中一個對象結構是怎么樣的,以下源碼基于CPython 3.7.0 alpha 1

//object.h
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;//引用計數
    struct _typeobject *ob_type;//類型
} PyObject;

和Objective-C中類似,ob_type其實就是一個isa指針,代表是什么類型。

而再看看PyTypeObject是怎么樣的。

//object.h
typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
   //....
} PyTypeObject;

#define PyObject_VAR_HEAD      PyVarObject ob_base;

typedef struct {
    PyObject ob_base;  
    Py_ssize_t ob_size; //對象長度
} PyVarObject;

PyVarObject是一種可變長度對象,是在PyObject基礎上加上了對象的長度。而開始的內存包括了ob_base這個PyObject,就代表可以用PyObject指針進行引用。所以可以說,結構體中剛開始的部分是一個PyObject對象,在Python中引用就是一個對象。那么PyTypeObject開頭是一個PyVarObject,也就是一個對象。也就是說,Python里的Class,也是一個對象。

#在python中生成一個Class
MyClass = type('MyClass', (), {})           

先看看Python里面的type關鍵字是什么。

//bltinmodule.c
SETBUILTIN("type",                  &PyType_Type);
//typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    //.....
    type_init,                                  /* tp_init */
    //....                                
    type_new,                                   /* tp_new */
    //....
};

可以發現type關鍵字是PyType_Type的一個引用,而PyType_Type是返回一個PyTypeObject,生成類的對象。而PyVarObject_HEAD_INIT遞歸引用了自己(PyType_Type)作為它的type,所以可以得知type(class) == type 。也就是說,Python中類的isa指針指向type,也就說type其實就是MetaClass,而同時type(type) == type,也就是type的isa指針指向type自身。那么Python的對象鏈就如下圖。

86fbc69def5f2afddd652a5d83e69456_b

而Objective-C不太一樣的是,并不是每一個類都有一個MetaClass,而是所有的類默認都是同一個MetaClass。當然,Python里可以自定義新的MetaClass。

Python中為何要使用元類的原因可能是,Python希望讓使用者對類有著最高的控制權,可以通過對元類的自定義而改變制造類的過程(例如Django里的ORM)。也就是,Python開放了面向對象中類的制造者的權限。而同時,根據StackOverFlow這個問答,Python的類的設計是借鑒于Smalltalk這門語言。

Smalltalk!!Objective-C的特性基本上是照搬的Smalltalk,看來Smalltalk里可以找到一些線索。

Smalltalk-面向對象的前輩

Smalltalk,被公認為歷史上第二個面向對象的語言,其亮點是它的消息發送機制。

Smalltalk中的MetaClass的設計是Smalltalk-80加入的。而之前的Smalltalk-76,并不是每個類有一個MetaClass,而是所有類的isa指針都指向一個特殊的類,叫做Class(這種設計之后也被Java借鑒了)。

而每個類都有自己MetaClass的設計,加入的原因是,因為Smalltalk里面,類是對象,而對象就可以響應消息,那么類的消息的響應的方法就應該由類的類去存儲,而每個MetaClass就持有每個類的類方法。

問題1:每個MetaClass的isa指針指向什么?

如果MetaClass再有MetaClass,那么這個關系將無窮無盡。Smalltalk里的解決方案是,指向同一個叫MetaClass的類。

問題2:MetaClass的isa指針指向什么?

指向他的實例,也就是實例的isa指向MetaClass,同時MetaClassisa指向實例,相互指著。

那么Smalltalk的繼承關系,其實和Objective-C的很像了(后面有class的是前者的MetaClass)。

屏幕快照 2017-09-15 上12.40.10

這時候產生了一個重要的問題,假如去掉MetaClass,把類方法放到也類里面是否可行?

這個問題,我思索許久,發現其實是一個對面向對象的哲學思想問題,要對這個問題下結論,不得不重新講講面向對象。

從Smalltalk重新認識面向對象

以前談到面向對象,總會提到,面向對象三特征:封裝、繼承、多態。但其實,面向對象中也分流派,如C++這種來自Simula的設計思想的,更注重的是類的劃分,因為方法調用是靜態的。而如Objective-C這種借鑒Smalltalk的,更注重的是消息傳遞,是動態響應消息。

而面向對象三種特征,更基于的是類的劃分而提出的。

這兩種思想最大的不同,我認為是自上而下自下而上的思考方式。

  • 類的劃分,要求類的設計者是以一個很高的層次去設計這個類,提取出類的特性和本質,進行類的構建。知道類型才可以去發送消息給對象。
  • 消息傳遞,要求的是類的設計者以消息為起點去構建類,也就是對外界的變化進行響應,而不關心自身的類型,設計接口。嘗試理解消息,無法處理則進行特殊處理。

在此不討論兩種方式的優劣之分,而著重講講Smalltalk這種設計。

消息傳遞對于面向對象的設計,其實在于給出一種對消息的解決方案。而面向對象優點之一的復用,在這種設計里,更多在于復用解決方案,而不是單純的類本身。這種思想就如設計組件一般,關心接口,關心組合而非類本身。其實之所以有MetaClass這種設計,我的理解并不是先有MetaClass,而是在萬物都是對象的Smalltalk里,向對象發送消息的基本解決方案是統一的,希望復用的。而實例和類之間用的這一套通過isa指針指向的Class單例中存儲方法列表和查詢方法的解決方案的流程,是應該在類上復用的,而MetaClass就順理成章出現罷了。

最后

回到一開始那個問題,為什么要設計MetaClass,去掉把類方法放到類里面行不行?

我的理解是,可以,但不Smalltalk。這樣的設計是C++那種自上而下的設計方式,類方法也是類的一種特征描述。而Smalltalk的精髓正在于消息傳遞,復用消息傳遞才是根本目的,而MetaClass只不過是因此需要的一個工具罷了。

PS:筆者這個問題從MetaClass入手去思考,是百思不得其解的。后來看了很多面向對象的東西,才發現這不過是一個產物,而并不是一個重點。

PSS:對于類的實現,Javascript中那種使用Protocol實現的方式也很有意思,受限于篇幅,暫不展開

有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容