這篇文章源于美團(tuán)面試官問(wèn)的我一個(gè)問(wèn)題蚁孔,為什么Objective-C中有Class和MetaClass這種設(shè)計(jì)奶赔?去掉是否可以?當(dāng)時(shí)的我并沒(méi)有深入思考過(guò)這個(gè)問(wèn)題杠氢,而網(wǎng)上搜索的結(jié)果都是在闡述有MetaClass而簡(jiǎn)略的解釋了原因站刑。我認(rèn)為這個(gè)問(wèn)題是個(gè)很關(guān)鍵的問(wèn)題,花了大概兩周時(shí)間查閱資料鼻百,查看源碼绞旅。這篇文章試圖展開(kāi)探討一個(gè)問(wèn)題,為什么Objective-C中有MetaClass這個(gè)設(shè)計(jì)愕宋?
前置知識(shí)
首先簡(jiǎn)單分析下在Objective-C中玻靡,對(duì)象是什么结榄。下面源碼基于Runtime-709分析中贝。
typedef struct objc_object *id;//id其實(shí)是一個(gè)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; // 用來(lái)緩存指針和虛函數(shù)表
class_data_bits_t bits; //方法列表等
//...
}
可以看到臼朗,對(duì)象最基本的就是有一個(gè)isa指針邻寿,指向他的class,而Class本身是繼承自object视哑。isa指針的理解誒就是英文is a绣否,代表“xxx is a (class)”。那么也就是說(shuō)挡毅,一個(gè)對(duì)象的isa指向哪個(gè)class蒜撮,代表它是那個(gè)類的對(duì)象。那么對(duì)于class來(lái)說(shuō)跪呈,它也是一個(gè)對(duì)象段磨,它的isa指針指向什么呢?
對(duì)于Class來(lái)說(shuō)耗绿,也就需要一個(gè)描述他的類苹支,也就是“類的類”,而meta正是“關(guān)于某事自身的某事”的解釋误阻,所以MetaClass就因此而生了债蜜。
而從runtime動(dòng)態(tài)生成一個(gè)類的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 名字已存在或父類沒(méi)有通過(guò)認(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ò)這個(gè)方法生成后寻定,就成了大家熟悉的那張圖儒洛。
從這張圖上,我們可以看到通過(guò)這么一層繼承關(guān)系狼速,Objective-C的對(duì)象原型繼承鏈就完整了晶丘。
同時(shí),實(shí)例的實(shí)例方法函數(shù)存在類結(jié)構(gòu)體中唐含,類方法函數(shù)存在metaclass結(jié)構(gòu)體中浅浮,而Objective-C的方法調(diào)用(消息)就會(huì)根據(jù)對(duì)象去找isa指針指向的Class對(duì)象中的方法列表找到對(duì)應(yīng)的方法。
Python中的MetaClass
再講Objective-C之前捷枯,先講講別的語(yǔ)言的設(shè)計(jì)滚秩,通過(guò)各種語(yǔ)言的比較,可以從更廣的層面去理解語(yǔ)言的設(shè)計(jì)思想淮捆。而之所以先講起Python郁油,是因?yàn)槲以谒阉鱉etaClass時(shí),搜索結(jié)果中大部分其實(shí)是講Python中MetaClass的攀痊。
先看看Python中一個(gè)對(duì)象結(jié)構(gòu)是怎么樣的桐腌,以下源碼基于CPython 3.7.0 alpha 1。
//object.h
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;//引用計(jì)數(shù)
struct _typeobject *ob_type;//類型
} PyObject;
和Objective-C中類似苟径,ob_type其實(shí)就是一個(gè)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; //對(duì)象長(zhǎng)度
} PyVarObject;
PyVarObject是一種可變長(zhǎng)度對(duì)象蟆盐,是在PyObject基礎(chǔ)上加上了對(duì)象的長(zhǎng)度。而開(kāi)始的內(nèi)存包括了ob_base這個(gè)PyObject遭殉,就代表可以用PyObject指針進(jìn)行引用石挂。所以可以說(shuō),結(jié)構(gòu)體中剛開(kāi)始的部分是一個(gè)PyObject對(duì)象险污,在Python中引用就是一個(gè)對(duì)象痹愚。那么PyTypeObject開(kāi)頭是一個(gè)PyVarObject,也就是一個(gè)對(duì)象蛔糯。也就是說(shuō)拯腮,Python里的Class,也是一個(gè)對(duì)象渤闷。
#在python中生成一個(gè)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的一個(gè)引用,而PyType_Type是返回一個(gè)PyTypeObject飒箭,生成類的對(duì)象狼电。而PyVarObject_HEAD_INIT遞歸引用了自己(PyType_Type)作為它的type蜒灰,所以可以得知type(class) == type
。也就是說(shuō)肩碟,Python中類的isa指針指向type强窖,也就說(shuō)type其實(shí)就是MetaClass,而同時(shí)type(type) == type
削祈,也就是type的isa指針指向type自身翅溺。那么Python的對(duì)象鏈就如下圖。
而Objective-C不太一樣的是髓抑,并不是每一個(gè)類都有一個(gè)MetaClass咙崎,而是所有的類默認(rèn)都是同一個(gè)MetaClass。當(dāng)然吨拍,Python里可以自定義新的MetaClass褪猛。
Python中為何要使用元類的原因可能是,Python希望讓使用者對(duì)類有著最高的控制權(quán)羹饰,可以通過(guò)對(duì)元類的自定義而改變制造類的過(guò)程(例如Django里的ORM)伊滋。也就是,Python開(kāi)放了面向?qū)ο笾?strong>類的制造者的權(quán)限队秩。而同時(shí)笑旺,根據(jù)StackOverFlow這個(gè)問(wèn)答,Python的類的設(shè)計(jì)是借鑒于Smalltalk這門語(yǔ)言馍资。
SmalltalkM仓鳌!Objective-C的特性基本上是照搬的Smalltalk迷帜,看來(lái)Smalltalk里可以找到一些線索物舒。
Smalltalk-面向?qū)ο蟮那拜?/h3>
Smalltalk色洞,被公認(rèn)為歷史上第二個(gè)面向?qū)ο蟮恼Z(yǔ)言戏锹,其亮點(diǎn)是它的消息發(fā)送機(jī)制。
Smalltalk中的MetaClass的設(shè)計(jì)是Smalltalk-80加入的火诸。而之前的Smalltalk-76锦针,并不是每個(gè)類有一個(gè)MetaClass,而是所有類的isa指針都指向一個(gè)特殊的類置蜀,叫做Class
(這種設(shè)計(jì)之后也被Java借鑒了)奈搜。
而每個(gè)類都有自己MetaClass的設(shè)計(jì),加入的原因是盯荤,因?yàn)镾malltalk里面馋吗,類是對(duì)象,而對(duì)象就可以響應(yīng)消息秋秤,那么類的消息的響應(yīng)的方法就應(yīng)該由類的類去存儲(chǔ)宏粤,而每個(gè)MetaClass就持有每個(gè)類的類方法脚翘。
問(wèn)題1:每個(gè)MetaClass的isa指針指向什么?
如果MetaClass再有MetaClass绍哎,那么這個(gè)關(guān)系將無(wú)窮無(wú)盡来农。Smalltalk里的解決方案是,指向同一個(gè)叫MetaClass
的類崇堰。
問(wèn)題2:MetaClass
的isa指針指向什么沃于?
指向他的實(shí)例,也就是實(shí)例的isa指向MetaClass
海诲,同時(shí)MetaClass
isa指向?qū)嵗庇ǎ嗷ブ钢?/p>
那么Smalltalk的繼承關(guān)系,其實(shí)和Objective-C的很像了(后面有class的是前者的MetaClass)特幔。
這時(shí)候產(chǎn)生了一個(gè)重要的問(wèn)題蒋困,假如去掉MetaClass,把類方法放到也類里面是否可行敬辣?
這個(gè)問(wèn)題雪标,我思索許久,發(fā)現(xiàn)其實(shí)是一個(gè)對(duì)面向?qū)ο蟮恼軐W(xué)思想問(wèn)題溉跃,要對(duì)這個(gè)問(wèn)題下結(jié)論村刨,不得不重新講講面向?qū)ο蟆?/p>
從Smalltalk重新認(rèn)識(shí)面向?qū)ο?/h3>
以前談到面向?qū)ο螅倳?huì)提到撰茎,面向?qū)ο笕卣鳎?strong>封裝嵌牺、繼承、多態(tài)龄糊。但其實(shí)逆粹,面向?qū)ο笾幸卜至髋桑鏑++這種來(lái)自Simula的設(shè)計(jì)思想的炫惩,更注重的是類的劃分僻弹,因?yàn)榉椒ㄕ{(diào)用是靜態(tài)的。而如Objective-C這種借鑒Smalltalk的他嚷,更注重的是消息傳遞蹋绽,是動(dòng)態(tài)響應(yīng)消息。
而面向?qū)ο笕N特征筋蓖,更基于的是類的劃分而提出的卸耘。
這兩種思想最大的不同,我認(rèn)為是自上而下和自下而上的思考方式粘咖。
- 類的劃分蚣抗,要求類的設(shè)計(jì)者是以一個(gè)很高的層次去設(shè)計(jì)這個(gè)類,提取出類的特性和本質(zhì)瓮下,進(jìn)行類的構(gòu)建翰铡。知道類型才可以去發(fā)送消息給對(duì)象设哗。
- 消息傳遞,要求的是類的設(shè)計(jì)者以消息為起點(diǎn)去構(gòu)建類两蟀,也就是對(duì)外界的變化進(jìn)行響應(yīng)网梢,而不關(guān)心自身的類型,設(shè)計(jì)接口赂毯。嘗試?yán)斫庀⒄铰玻瑹o(wú)法處理則進(jìn)行特殊處理。
在此不討論兩種方式的優(yōu)劣之分党涕,而著重講講Smalltalk這種設(shè)計(jì)烦感。
消息傳遞對(duì)于面向?qū)ο蟮脑O(shè)計(jì),其實(shí)在于給出一種對(duì)消息的解決方案膛堤。而面向?qū)ο髢?yōu)點(diǎn)之一的復(fù)用手趣,在這種設(shè)計(jì)里,更多在于復(fù)用解決方案肥荔,而不是單純的類本身绿渣。這種思想就如設(shè)計(jì)組件一般,關(guān)心接口燕耿,關(guān)心組合而非類本身中符。其實(shí)之所以有MetaClass這種設(shè)計(jì),我的理解并不是先有MetaClass誉帅,而是在萬(wàn)物都是對(duì)象的Smalltalk里淀散,向?qū)ο蟀l(fā)送消息的基本解決方案是統(tǒng)一的,希望復(fù)用的蚜锨。而實(shí)例和類之間用的這一套通過(guò)isa指針指向的Class單例中存儲(chǔ)方法列表和查詢方法的解決方案的流程档插,是應(yīng)該在類上復(fù)用的,而MetaClass就順理成章出現(xiàn)罷了亚再。
最后
回到一開(kāi)始那個(gè)問(wèn)題郭膛,為什么要設(shè)計(jì)MetaClass,去掉把類方法放到類里面行不行针余?
我的理解是饲鄙,可以,但不Smalltalk圆雁。這樣的設(shè)計(jì)是C++那種自上而下的設(shè)計(jì)方式,類方法也是類的一種特征描述帆谍。而Smalltalk的精髓正在于消息傳遞伪朽,復(fù)用消息傳遞才是根本目的,而MetaClass只不過(guò)是因此需要的一個(gè)工具罷了汛蝙。
PS:筆者這個(gè)問(wèn)題從MetaClass入手去思考烈涮,是百思不得其解的朴肺。后來(lái)看了很多面向?qū)ο蟮臇|西,才發(fā)現(xiàn)這不過(guò)是一個(gè)產(chǎn)物坚洽,而并不是一個(gè)重點(diǎn)戈稿。
PSS:對(duì)于類的實(shí)現(xiàn),Javascript中那種使用Protocol實(shí)現(xiàn)的方式也很有意思讶舰,受限于篇幅鞍盗,暫不展開(kāi)
有任何問(wèn)題歡迎評(píng)論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz