iOS 类的底层探索(上)
上篇内容我们研究了对象的本质,并通过对象的 isa 指针找到了类,那么类的本质是什么?
1.实例、类、元类
对象的 isa 指针中的 shiftcls 指向了对象所对应的类。
可以通过以下代码进行验证:
获取类对象的三种方式:
可以得出结论:类对象在内存里有且只存在一个
元类
源码搜索 struct objc_class发现类的本质是 objc_class,继承自objc_object,一切皆对象,类也有一个 isa 指针,那么类的 isa 指针指向哪里?
打印一下类的 isa 指向,发现 NSObject 类的 isa 指针竟然指向 NSObject,但是通过内存地址可以发现,这两个不是同一个东西:
这里介绍一下元类,元类的编译和创建都是由编译器自动完成。
元类保存了类方法的列表,当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有则该元类会向它的父类查找该方法,直到一直找到继承链的头
- 总结:对象 isa -> 类,类 isa -> 元类
根元类
在上面的案例中,我们通过类对象的 isa 找到了元类对象,那么元类对象的 isa 又指向哪里?
通过打印打印发现,元类的 isa 竟然指向
NSObject!,下面验证一下 NSObject类的地址是否和这个地址相同
结果发现指向并不相同,那么
NSObject类的 isa 又指向哪里呢?
我们发现 user 元类的 isa 指针和
NSObject 类的 isa 指针地址相同,都指向 NSObject 元类,因为 NSObject 是所有对象的根类,所以将 NSObject 元类称为根元类。
那么根元类的 isa 又指向哪里呢?
发现
根元类的 isa 指向自己
- 总结:
元类的 isa -> 根元类,NSObject 元类 -> 根元类, 根元类 isa -> 自己
isa 指针分析总结
2.superclass
查看 objc_class 源码实现:
发现
superclass 在类地址的第二个 8 字节里,因为第一个 8 字节是 isa 指针
类 superclass
创建 NetworkUser 继承自 User,很容易知道 superclass 的指向,即:
NetworkUser -> User -> NSObject -> nil
验证如下:
元类 superclass
类的 superclass 很容易理解,那么元类的 superclass 指向如何?下面进行验证:
从上面的验证可以发现,
NetworkUser元类的superclass指向User,User的superclass指向NSObject,NSObject的superclass指向NSObject 根类。通过验证可以确定,这里的User为元类,并且其superclass指向的是NSObject 元类。
- 总结:
NetworkUser 元类 -> User 元类 -> NSObject 元类 -> NSObject 根类
superclass 总结
看一下苹果官方对 isa 和 superclass 的解释:
3.类结构分析
先来看一下类的 OC 层和 C\C++ 层之间的关系:
- 使用
clang命令查看 C++ 源码,发现NSObject为objc_object类型
类结构的初步分析:
isa和superclass前面已经做了分析,这两个指针确定了类之间的关系cache中存储了运行中的一些缓存信息,比如消息缓存。从objc_class提供的方法可以看出,在获取一些数据时,优先从缓存中获取,如果没有缓存,则从bits中获取。目的是保证响应速度
bits, 类中相关的数据信息,比如方法列表、属性列表都存储在了bits中
cache_t cache
见这篇文章
class_data_bits_t bits
class_data_bits_t 是一个结构体,objc_class 为其提供了data() 和 setData() 方法:
// objc_class
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// class_data_bits_t
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
在类的实现过程中,会创建一个 class_rw_t, 会将 MachO 文件中的 class_ro_t 数据拷贝到 class_rw_t 中,并设置到类中。在 class_rw_t 中提供了一些方法,可以获取类的属性、协议、分类、方法等
4.类结构探索
定义一个 User 类,包括一个成员变量 city,两个属性name 和 age,一个实例方法instanceMethod(),一个类方法classMethod():
@interface User : NSObject {
NSString *city;
}
@property (nonatomic, strong) NSString* name;
@property (nonatomic, assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation User
- (void)instanceMethod {
NSLog(@"%s", __func__);
}
+ (void)classMethod {
NSLog(@"%s", __func__);
}
@end
打印 User 类的地址,并用 x/6gx 打印相应结构,由前面源码结构可以知道,isa 占用 8字节,superclass 占用 8 字节,cache 占用 16字节,所以只要类地址向左平移动 32 字节,就可以得到 bits 的首地址,下面进行操作(需要在源码里面操作):
此时
bits 不为空,所以类中已经存储了相关信息,上面说到 class_data_bits_t 结构里提供了 data() 方法,用于获取 class_rw_t, class_rw_t 在类初始化过程中已经被创建了,并且 class_rw_t 的相关数据来自 MachO 文件中的 ro 数据
下面开始获取 class_rw_t 内容:
在
class_rw_t 可以获取相关方法、属性等内容
探索方法
查看 class_rw_t 源码,内部提供了 const method_array_t methods() 方法获取方法列表:
至此就可以拿到方法列表了,
method_list_t, 并且里面有 count = 6 个方法,下面遍历改列表,输出第一个方法:
发现打印为空,此时查看 method_t 结构体的底层实现,发现内部有一个 big 结构体,包含了方法编号 SEL,方法 type 和 方法实现:
打印 big 信息,发现报错了:
原因是不同架构下的存储方式,即大端存储和小端存储,intel 芯片是大端存储,apple 芯片是小端存储,具体见附加内容
所以在 apple 芯片下要用 small 来获取方法信息:
发现 small 打印出来的仍然看不懂,这时候就要用
getDescription() 来获取:
打印出来发现属性会自动生成
getter 和 setter 方法,那么成员变量、类方法在哪?cxx_destruct 是析构函数,用来释放结构体内存
探索属性
和探索方法类似,通过调用 class_rw_t 中的 properties() 方法,可以获取属性列表:
成功找到了属性
name 和 age,那么成员变量在哪?,name 和 age 对应的成员变量在哪?
class_ro_t
在 class_rw_t 源码中并没有发现关于成员变量的方法或者属性,发现有个 const class_ro_t *ro()方法,有个结构体 class_ro_t,在 iOS 中,用 ivar 表示成员变量,发现其中有个属性 const ivar_list_t * ivars;,是个常量,在编译之初就被确定,并且在运行时不会被修改,用 ro() 打印出来:
在
class_ro_t 中,包括方法列表、协议列表、变量列表,那么成员变量是否在里面?打印一下 ivars:
结论是:成员变量在 class_ro_t 里
为什么成员变量在类里,而成员变量的值存放在实例对象里?
因为类的本质是一个结构体,可以理解为一个模版,模版里面包含了成员变量、方法、属性、协议等,而成员变量的值在不同的对象里是不同的,所以把值放在实例对象里
类方法
前面说到元类里面保存了类方法,下面进行验证,
跟踪消息慢速方法查找,发现此时的cls已经不是类了,而是元类
再用同样的方法对元类进行操作,获取
class_rw_t 中的 methods:
- 补充
class_copyMethodList方法
该方法的内部实现是const auto methods = cls->data()->methods();,可以获取类中的方法列表。当传入的分别是类和元类时,会获取不同的方法列表,从而也可以区分出实例方法和类方法的存储位置的不同。
总结:对象是类的实例,类是元类的实例,方法都存储在各自的类中。
5.拓展
1.大端 小端
-
大端对齐:高位字节存放内存的低地址段,低位字节存放内存的高地址段。
-
小端对齐:高位字节存放内存的高地址段,低位字节存放内存的高低地址段。
0x12345678 0x10001 0x10002 0x10003 0x10004
大端 0x12 0x34 0x56 0x78
小端 0x78 0x56 0x34 0x12
2.rw 和 ro 的区别
3.面试问题:元类的作用?
主要的目的是为了复用消息机制。
在 OC 中调用方法,其实是在给某个对象发送某条消息。消息的发送在编译的时候编译器就会把方法转换为
objc_msgSend这个函数。
id objc_msgSend(id self, SEL op, ...)这个函数有两个隐式的参数:消息的接收者,消息的方法名。通过这两个参数就能去找到对应方法的实现。
objc_msgSend函数就会通过第一个参数消息的接收者的isa指针,找到对应的类,如果我们是通过实例对象调用方法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对 象的元类对象,然后再通过SEL方法名找到对应的imp,然后就能找到方法对应的实现。那如果没有元类的话,那这个
objc_msgSend方法还得多加两个参数,一个参数用来判断这个方法到底是类方法还是实例方法。一个参数用来判断消息的接收者到底是类对象还是实例对象。消息的发送,越快越好。那如果没有元类,
在objc_msgSend内部就会有有很多的判断,就会影响消息的发送效率。所以元类的出现就解决了这个问题,让各类各司其职,
实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,符合设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。所以这个元类的出现,最大的好处就是能够复用消息传递这套机制。不管是什么类型的方法,都是同一套流程。