這篇文章源于美團面試官問的我一個問題,為什么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;
}
通過這個方法生成后,就成了大家熟悉的那張圖。
從這張圖上,我們可以看到通過這么一層繼承關系,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
,同時MetaClass
isa指向實例,相互指著。
那么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實現的方式也很有意思,受限于篇幅,暫不展開
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz