Runtime之MetaClass

iOS优化

对于iOS开发者来说,元类一直是一个在面试时被重点考察的点,为什么在语言设计时要增加元类这个概念?他的优点是什么?他到底有什么作用呢?这篇文章我们从Runtime源码的角度和语言设计的角度来探讨MetaClass存在的原因以及他的存在解决了哪些问题。

测试题

下面我们先看一下下面这个题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)classJudge {
Class ccls = [NSObject class];
Class icls = [Person class];

NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(ccls),NSStringFromClass(ccls),@([ccls isKindOfClass:ccls]));
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isKindOfClass:icls]));
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isKindOfClass:ccls]));

NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(ccls),NSStringFromClass(ccls),@([ccls isMemberOfClass:ccls]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isMemberOfClass:icls]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isMemberOfClass:ccls]));

}

请问上述代码的执行结果是什么?为什么?上面的代码中主要涉及到三个方法

  • +class 类方法
  • isKindOfClass
  • isMemberOfClass

下面我们带着这些问题,看下上面这三个方法

Class 方法

1
2
3
4
5
6
7
+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

不过我们在NSObject.h中只发现了

1
- (Class)class

对象方法的声明,且当我们点击我们自己代码中的方法名进行跳转时也是跳转到了对应的对象方法中,因此我们可以知道在我们平时开发中调用class方法时,都是调用的对象方法(即使是使用类调用)。

我们看到对于对象方法中实际上是调用了object_getClass方法,因此我们继续看这个方法的实现:

1
2
3
4
5
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}

这个方法是返回了当前对象的元类。

看完了方法的实现我们来分析下我们上面示例代码中获取到的类是什么:

1
2
Class ccls = [NSObject class];
Class icls = [Person class];

根据源码我们很容易得出结论:cclsNSObject类的元类 iclsPerson类的元类

isKindOfClass

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

与Class方法相同,在NSObject.h文件中我们只发现了

1
- (BOOL)isKindOfClass:(Class)cls

一个方法的声明,因此我们重点分析下这个方法的实现:

  • 判断当前类与给定类是否相同
  • 如果不相同,则查找当前类的父类是否为给定类
  • 如果相同,则返回
  • 如果查找到最后都不等于给定类 则返回NO

结合我们上面的示例,我们认为

1
2
3
NSLog(@"%@ isKindOfClass %@  result %@", NSStringFromClass(ccls),NSStringFromClass(ccls),@([ccls isKindOfClass:ccls]));
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isKindOfClass:icls]));
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isKindOfClass:ccls]));

的结果为:1 1 1
原因为:

  • 1和3均为相同的类 通过因此肯定是相等的
  • 2 因为Person类是继承自NSObject的因此这里也应该为1

isMemberOfClass

1
2
3
4
5
6
7
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

与前面两个方法相同,在NSObject.h文件中我们也是仅发现了对象方法的声明

1
- (BOOL)isMemberOfClass:(Class)aClass;

根据上述源码的实现,isMemberOfClass方法实际是:

  • 判断当前类的class是否为给给定类

在结合我们上面的代码示例

1
2
3
NSLog(@"%@ isMemberOfClass %@  result %@", NSStringFromClass(ccls),NSStringFromClass(ccls),@([ccls isMemberOfClass:ccls]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isMemberOfClass:icls]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isMemberOfClass:ccls]));

这里我们判断输出结果应该为: 1 1 1

结果分析

那么我们带着上面的猜测,确认打印输出:

1
2
3
4
5
6
2021-01-01 19:26:08.475701+0800 MetaClassDemo[5964:166878] NSObject isKindOfClass NSObject  result 1
2021-01-01 19:26:08.475858+0800 MetaClassDemo[5964:166878] Person isKindOfClass Person result 0
2021-01-01 19:26:08.475963+0800 MetaClassDemo[5964:166878] Person isKindOfClass NSObject result 1
2021-01-01 19:26:08.476068+0800 MetaClassDemo[5964:166878] NSObject isMemberOfClass NSObject result 0
2021-01-01 19:26:08.476188+0800 MetaClassDemo[5964:166878] Person isMemberOfClass Person result 0
2021-01-01 19:26:08.476315+0800 MetaClassDemo[5964:166878] Person isMemberOfClass NSObject result 0

结果很意外,我们猜想的是所有结果都为1,但是结果 2、4、5、6的结果均为0,那么我们来分析下原因:

isKindOfClass

1
2
3
Class ccls = [NSObject class];
Class icls = [Person class];
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isKindOfClass:icls]));

上面代码的结果为:0,明显我们知道Person类为NSObject类的子类,那么为何此处返回的是0呢?

我们在来看下isKindOfClass方法的实现:

1
2
3
4
5
6
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

我们发现

1
Class tcls = [self class];

这时候,tcls到底是到底是什么呢? 因为是对象方法因此这里调用到的是class对象方法,而Class的对象方法实现为:

1
2
3
4
5
6
7
8
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}

很明显这里我们得到的变量tcls实际上是类的元类,也就是说我们在for循环里遍历的是元类的superclass,那么for循环里的对比就很明显了 这里是用cls和元类的cls做对比肯定是不相同的。

那么问题又来了,为什么对于NSObject这个判断条件的结果是正确的呢?

这里我们就要贴出我们很熟悉的一张图来解释下了?

对于我们的示例

首先对于

1
2
3
Class ccls = [NSObject class];
Class icls = [Person class];
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isKindOfClass:icls]));

我们在isKindOfClass方法中实际上对比的是

1
2
3
if (icls == object_getClass(self)) {
// icls 是否等于icls的metacls
}

对于NSObject,NSObject的元类的superclass依然是NSObject(这里是一个闭环),因此对于NSObject来说上述代码比较结果为YES,而对于Person类对比Person类和Person的元类(以及元类的父类 RootClass为NSObject)是否相等答案显然是NO。

而对于

1
[Person Class] == object_getClass([NSObject class]) 

我们实际上比较的是NSobject类和Person的元类的父类(一直向上搜索)即NSObject类。而对于

1
[Person Class] == object_getClass([Person class]) 

实际比较的是Person类和Person类的元类的父类(一直向上搜索)即NSObject类 因此这里返回的是NO。

isMemberOfClass

根据isKindOfClass的提示我们来重新分析下isMemberOfClass方法的实现:

1
2
3
4
5
6
7
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

从上面的分析我们已经知道我们实际上调用的是对象方法- (BOOL)isMemberOfClass:(Class)cls,我们发现isMemberOfClass的判断条件为

1
[self class] == cls

对于下面这几种情况

1
2
3
4
5
NSLog(@"%@ isMemberOfClass %@  result %@", NSStringFromClass(ccls),NSStringFromClass(ccls),@([ccls isMemberOfClass:ccls]));

NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(icls),@([icls isMemberOfClass:icls]));

NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isMemberOfClass:ccls]));
  • NSObject == object_getClass([NSObject class]) 结果为NO
  • Person == object_getClass([Person class]) 结果为NO
  • Person == object_getClass([NSObject class]) 结果也为NO

我们发现实际上isMemberOfClass并没有像isKindOfClass那样递归的去查找父类,而是只判断了当前类。

总结

通过对上面题目的分析,我们了解了这两个重要方法的实现:

  • isKindOfClass

[A isKindOfClass: B];

B类是否为A类的元类或者A类元类的父类,这里因为NSObject的元类的superClass就是NSObject因此对于[[NSObjec class] isKindOfClass:[NSObject class]]返回是YES,更清晰的来说只有在判断某个类是否为NSObject类时这个条件才成立,为此我们特地做下面的测试:

1
2
3
4
5
Class ccls = [NSObject class];
Class icls = [Person class];
Class scls = [Female class];
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(icls),NSStringFromClass(ccls),@([icls isKindOfClass:ccls]));
NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass(scls),NSStringFromClass(icls),@([scls isKindOfClass:icls]));

结果:1,0。
这也验证了我们上面的结论

  • isMemberOfClass

[A isMemberOfClass: B]

B 类是否为A类元类 对于类方法的调用中这个条件是永远都不成立的。无论A与B之间是否存在继承或者被继承关系。

  • 对象方法

实际我们平时开发中最常用的是

1
2
3
4
5
6
7
8
- (void)instanceTest {
Female *f = [Female new];

NSLog(@"%@ isKindOfClass %@ result %@", NSStringFromClass([f class]),NSStringFromClass([Person class]),@([f isKindOfClass:[Person class]]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass([f class]),NSStringFromClass([Person class]),@([f isMemberOfClass:[Person class]]));
NSLog(@"%@ isMemberOfClass %@ result %@", NSStringFromClass([f class]),NSStringFromClass([Female class]),@([f isMemberOfClass:[Female class]]));

}

打印结果:

1
2
3
2021-01-01 23:27:53.040093+0800 MetaClassDemo[8537:312528] Female isKindOfClass Person  result 1
2021-01-01 23:27:53.040223+0800 MetaClassDemo[8537:312528] Female isMemberOfClass Person result 0
2021-01-01 23:27:53.040337+0800 MetaClassDemo[8537:312528] Female isMemberOfClass Female result 1

这里我们套用上面的结论:

  • 对于isKindOfClass 我们比较的是对象f的元类(即Female类)及其父类是否为Person类 很显然返回值为YES
  • 对于isMemberOfClass 我们比较的是f的元类是否为Person类 很明显这里应该是Female类因此第二行输出NO,第三行输出YES。

MetaClass结构

下面我们先来看看这个类的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

metaClass 也是objc_class 因此结构大概也是这样的!,不过就目前所知到的metaClass是用来存放类方法的! 因为OC中没有类属性因此 objc_ivar_list 为空!

实际上在OBJC2之后class的结构已经更新为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() const {
return bits.data();
}
// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
}

MetaClass的创建

首先了解一点,在OC中每一个类都有一个对应的MetaClass! 因此 我们在创建一个类的时候,就会一起创建这个类的元类。

因此,我们可以看一下当我们动态创建一个类的时候 运行时实际上都做了什么!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class objc_allocateClassPair(Class superclass, const char *name, 
size_t extraBytes)
{
Class cls, meta;

rwlock_writer_t lock(runtimeLock);

// Fail if the class name is in use.
// Fail if the superclass isn't kosher.
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}

// Allocate new classes.
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);

// fixme mangle the name if it looks swift-y?
objc_initializeClassPair_internal(superclass, name, cls, meta);

return cls;
}

从上面的代码中我们可以很明显的看出:

1
2
cls  = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);

没创建一个cls我们都会创建一个metaClass, clsmeta的创建方法和参数也是完全一致的!

怎么获取一个类的MetaClass

objc_getMetaClass(const char * _Nonnull name)

我们来看一下这个方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class objc_getMetaClass(const char *aClassName)
{
Class cls;

if (!aClassName) return Nil;

cls = objc_getClass (aClassName);
if (!cls)
{
_objc_inform ("class `%s' not linked into application", aClassName);
return Nil;
}

return cls->ISA();
}

先是使用objc_getClass获取类名对应的类 然后直接利用cls->ISA()获取这个类对应的元类

同样在Runtime新版本中我们也能找到下面这个方法:

1
2
3
4
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}

判断一个类是否为元类

bool class_isMetaClass(Class _Nullable cls)

1
2
3
4
5
6
7
8
9
10
11
12

BOOL class_isMetaClass(Class cls)
{
if (!cls) return NO;
return cls->isMetaClass();
}

bool isMetaClass() {
assert(this);
assert(isRealized());
return data()->ro->flags & RO_META;
}

从上面的代码我们可以看出 判断是否为元类的条件是 data()->ro->flags & RO_META。那么这个类的flags是什么时候设置的呢?

找了好久,我们在objc_initializeClassPair_internal方法中找到了下面这段代码

1
2
3
4
5
6
cls_ro_w->flags = 0;
meta_ro_w->flags = RO_META;
if (!superclass) {
cls_ro_w->flags |= RO_ROOT;
meta_ro_w->flags |= RO_ROOT;
}

那么这个方法什么时候被调用呢? 追踪了一下我们发现 是在objc_initializeClassPair_internal这个方法中被调用的! 这个方法是不是很熟悉呢! 他就是objc_allocateClassPair中的最后一句代码。

MetaClass 存在的意义是什么呢?

实际上如果没有元类的存在,而是在class中增加一个classmethodlist用来存放这个类的类方法,也是可以达到我们想要实现的目的。

那么MetaClass存在的意义到底是什么呢?

为了更好的了解MetaClass存在的原因,我们先看一下他的作用:

先看一下下面这张图片:

metaclass

一切皆对象的思想

我们都知道对象里有一个isa指针

ISA指针 实际上就是 is a 的缩写。表示这个对象是一个什么

从上面的图上我们也可以看出最顶层的RootMetaClass的isa指向知己superClass指向RootClass,这就形成了一个闭环。

加入 我们将元类去掉,那么我们类的ISA指针应该指向什么呢?如果没办法指向一个RootMetaClass那表明这个类是一个什么类型的对象呢?

当然这个是根据结论反推原因,是不太合逻辑的,但是 更容易被我们理解。

从Smalltalk重新认识面向对象

以前谈到面向对象,总会提到,面向对象三特征:封装、继承、多态。但其实,面向对象中也分流派,如C++这种来自Simula的设计思想的,更注重的是类的划分,因为方法调用是静态的。而如Objective-C这种借鉴Smalltalk的,更注重的是消息传递,是动态响应消息。

而面向对象三种特征,更基于的是类的划分而提出的。

这两种思想最大的不同,我认为是自上而下和自下而上的思考方式。

  • 类的划分,要求类的设计者是以一个很高的层次去设计这个类,提取出类的特性和本质,进行类的构建。知道类型才可以去发送消息给对象
  • 消息传递,要求的是类的设计者以消息为起点去构建类,也就是对外界的变化进行响应,而不关心自身的类型,设计接口。尝试理解消息,无法处理则进行特殊处理。

消息传递对于面向对象的设计,其实在于给出一种对消息的解决方案。而面向对象优点之一的复用,在这种设计里,更多在于复用解决方案,而不是单纯的类本身。这种思想就如设计组件一般,关心接口,关心组合而非类本身。其实之所以有MetaClass这种设计,我的理解并不是先有MetaClass,而是在万物都是对象的Smalltalk里,向对象发送消息的基本解决方案是统一的,希望复用的。而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。

上面这部分是摘自:Why is MetaClass in Objective-C

维基百科的解释

1
2
3
4
5
6
7
8
9
10
11
12
13

Metaclasses in Objective-C are almost the same as those in Smalltalk-80—not surprising since Objective-C borrows a lot from Smalltalk. Like Smalltalk, in Objective-C, the instance variables and methods are defined by an object's class. A class is an object, hence it is an instance of a metaclass.(OC中的Metaclasses 基本上和Smalltalk-80相同,鉴于OC从Smalltalk中借鉴了很多因此这并不令人干到奇怪。类似Smalltalk 在OC中实例的变量和方法被定义在对象的类中,class也是一个对象,于是class是mataclass的一个实例。)

Like Smalltalk, in Objective-C, class methods are simply methods called on the class object, hence a class's class methods must be defined as instance methods in its metaclass. Because different classes can have different sets of class methods, each class must have its own separate metaclass. Classes and metaclasses are always created as a pair: the runtime has functions objc_allocateClassPair() and objc_registerClassPair() to create and register class-metaclass pairs, respectively.(类似Smalltalk,在OC中类方法通过类对象调用,于是一个类的类方法必须在metaclass中以实例方法的形式定义。因为不同的类可以后不同的类方法集合,每一个类必须有自己独立的metaClass。Class和class-metaClass一起被创建和注册)

There are no names for the metaclasses; however, a pointer to any class object can be referred to with the generic type Class (similar to the type id being used for a pointer to any object). (metaClass没有名字,然而,指向任何类对象的指针可以用泛型类型引用)(类似id可以指向所有的对象)

Because class methods are inherited through inheritance, like Smalltalk, metaclasses must follow an inheritance scheme paralleling that of classes (e.g. if class A's parent class is class B, then A's metaclass's parent class is B's metaclass), except that of the root class.(因为,类方法通过继承获取,类似Smalltalk,元类必须遵循与类类似的继承方案

Unlike Smalltalk, the metaclass of the root class inherits from the root class (usually NSObject using the Cocoa framework) itself. This ensures that all class objects are ultimately instances of the root class, so that you can use the instance methods of the root class, usually useful utility methods for objects, on class objects themselves.(与Smalltalk不同,根类的metaclass继承自根类本身,这就确保了所有的类对象都是根类的对象。以便您可以使用根类的实例方法,通常是对象的有用实用工具方法,以及类对象本身。)

Since metaclass objects do not behave differently (you cannot add class methods for a metaclass, so metaclass objects all have the same methods), they are all instances of the same class—the metaclass of the root class (unlike Smalltalk). Thus, the metaclass of the root class is an instance of itself. The reason for this is that all metaclasses inherit from root class; hence, they must inherit the class methods of the root class.
(由于元类对象行为不同(你不能为元类添加类方法,所以元类对象都有相同的方法),它们都是同一类的实例 - 根类的元类(与Smalltalk不同)因此,根类的元类是它自己的一个实例。原因是所有元类都继承自根类;因此,他们必须继承根类的类方法。)

最后

回到一开始那个问题,为什么要设计MetaClass,去掉把类方法放到类里面行不行?

我的理解是,可以,但不Smalltalk。这样的设计是C++那种自上而下的设计方式,类方法也是类的一种特征描述。而Smalltalk的精髓正在于消息传递,复用消息传递才是根本目的,而MetaClass只不过是因此需要的一个工具罢了。

参考文章

Why is MetaClass in Objective-C
function/bind的救赎(上)