这篇文章源于美团面试官问的我一个问题,为什么 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,同时 MetaClass的 isa 指向实例,相互指着。

那么 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 实现的方式也很有意思,受限于篇幅,暂不展开

参考链接