iOS--oc对象,类,和元类本质
- 前言
- 实例对象的具体结构
- 自定义类对象的结构
- 继承关系
- 类信息的存放
- 对isa、superclass总结
前言
最近在学习runtime的过程中,发现其中消息发送-动态方法解析-消息转发中涉及到了大量的类与对象的底层知识,看别人博客的时候,别人往往也会先讲一遍oc对象的本质 ;
具体参考了iOS底层原理总结 - 探寻OC对象的本质
实例对象的具体结构
在探讨oc对象的本质前,首先我们要明白oc语言他的底层实现都是c/c++代码 ;
如图:
oc中的对象,在c/c++中往往以结构体的形式存在 ;
NSobject对象如下:
struct NSObject_IMPL {
Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;
我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。
也就是说Nsobject结构体中只存有一个isa指针,至于这个指针指向哪里,其实我们也可以大致猜到了,这个isa指针指向了类(结构体对象);
这里注意一个点,这里的isa指针在arm64架构前是一个单纯的指针,但在arm64架构优化指针后底层是一个联合图或者说共用体(union);这里的isa使用的共用体为了节省空间,不断的进行值覆盖的操作,结合位域可以更大限度的节约内存空间,还不用覆盖旧值 ;
其中的具体细节就不说了,只需要知道优化后的共用体不仅存放这类对象或元类对象地址,还存放了很多额外属性 ;
- 还有,指针在64位架构中占8个字节;
- 在Objective-C中,对象的地址确实可以视为指向其内部isa指针的地址,因为isa是对象内存布局的第一个元素。所以,当你说“objc存储的就是isa的地址”,这一点是对的。
自定义类对象的结构
这里用别人的一个例子:
@interface Student : NSObject{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
NSLog(@"%@",stu);
}
return 0;
}
@end
这里中的对象的结构体可以转换为如下的形式:
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
此结构体占用多少存储空间,对象就占用多少存储空间。因此结构体占用的存储空间为,isa指针8个字节空间+int类型_no4个字节空间+int类型_age4个字节空间共16个字节空间;
具体内存大小的分析记得要内存对齐 ;
那么一个NSObject对象占用多少内存? NSObjcet实际上是只有一个名为isa的指针的结构体,因此占用一个指针变量所占用的内存空间大小,如果64bit占用8个字节,如果32bit占用4个字节。
继承关系
/* Person */
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]),
class_getInstanceSize([Student class])
);
}
return 0;
}
类对象实质上是以结构体的形式存储在内存中;下面是类对象结构体的图示:
注意一下,上面的类对象结构体是不完整的,应该是一种简化版本 ;
- 我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。
那么他们所占的内存空间是多少呢?单纯的将指针和成员变量所占的内存相加即可吗?上述代码实际打印的内容是16 16,也就是说,person对象和student对象所占用的内存空间都为16个字节。
其实实际上person对象确实只使用了12个字节。但是因为内存对齐的原因。使person对象也占用16个字节。
类信息的存放
从实例对象的结构出发,我们知道了它的结构体中只有isa指针和成员变量 ;
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象;
instance对象在内存中存储的信息包括
- isa指针
- 其他成员变量
既然instance对象结构体中不包括类的方法信息 ;那我们该如何访问类的方法信息 ?
class对象 我们通过class方法或runtime方法得到一个class对象。class对象也就是类对象
每一个类在内存中有且只有一个class对象。
class对象在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的属性信息(@property),类的成员变量信息(ivar)
- 类的对象方法信息(instance method),类的协议信息(protocol)
所以instance对象的isa指针指向class对象来访问方法信息 ;
成员变量的值时存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。
同样的class对象的isa指针指向的是它的元类对象 ;
元类对象 meta-class
在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的类方法的信息(class method)
meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。
class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
1.当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了。
2.当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象
3.当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象superclass指针。
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用,同样如果Person发现自己没有响应的对象方法,又会通过Person的superclass指针找到NSObject的class对象,去寻找响应的方法
这里我猜想可能与runtime的消息流程有关,先从isa指针寻找,找不到就寻找到父类中,然后从父类的isa指针寻找 ;下面寻找父类的类方法也是一样的 ;
4.当类对象调用父类的类方法时,就需要先通过isa指针找到meta-class,然后通过superclass去寻找响应的方法
对isa、superclass总结
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class,基类的isa指向自己
- class的superclass指向父类的class,如果没有父类,superclass指针为nil
- meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
- instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类
- class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类