iOS面試題:爲什麼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;
}

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

從這張圖上,我們可以看到通過這麼一層繼承關係,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的對象鏈就如下圖。

而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)。

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

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

從Smalltalk重新認識面向對象

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

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

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

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

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

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

最後

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

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

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

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


更多:iOS面試題大全
更多:《BAT面試答案文集.PDF》,獲取可加iOS技術交流圈:937194184

收錄原文地址

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章