這篇文章源于美團(tuán)面試官問的我一個問題,為什么Objective-C中有Class和MetaClass這種設(shè)計?去掉是否可以?當(dāng)時的我并沒有深入思考過這個問題,而網(wǎng)上搜索的結(jié)果都是在闡述有MetaClass而簡略的解釋了原因。我認(rèn)為這個問題是個很關(guān)鍵的問題,花了大概兩周時間查閱資料,查看源碼。這篇文章試圖展開探討一個問題,為什么Objective-C中有MetaClass這個設(shè)計?
前置知識
首先簡單分析下在Objective-C中,對象是什么。下面源碼基于Runtime-709分析。
typedef struct objc_object *id;//id其實是一個object結(jié)構(gòu)體的指針,所以id不用加*
typedef struct objc_class *Class;//Class是class結(jié)構(gòu)體的指針
struct objc_object {
Class isa;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 用來緩存指針和虛函數(shù)表
class_data_bits_t bits; //方法列表等
//...
}
可以看到,對象最基本的就是有一個isa指針,指向他的class,而Class本身是繼承自object。isa指針的理解誒就是英文is a,代表“xxx is a (class)”。那么也就是說,一個對象的isa指向哪個class,代表它是那個類的對象。那么對于class來說,它也是一個對象,它的isa指針指向什么呢?
對于Class來說,也就需要一個描述他的類,也就是“類的類”,而meta正是“關(guān)于某事自身的某事”的解釋,所以MetaClass就因此而生了。
而從runtime動態(tài)生成一個類的Api的方法中,我們也可以發(fā)現(xiàn)metaClass的蹤跡。
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
Class cls, meta;
rwlock_writer_t lock(runtimeLock);
// 如果 Class 名字已存在或父類沒有通過認(rèn)證則創(chuàng)建失敗
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}
//分配空間
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
//構(gòu)建meta和class的關(guān)系
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
通過這個方法生成后,就成了大家熟悉的那張圖。

從這張圖上,我們可以看到通過這么一層繼承關(guān)系,Objective-C的對象原型繼承鏈就完整了。
同時,實例的實例方法函數(shù)存在類結(jié)構(gòu)體中,類方法函數(shù)存在metaclass結(jié)構(gòu)體中,而Objective-C的方法調(diào)用(消息)就會根據(jù)對象去找isa指針指向的Class對象中的方法列表找到對應(yīng)的方法。
Python中的MetaClass
再講Objective-C之前,先講講別的語言的設(shè)計,通過各種語言的比較,可以從更廣的層面去理解語言的設(shè)計思想。而之所以先講起Python,是因為我在搜索MetaClass時,搜索結(jié)果中大部分其實是講Python中MetaClass的。
先看看Python中一個對象結(jié)構(gòu)是怎么樣的,以下源碼基于CPython 3.7.0 alpha 1。
//object.h
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;//引用計數(shù)
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基礎(chǔ)上加上了對象的長度。而開始的內(nèi)存包括了ob_base這個PyObject,就代表可以用PyObject指針進(jìn)行引用。所以可以說,結(jié)構(gòu)體中剛開始的部分是一個PyObject對象,在Python中引用就是一個對象。那么PyTypeObject開頭是一個PyVarObject,也就是一個對象。也就是說,Python里的Class,也是一個對象。
#在python中生成一個Class
MyClass = type('MyClass', (), {})
先看看Python里面的type關(guān)鍵字是什么。
//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 */
//....
};
可以發(fā)現(xiàn)type關(guān)鍵字是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,而是所有的類默認(rèn)都是同一個MetaClass。當(dāng)然,Python里可以自定義新的MetaClass。
Python中為何要使用元類的原因可能是,Python希望讓使用者對類有著最高的控制權(quán),可以通過對元類的自定義而改變制造類的過程(例如Django里的ORM)。也就是,Python開放了面向?qū)ο笾?strong>類的制造者的權(quán)限。而同時,根據(jù)StackOverFlow這個問答,Python的類的設(shè)計是借鑒于Smalltalk這門語言。
Smalltalk!!Objective-C的特性基本上是照搬的Smalltalk,看來Smalltalk里可以找到一些線索。
Smalltalk-面向?qū)ο蟮那拜?/h3>
Smalltalk,被公認(rèn)為歷史上第二個面向?qū)ο蟮恼Z言,其亮點是它的消息發(fā)送機(jī)制。
Smalltalk中的MetaClass的設(shè)計是Smalltalk-80加入的。而之前的Smalltalk-76,并不是每個類有一個MetaClass,而是所有類的isa指針都指向一個特殊的類,叫做Class(這種設(shè)計之后也被Java借鑒了)。
而每個類都有自己MetaClass的設(shè)計,加入的原因是,因為Smalltalk里面,類是對象,而對象就可以響應(yīng)消息,那么類的消息的響應(yīng)的方法就應(yīng)該由類的類去存儲,而每個MetaClass就持有每個類的類方法。
問題1:每個MetaClass的isa指針指向什么?
如果MetaClass再有MetaClass,那么這個關(guān)系將無窮無盡。Smalltalk里的解決方案是,指向同一個叫MetaClass的類。
問題2:MetaClass的isa指針指向什么?
指向他的實例,也就是實例的isa指向MetaClass,同時MetaClassisa指向?qū)嵗?,相互指著?/p>
那么Smalltalk的繼承關(guān)系,其實和Objective-C的很像了(后面有class的是前者的MetaClass)。

這時候產(chǎn)生了一個重要的問題,假如去掉MetaClass,把類方法放到也類里面是否可行?
這個問題,我思索許久,發(fā)現(xiàn)其實是一個對面向?qū)ο蟮恼軐W(xué)思想問題,要對這個問題下結(jié)論,不得不重新講講面向?qū)ο蟆?/p>
從Smalltalk重新認(rèn)識面向?qū)ο?/h3>
以前談到面向?qū)ο螅倳岬?,面向?qū)ο笕卣鳎?strong>封裝、繼承、多態(tài)。但其實,面向?qū)ο笾幸卜至髋?,如C++這種來自Simula的設(shè)計思想的,更注重的是類的劃分,因為方法調(diào)用是靜態(tài)的。而如Objective-C這種借鑒Smalltalk的,更注重的是消息傳遞,是動態(tài)響應(yīng)消息。
而面向?qū)ο笕N特征,更基于的是類的劃分而提出的。
這兩種思想最大的不同,我認(rèn)為是自上而下和自下而上的思考方式。
- 類的劃分,要求類的設(shè)計者是以一個很高的層次去設(shè)計這個類,提取出類的特性和本質(zhì),進(jìn)行類的構(gòu)建。知道類型才可以去發(fā)送消息給對象。
- 消息傳遞,要求的是類的設(shè)計者以消息為起點去構(gòu)建類,也就是對外界的變化進(jìn)行響應(yīng),而不關(guān)心自身的類型,設(shè)計接口。嘗試?yán)斫庀ⅲ瑹o法處理則進(jìn)行特殊處理。
在此不討論兩種方式的優(yōu)劣之分,而著重講講Smalltalk這種設(shè)計。
消息傳遞對于面向?qū)ο蟮脑O(shè)計,其實在于給出一種對消息的解決方案。而面向?qū)ο髢?yōu)點之一的復(fù)用,在這種設(shè)計里,更多在于復(fù)用解決方案,而不是單純的類本身。這種思想就如設(shè)計組件一般,關(guān)心接口,關(guān)心組合而非類本身。其實之所以有MetaClass這種設(shè)計,我的理解并不是先有MetaClass,而是在萬物都是對象的Smalltalk里,向?qū)ο蟀l(fā)送消息的基本解決方案是統(tǒng)一的,希望復(fù)用的。而實例和類之間用的這一套通過isa指針指向的Class單例中存儲方法列表和查詢方法的解決方案的流程,是應(yīng)該在類上復(fù)用的,而MetaClass就順理成章出現(xiàn)罷了。
最后
回到一開始那個問題,為什么要設(shè)計MetaClass,去掉把類方法放到類里面行不行?
我的理解是,可以,但不Smalltalk。這樣的設(shè)計是C++那種自上而下的設(shè)計方式,類方法也是類的一種特征描述。而Smalltalk的精髓正在于消息傳遞,復(fù)用消息傳遞才是根本目的,而MetaClass只不過是因此需要的一個工具罷了。
PS:筆者這個問題從MetaClass入手去思考,是百思不得其解的。后來看了很多面向?qū)ο蟮臇|西,才發(fā)現(xiàn)這不過是一個產(chǎn)物,而并不是一個重點。
PSS:對于類的實現(xiàn),Javascript中那種使用Protocol實現(xiàn)的方式也很有意思,受限于篇幅,暫不展開
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz