面试题:对象的isa指针指向哪里?OC的类信息存放在哪里?
一、OC对象的分类
Objective-C中的对象,简称OC对象,主要可以分为3种:
- instance对象(实例对象)
- class对象(类对象)
- meta-class对象(元类对象)
1.1. instance对象
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。
1 | NSObject *object1 = [[NSObject alloc] init]; |
object1
、object2
是NSObject的instance对象(实例对象)。
它们是不同的两个对象,分别占据着两块不同的内存。
instance对象在内存中存储的信息包括:
- isa指针
- 其他成员变量
1.2. class对象
1 | Class objectClass1 = [object1 class]; |
objectClass1 ~ objectClass5
都是NSObject的class对象(类对象)。
它们是同一个对象,每个类在内存中有且只有一个class对象。
class对象在内存中存储的信息主要包括:
- isa指针
- superclass指针
- 类的属性信息(@property)
- 类的对象方法信息(instance method)
- 类的协议信息(protocol)
- 类的成员变量信息(ivar)
- ……
1.3. meta-class对象
1 | // 将类对象当做参数传入,获得元类对象 |
objectMetaClass
是NSObject的meta-class对象(元类对象)。
每个类在内存中有且只有一个meta-class对象。
meta-class对象和class对象的内存结构是一样的(都是Class类型),但是用途不一样,在内存中存储的信息主要包括:
- isa指针
- superclass指针
- 类的类方法信息(class method)
- ……
注意:以下代码获取的objectClass
是class对象(class方法返回的一直是class对象),并不是meta-class对象。
1 | Class objectClass = [[NSObject class] class]; |
查看Class是否为meta-class:
1 | BOOL result = class_isMetaClass([NSObject class]); |
1.4. object_getClass和objc_getClass
object_getClass
和objc_getClass
有什么区别呢?我们可以看一下源码(objc-class.mm
)。
由于object_getClass
内部调用了getIsa
方法,因此这个方法会返回指向父类的信息。
- 如果是instance对象,返回class对象。
- 如果是class对象,返回meta-class对象。
- 如果是meta-class对象,返回NSObject(基类)的meta-class对象。
1 | Class object_getClass(id obj) |
通过objc_getClass
方法传入一个类名字符串,最终返回对应的类对象。如果类不存在,就返回nil
。
由于看不到- (Class)class
和+ (Class)class
方法的具体实现,但是从两个方法的返回结果可以看出(也可以把代码转为C++),内部调用的就是objc_getClass
方法。
1 | Class objc_getClass(const char *aClassName) |
二、isa和superclass
示例代码:
1 | @interface Person : NSObject<NSCopying> |
2.1. isa指针
1 | int main(int argc, const char * argv[]) { |
把上面的代码转为C++后可以看到,OC调用方法(实例方法/类方法)的本质就是消息转发。
1 | int main(int argc, const char * argv[]) { |
当一个实例对象调用对象方法或类方法时,是如何让实例对象与类对象和元类对象之间产生关系呢?通过isa。
Student的实例对象调用实例方法时,会向实例对象发送消息,然后通过实例对象的isa找到class,最终调用class中的实例方法。
Student类调用类方法时,会向类对象发送消息,然后通过类对象的isa找到meta-class,最终调用meta-class中的类方法。
instance的isa指向class:当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。
class的isa指向meta-class:当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。
2.2. class对象的superclass指针
1 | int main(int argc, const char * argv[]) { |
把上面的代码转为C++:
1 | int main(int argc, const char * argv[]) { |
很神奇的发现,调用父类的实例方法和调用自己的实例方法,两者代码几乎是一样的,都是向Student的实例对象发送消息。Student的实例对象是怎么找到父类中的方法呢?通过superclass。
当Student的实例对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用。
2.3. meta-class对象的superclass指针
1 | int main(int argc, const char * argv[]) { |
把上面的代码转为C++:
1 | int main(int argc, const char * argv[]) { |
原理和上面的2.2差不多,这里就不在赘述,但需要再次强调一点:类方法存储在元类对象的内存中。
当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用。
2.4. isa和superclass之间的关系
下图中的Subclass(子类)、Superclass(父类)、Rootclass(基类)很好的诠释了Instance、class、meta这三者之间的关系。
- instance的isa指向class。
- class的isa指向meta-class。
- meta-class的isa指向基类的meta-class(基类的meta-class的isa指向自己的meta-class)。
- class的superclass指向父类的class(如果没有父类,superclass指针为nil)。
- meta-class的superclass指向父类的meta-class(基类的meta-class的superclass指向基类的class)。
- instance调用对象方法的轨迹:instance的isa找到自己的class,在class中找方法,如果方法不存在,就通过superclass找父类的class中的方法,直到基类的class,如果最终没找到并且没有做处理,程序就会崩溃
unrecognized selector sent to instance 0x1005453a0
)。 - class调用类方法的轨迹:instance的isa找自己的class,通过class的isa找到meta-class,在meta-class中找方法,如果方法不存在,就通过superclass找父类的meta-class中的方法,直到基类的meta-class,如果基类的meta-class中也没有找到对应的方法,就会到基类的class中找,如果最终没找到并且没有做处理,程序就会崩溃
unrecognized selector sent to class 0x100008478
。
2.5. 流程分析
接下来通过一个小案例,加强对2.4的理解。
新建一个Person类,并添加一个类方法+ (void)test
。创建NSObject的分类,并新增类方法+ (void)test
。
Person:
1 | @interface Person : NSObject |
NSObject+Test:
1 | @interface NSObject (Test) |
main.m:
1 | // |
如果不实现Person类的+ (void)test
方法,最终Person调用test
方法时会找父类元类对象,看其是否存在该方法。
[Person test]
本质是向Person类对象发消息,所以在NSObject分类中打印的self
是Person类对象。
1 | @implementation Person |
在上面的基础上,如果把NSObject分类中的实现方法修改为对象方法,会出现什么情况呢?
1 | @implementation NSObject (Test) |
发现最终调用的是对象方法。代码中调用的是类方法,为什么会调用对象方法呢?
其实上面讲解的2.3和2.4已经给出答案了。调用类方法会从元类对象中查找方法,如果不存在会沿着元类的superclass依次往上查找,最终基类的元类对象也没有找到方法时,会找基类中的方法。
疑问:为什么最终基类的meta-class中类方法找不到,会到基类的class中找(class中只有对象方法,类方法也只会在meta-class中存储)?
这是因为OC调用方法的本质是对象发送消息,[Person test]
的本质就是objc_msgSend([Person class], @selector(test));
。重点来了,objc_msgSend
的第一个参数传入的是对象,第二个参数是让传入一个方法名,但是并没有说明是类方法(+)还是对象方法(-)。因此,当基类中的元类对象中没有找到类方法时,会通过基类的元类对象superclass找到基类对象,然后在基类中查找对象方法,如果还是没有找到就会报错。
通过上面的示例代码可以看出,无论是实例对象还是类对象,调用方法的流程基本都是一样的。先通过isa找自己类/元类中的方法,然后通过superclass找,最终到达基类的superclass(指向nil)。
三、Class的结构
通过上面的了解,我们已经知道每一个对象都有一个Class类型的isa指针,也就是说实例对象的isa保存的是类对象的地址,类对象的isa保存的是元类对象的地址。我们通过打印相关参数验证一下。
1 | Person *person = [[Person alloc] init]; |
由于无法直接获取到isa信息,我们可以通过lldb指令在控制台打印相关信息。
为什么类的实例对象isa指向的地址和类对象地址不一样?这是因为从64bit开始,isa需要进行一次位运算,才能计算出真实地址。
ISA_MASK的值在objc源码可以看到,分为arm64(真机)和x86_64(模拟器),下面的代码是精简后的:
1 |
通过位运算就可以得到真正的isa地址,最终结果和personClass
地址一致(注意:由于我运行的是Mac程序,所以使用的是x86对应的值)。
默认情况下,类对象的isa和元类对象的isa是无法直接获取到的(即使通过控制台访问也不行),我们可以模仿objc_class
的实现。
Class在API中的相关定义(class、meta-class对象的本质结构都是struct objc_class
):
1 | typedef struct objc_class *Class; |
模仿objc_class:
1 | struct db_objc_class { |
只有isa会进行位运算,superclass保存的还是原值。
面试题1:对象的isa指针指向哪里?
解答:instance对象的isa指向class对象,class对象的isa指向meta-class对象,meta-class对象的isa指向基类的meta-class对象。
面试题2:OC的类信息存放在哪里?
解答:对象方法、属性、成员变量、协议信息,存放在class对象中。类方法,存放在meta-class对象中。成员变量的具体值,存放在instance对象。