苹果统一把消息接收者作为对象。所以,这是说,类也是对象。也就是说,OC中所有的类都是一种对象:由一个类实例化来的对象叫实例对象,这好理解,那么,类作为对象(称之为类对象),又是什么类的对象?当然也容易猜到,就是今天要学习的内容——元类(Metaclass)。

定义

元类就是类对象所属的类。所以,实例对象是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都一种对象,所以元类也是对象,那么元类是什么的实例呢?答:根元类,根元类是其自身的实例。这样一套下来,感觉就清晰了好多。

区别

  • 实例对象:当我们在代码中new一个实例对象时,拷贝了实例所属的类的成员变量,但不拷贝类定义的方法。调用实例方法时,根据实例的isa指针去寻找方法对应的函数指针。
  • 类对象:是一个功能完整的对象。特殊之处在于它们是由程序员定义而在运行时由编译器创建的,它没有自己的实例变量(这里区别于类的成员变量,他们是属于实例对象的,而不是属于类对象的,类方法是属于类对象自己的),但类对象中存着成员变量与实例方法列表。
  • 元类对象:OC的类方法是使用元类的根本原因,因为其中存储着对应的类对象调用的方法即类方法。其他时候都倾向于隐藏元类,因此真实世界没有人发送消息给元类对象。元类的定义和创建看起来都是编译器自动完成的,无需人为干涉。

获取方法

要获取一个类的元类,可使用如下定义的函数:

Class objc_getMetaClass(const char* name); //name为类的名字

此外还有一个获取对象所属的类的函数:

Class object_getClass(id obj) ;

元类的构建

OC作为运行时语言,上面提到的类与元类在运行时都是objc_class类型。

struct objc_class { 
  	Class isa; 
  	Class super_class; 
 	struct objc_class; 
  	struct objc_class* ; 
  	struct objc_class* super_class; 
  	const char* name; 
  	long version; 
  	long info; 
  	long instance_size; 
  	struct objc_ivar_list* ivars; 
  	struct objc_method_list** methodLists; 
  	struct objc_cache* cache; 
  	struct objc_protocol_list* protocols; 
}

Class定义如下:

typedef struct objc_class *Class;

OC中每个类中都包含一个isa变量,显然这里的isa是指向另一个类的指针,说白了就是表明这个类是哪个类的实例,以便找到代码中调用的本类或父类的类方法。对于NSObject及其子类,指向的就是它的元类,正如实例中也有个isa指针指向其所属的类一样。而对于元类,每个元类的isa都指向根元类。那么根元类的isa指向哪里?——它自己。这样就构成一个封闭的循环,实现了无懈可击的OC类系统。

每个类对象都有对应的元类,每个类(根类除外)都有一个superclass,同样每个元类也有一个superclass,并且子类与子元类、父类与父元类分别在同一层次。这种关系借用网上的一张图来说明,一目了然:

iOS 元类 ios 类 元类_ios


对象的isa指针指向哪里?可以看出:

  • instance对象的isa指针指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象(所以基类的meta-class对象指向自己)

注意:根元类的superclass不是nil而是根类。对于OC原生的类,根元类的父类就是系统的根类NSOject。但根类不一定是NSObject

元类的运行机制

上面讲到了OC运行时类的定义,这里再看对象的定义:

struct objc_object{ 
  Class isa; 
}

这说明OC中每个对象也都包含一个isa变量。这里的isa,指向实例对象所属的类。在运行时,[obj aMessage];被转化为objc_msgSend(obj, @selector (aMessage));,这里,@selector (aMessage)返回一个SEL数据类型,即方法选择器。SEL主要作用是快速的通过方法名字(aMessage)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个int型的地址,地址中存放着方法的名字。在一个类中,每一个方法对应着一个SEL。iOS类中不能存在两个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
类方法的调用可以和实例对象的方法寻址完美统一起来。当类调用类方法时,首先根据类的isa找到类对应的元类,然后在元类的cache中查找方法的实现,如果查找到就调用该方法,如果查找不到则从当前类methodLists中去查找方法实现,如果未找到则继续沿着superclass查找去寻找。元类也有父类,类方法的查找也可以沿着继承链去查找方法的实现。这样类方法的寻址和实例对象的寻址就可以按照同样的寻址方式去查找,也就不用对类方法和实例方法做区分对待。实现了方法的不区分寻址,在很大程度上简化了寻址流程,提升了方法执行的速度。
class即类对象中存储着实例方法。实际上,类对象中存储着类定义的一切:成员变量、属性列表,遵守的协议等,但不包括类方法。类方法存在哪?类方法是存在元类中的,此外元类中还存有类的信息(类的版本,名字)。比如发送一个类消息[class aMessage];class中的isa就指向class的元类,在元类中搜索调用的类方法,搜索层次类似于实例方法的搜索。
下面创建两个类:Person和它的子类Student,我们来看看:

Person *person = [[Person alloc] init];
Student *student = [[Student alloc] init];
        
Class person_class = object_getClass(person);
NSLog(@"person_class%@元类, address:%p", class_isMetaClass(person_class) ? @"是" : @"不是", 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);
NSLog(@"%@%@元类, address:%p", student_class,class_isMetaClass(student_class) ? @"是" : @"不是", 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不是元类, address:0x100009030
person_isa是元类, name:Person,address:0x100009058
Student不是元类, address:0x100008e78
student_isa是元类, name:Student, address:0x100008e50
student_isa_superclass是元类, name:Person, address:0x100009058