注:我们引用apple开源代码中objc4-750中的相关源代码定义。
OC中的对象是类的实例化出来的,这个我们都能理解。可是元类是什么,元类存在的意义是什么?
首先来看一下OC中关于对象的实现定义,
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
...
};
在对象结构体中居然只有一个isa_t类型的isa联合体[isa_t是拓展之后的isa,这个结构中包含了很多的信息,远不止是一个类指针信息,为了简化期间,以下讨论简化isa为一个指针],那么对象的实例方法和成员变量放在哪里呢?因为只有一个isa指针,所以我们也只能从isa中去搜一下。下边我们看一下class的实现
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
...
}
在这个结构体定义中,发现了父类的指针(superclass),发现了类的名称(name)以及其他描述信息(版本号version,类信息标志info,以及实例变量的大小描述instance_size)我们不难发现其实对象的所有成员变量(ivars),方法列表(methodLists),协议列表(protocols)都是存放在存放在类的定义中.而在实例对象进行方法调用时,对象会首先根据isa指针找到对应的类,然后从类的缓存(cache)中寻找方法实现,如果查找到方法实现则调用同时终止查找,如果没有则从methodLists中继续查找,如果找到则调用同时将该方法加入cache中并终止查找;如果没有发现,则顺着superclass指针继续遍历父类的methodLists,找到则调用同时将方法加入当前类的cache中并终止查找,依次递归直至找到方法实现或者直至父类为nil。 这种将对象方法保存在类中的做法,使得通过同一个类实例化的对象共享了同一份实例方法的实现(所有的对象共享保存在类中的一份方法实现),对象本身只需要保存自己的独立的数据就好了,既节约了存储空间又极大提升了运行的速度。
那么问题来了,在上边类的结构中并没有发现方法有区分实例方法和类方法,那么类的方法保存在哪里,调用类方法是又是如何查找实现的呢?
是不是觉得漏掉了啥?是的,在这个结构中类的结构是继承自struct objc_object结构可以理解为类是一种特殊的对象.所以在类中保存了一个跟实例对象一模一样的isa结构,那这个结构会是干嘛的呢?试想这样的场景:如果类方法保存在当前的类中,那么继承自同一个类的子类就必须要具有多个类方法的拷贝,不仅占据了大量的存储空间,也会给方法实现的查找带来巨大的额外开销。所以类方法的存储要么在superclass指向空间,要么就在isa保存的信息指向的空间。而如果类方法保存在superclass指向空间的话,会面临同样的问题,需要单独开辟空间来存储类方法和实例方法的标记,而每次调用都会陷入首先区分类方法还是实例方法,然后再去找遍历对应的方法列表,而事实上在上述类的实现中我们也并没有发现关于类方和实例方法的区分标志,但是在类的实现中很显然没有对方法进行类方法列表和实例方法列表的区分。那如果是像实例对象一样,类方法的实现保存在另外一个独立的地方,而同一个类所有的子类共享了一份类方法的实现,这样是不是就可以把对象方法的寻址方式和类方法的寻址方式统一起来了?所以类中的isa结构的作用就出现了,而类对象isa指针指向的空间地址,就是抽象出来的元类。
根据以上的理解,类方法的调用就会和实例对象的方法寻址完美统一起来。当类调用类方法时,首先根据类的isa找到类对应的元类,然后在元类的cache中查找方法的实现,如果查找到就调用该方法,如果查找不到则从当前类methodLists中去查找方法实现,如果未找到则继续沿着superclass查找去寻找。元类也有父类,类方法的查找也可以沿着继承链去查找方法的实现。这样类方法的寻址和实例对象的寻址就可以按照同样的寻址方式去查找,也就不用对类方法和实例方法做区分对待。实现了方法的不区分寻址,在很大程度上简化了寻址流程,提升了方法执行的速度。
于是乎,来看看这在网上已经被多次引用到的实例对象,类对象和元类对象的关系图。
从这张关系图我们可以看出如下结论:
- 实例对象的isa指针指向了初始化该对象的类,而该对象的实例方法就保存在这个类的继承链中;
- 类对象的isa指针指向了该类的元类,类方法存在与该元类的继承链中;
- 元类与普通类一样,也有父类,也具备自己的继承关系链;
- 元类的super_class指向了自己的父类,而isa 指向了最原始的元类;
- 最原始的根元类指向OC的Root class(NSObject),而Root class的isa却指向了最原始的元类;
- 根元类有个特别的属性,rootmetaclass->isa() 与 rootmetaclass相同.
下边来做个简单的验证,首先先来做些预备知识:
//用于获取obj中isa指向:所以如果obj是实例对象,isa就指向了生成对象的类;如果obj是类,isa就指向了该类的元类
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
//用于判断当前类是否是元类
bool isMetaClass() {
assert(this);
assert(isRealized());
return data()->ro->flags & RO_META;
}
定义Person继承自NSObject,Student继承自Person
(1)验证:Person的元类和Student的元类的父类是同一个类.
Class person_class = object_getClass([[Person alloc] init]);
NSLog(@"person_class%@元类", class_isMetaClass(person_class) ? @"是" : @"不是");
Class person_isa = object_getClass([Person class]);
NSLog(@"person_isa%@元类, name:%@,address:%p", class_isMetaClass(person_isa) ? @"是" : @"不是", person_isa, person_isa);
Class student_class = object_getClass([[Student alloc] init]);
NSLog(@"student_class%@元类", class_isMetaClass(student_class) ? @"是" : @"不是");
Class student_isa = object_getClass([Student class]);
NSLog(@"student_isa%@元类, name:%@, address:%p", class_isMetaClass(student_isa) ? @"是" : @"不是", student_isa ,student_isa);
Class student_isa_superclass = class_getSuperclass(student_isa);
NSLog(@"student_isa_superclass%@元类, name:%@, address:%p", class_isMetaClass(student_isa_superclass) ? @"是" : @"不是", student_isa_superclass,student_isa_superclass);
输出结果:
person_class不是元类
person_isa是元类, name:Person,address:0x100002178
student_class不是元类
student_isa是元类, name:Student, address:0x1000021c8
student_isa_superclass是元类, name:Person, address:0x100002178
从验证中,可以看出:
- Person->isa,student->isa和student->isa->superClass都是元类,而Person和Student却不是;
- Person->isa和Student->isa->superClass是指向同一块对象空间的;
- 实例类所对应的元类的名称是和实例类相同的,也就是说实例类生成了同名的元类。所以如果你用过Runtime动态注册类,就会发现是注册了"一对"类和元类两个类,而不仅仅是一个类:
OBJC_EXPORT void
objc_registerClassPair(Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
(2)如何获取一个类的全部类方法和实例方法?
根据上边的理论,实例方法保存在生成实例的类对象空间,而类方法则保存在类对应的元类对象空间,我们对刚才的Person和Student类做如下动
@interface Person : NSObject
+ (void)classMethodForPerson;
- (void)instanceMethodForPerson;
@end
@implementation Person
+ (void)classMethodForPerson {}
- (void)instanceMethodForPerson {}
@end
@interface Student : Person
+ (void)classMethodForStudent;
- (void)instanceMethodForStudent;
@end
@implementation Student
+ (void)classMethodForStudent {}
- (void)instanceMethodForStudent {}
@end
验证:类方法和实例方法分别保存在对应的类和元类中.
//Person
Class person_class = [Person class];
//等价于 object_getClass([[Person alloc]init])
NSMutableArray<NSString *> *methods_person_class = [NSMutableArray array];
unsigned int count_person_class = 0;
Method *methods = class_copyMethodList(person_class, &count_person_class);
for(unsigned int i = 0; i < count_person_class; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
[methods_person_class addObject:NSStringFromSelector(sel)];
}
NSLog(@"methods_person_class == %@", methods_person_class);
Class person_metaclass = object_getClass(person_class);
NSMutableArray<NSString *> *methods_person_metaclass = [NSMutableArray array];
unsigned int countOfmethods_Person_metaclass = 0;
methods = class_copyMethodList(person_metaclass, &countOfmethods_Person_metaclass);
for(unsigned int i = 0; i < countOfmethods_Person_metaclass; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
[methods_person_metaclass addObject:NSStringFromSelector(sel)];
}
NSLog(@"methodsOfPerson_metaclass == %@", methods_person_metaclass);
输出结果:
methods_person_class == (
instanceMethodForPerson
)
methodsOfPerson_metaclass == (
classMethodForPerson
)
可以使用Student类做同样的验证,可见类方法和实例方法确实不是保存在不同的对象中:实例方法保存在生成实例的类中,而类方法保存在类对应的元类中.
验证:不同的子类共享了相同的对象共享了相同的方法实现.
分别使用Person类生成两个不同的对象,分别调用instanceMethodForPerson方法
Person *person = [[Person alloc] init];
[person instanceMethodForPerson];
Person *person2 = [[Person alloc] init];
[person2 instanceMethodForPerson];
然后添加符号断点instanceMethodForPerson,同时添加Action为bt
然后运行:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000e0c LineTool`-[Person instanceMethodForPerson](self=0x00000001006a68b0, _cmd="instanceMethodForPerson") at main.m:22:34
frame #1: 0x0000000100000e86 LineTool`main(argc=1, argv=0x00007ffeefbff580) at main.m:50:9
frame #2: 0x00007fff6782f7fd libdyld.dylib`start + 1
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000e0c LineTool`-[Person instanceMethodForPerson](self=0x0000000100700400, _cmd="instanceMethodForPerson") at main.m:22:34
frame #1: 0x0000000100000ebd LineTool`main(argc=1, argv=0x00007ffeefbff580) at main.m:54:9
frame #2: 0x00007fff6782f7fd libdyld.dylib`start + 1
可以发现两次执行统一方法,虽然对象不同(self=0x00000001006a68b0和self=0x0000000100700400),但是调用方法的实现地址是相同的(0x0000000100000e0c).可以尝试打印一下这个方法:
(lldb) po (void(*)(void*,SEL,...))0x0000000100000e0c
(LineTool`-[Person instanceMethodForPerson] + 12 at main.m:22:34)
然后通过打印对应的信息就可以验证我们的结论。
欢迎留言评论指正交流