在运行时创建一个类
1 | Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0); |
添加的方法使用名为ReportFunction其实现的函数,其定义如下:
1 | void ReportFunction(id self, SEL _cmd) |
从表面上看,这一切都非常简单。在运行时创建类只需三个简单的步骤:
- 为“类对”分配存储(使用objc_allocateClassPair)。
- 根据需要将方法和ivars添加到类中(我添加了一个方法class_addMethod)。
- 注册该类以便可以使用(使用objc_registerClassPair)。
然而,当前的问题是:什么是“class pair”?该函数objc_allocateClassPair只返回一个值:类。那对的另一半在哪里?
我敢肯定你已经猜到了这一对的另一半是元类(这是这篇文章的标题)但是为了解释这是什么以及你为什么需要它,我将给出一些关于对象的背景知识和Objective-C中的类。
成为对象de数据结构需要什么?
在Objective-C中,对象的类由其isa指针确定。该isa指针指向对象的类。
1 | typedef struct objc_object { |
这就是说:任何以指向Class结构的指针开头的结构都可以视为objc_object。
Objective-C中对象最重要的特性是你可以向它们发送消息:
1 | [@"stringValue" |
什么是元级?
现在,正如您可能已经知道的那样,Class 在Objective-C中也是一个对象。这意味着您可以发送消息给Class。
1 | NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding]; |
在这种情况下,defaultStringEncoding被发送到NSString class。
这是有效的,因为Class 在Objective-C中的每个都是一个对象本身。这意味着Class结构必须以isa指针开头,以便它与objc_object上面显示的结构二进制兼容,并且结构中的下一个字段必须是指向superclass(或nil基类)的指针。
正如我上面所展示的那样Class,根据您运行的运行时版本,有几种不同的方式可以定义,但是,它们都以isa字段后跟字段开头superclass。
1 | typedef struct objc_class *Class; |
但是,为了让我们调用类上的方法,类的isa指针本身必须指向一个类结构,而这个类结构必须包含我们可以在类上调用的方法列表。
这就引出了元类的定义:元类是类对象的类。
简单地说:
- 当您向对象发送消息时,将在对象类的方法列表中查找该消息。
- 当您向类发送消息时,将在类的元类的方法列表中查找该消息。
元类非常重要,因为它存储类的类方法。每个类必须有一个惟一的元类,因为每个类都有一个潜在惟一的类方法列表。
元类的类是什么?
与之前的类一样,元类也是一个对象。这意味着您也可以在其上调用方法。当然,这意味着它还必须有一个类。
所有元类都使用基类的元类
(继承层次结构中顶层类的元类)作为类
。这意味着对于从NSObject派生的所有类(大多数类),元类
都将NSObject元类
作为其类
。
遵循所有元类都使用基类的元类作为它们的类的规则,任何基元类都将是它自己的类(它们的isa指针指向它们自己)。这意味着NSObject元类上的isa指针指向它自己(它是它自己的一个实例)。
类和元类的继承
就像类用它的超类指针指向超类一样,元类使用它自己的超类指针指向类的超类的元类。
更奇怪的是,基类
的元类
将其超类
设置为基类本身
。
这种继承层次结构的结果是,层次结构中的所有实例、类和元类都继承自层次结构的基类。
对于NSObject层次结构中的所有实例、类和元类,这意味着所有NSObject实例方法都是有效的。对于类和元类,所有NSObject类方法也是有效的。
所有这些在文本中都很混乱。Greg Parker将实例、类、元类和它们的超类以及它们是如何组合在一起的组成了一个很好的图表。
这一点的实验证实
为了确认这一切,让我们看看我在本文开头给出的ReportFunction的输出。这个函数的目的是跟踪isa指针并记录它所发现的内容。
要运行ReportFunction,我们需要创建动态创建的类的实例,并在其上调用report方法。
1 | id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil]; |
由于报表方法没有声明,所以我使用performSelector:调用它,因此编译器不会给出警告。
ReportFunction现在将遍历isa指针,并告诉我们使用哪些对象作为类、元类和元类的类。
获取对象的类:ReportFunction使用object_getClass跟踪isa指针,因为isa指针是该类的受保护成员(您不能直接访问其他对象的isa指针)。ReportFunction不使用类方法来实现这一点,因为在类对象上调用类方法不会返回元类,而是再次返回类(因此[NSString类]将返回NSString类而不是NSString元类)。
This is the output (minus NSLog prefixes) when the program runs:
1 | This object is 0x10010c810. |
反复查看isa值所达到的地址:
- 对象是地址0x10010c810。
- 类的地址是0x10010c600。
- 元类是地址0x10010c630。
- 元类的类(即NSObject元类)是地址0x7fff71038480。
- NSObject元类的类是它自己。
地址的值并不十分重要,除非它显示了从类到元类到NSObject元类的过程。
总结
元类是类对象的类。每个类都有自己独特的元类(因为每个类都可以有自己独特的方法列表)。这意味着所有类对象本身并不都属于同一个类。
元类将始终确保类对象具有层次结构中基类的所有实例和类方法,以及中间的所有类方法。对于派生自NSObject的类,这意味着所有NSObject实例和协议方法都为所有类(和元类)对象定义。
所有元类本身都使用基类的元类(NSObject层次结构类的NSObject元类)作为类,包括运行时中惟一自定义的基类元类。