一、NSObject 对象
1、NSObject 的底层实现
我们在 iOS 开发过程中,所编写的 Objective-C 代码,其底层实现都是使用的 C\C++ 代码,所以 Objective-C 的面向对象都是基于 C\C++ 的数据结构实现的。
例如,对于以下代码(main.m):
1 |
|
我们进到 NSObject.h 可以看到 NSObject 的结构如下:
1 | @interface NSObject <NSObject> { |
我们可以使用以下命令将 Objective-C 代码(main.m)转换为 C\C++ 代码(main.cpp):
1 | clang -rewrite-objc main.m |
(等价于:clang -rewrite-objc main.m -o main.cpp
)
但是有些 OC 代码要转成 C/C++ 代码时,在真机、模拟器、不同架构之间可能会存在较大差异。所以可以结合 xcrun 命令指定真机以及架构:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp |
在使用 clang 转换 OC 为 C++ 代码时,可能会遇到以下问题:
1 | cannot create __weak reference in file using manual reference |
解决方案:指定支持 ARC、运行时系统版本,比如:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp |
转换成 cpp 文件之后,我们可以找到 NSObject 的实现:
1 | struct NSObject_IMPL { |
由此,我们可以确定,NSObject 对象,实际上就是 C/C++ 的结构体。
在上面 NSObject_IMPL 结构体中,有一个 Class 类型的成员 isa,那么 Class 又是什么呢?我们可以在我们生成的 main.cpp 中看到:
1 | typedef struct objc_class *Class; |
所以,Class 是指向 objc_class 结构体的指针。
2、对象的分类
对象主要有以下类型:instance 对象、class 对象、meta-class 对象
(1) instance 对象
instance 对象即实例对象,也是开发者接触最多的一个对象。实例对象中不存储方法,只存储成员变量。instance 在内存中存储了如下信息:
- isa 指针
- 其他成员变量
isa 指针在 instance 对象中所有成员变量的第一位,是第一个成员变量。所以,isa 指针在内存中的地址就是当前 instance 对象的地址。
instance 对象中没有存储方法,方法实际上存储在 class 对象和 meta-class 对象中的。class 对象中存储了实例方法,meta-class 对象中存储了类方法。
为什么 instance 对象中不存储方法?原因很简单,一个类可能会被创建无数个实例对象,每个实例对象中都存储一份相同的方法信息,是对内存的一种浪费。而对于成员变量来说,每个实例对象的成员变量可能被赋不同值,所以是必须每个实例对象要存储自己的成员变量信息。
(2) class 对象
class 对象即类对象,每个类在内存中有且只有一个 class 对象,创建类对象的方法:
1 | Class classObject1 = [instanceObject class]; |
对于同一个 Class 类型,创建出来的所有 class 对象实际上都是同一个,可以验证一下:
1 | NSObject *instanceObject1 = [[NSObject alloc] init]; |
打印结果:
1 | instanceObject1=0x100507f30 |
可以发现,创建的多个 class 对象,内存地址都是一样的。
class 对象在内存中主要存储了如下信息:
- isa 指针
- superclass 指针
- 类的属性信息(@property)、类的对象方法信息(instance method)
- 类的协议信息(protocol)、类的成员变量信息(ivar)
- ……
此处的类的成员变量信息指的是:成员变量的描述信息(类型、变量名等)
这里需要注意的是,其中存储的方法信息为对象方法信息,而不是类方法信息。对象的方法并没有直接存储于对象的结构体中,是因为如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响。
(3) meta-class 对象
meta-class 对象即元类对象,每个类在内存中有且只有一个 meta-class 对象,创建 meta-class 对象方法:
1 | Class metaClassObject = object_getClass(classObject); // 接收的参数是类对象 |
例如:
1 | Class metaClassObject = object_getClass([NSObject class]); |
meta-class 对象和 class 对象的内存结构是一样的,只不过跟 class 对象相比有些字段是空的,例如属性信息、对象方法信息等这些相应字段 value 是空的。
meta-class 对象在内存中存储的信息主要包括:
- isa 指针
- superclass 指针
- 类的类方法信息(class method)
- ……
类方法就存储在 meta-class 对象中。
二、isa 指针和 superclass 指针
1、isa 指针
根据前面内容可以知道,当我们创建了一个 instance 对象时,instance 对象中存储了成员变量,对象方法存储在对应的 class 对象中,类方法存储在对应的 meta-class 对象中。当我们调用这个 instance 对象的对象方法或者类方法时,肯定就需要某种机制,将这些相应的对象关联起来,以保证可以正常调用到对应方法,这就是 isa 指针的作用。
instance 的 isa 指向 class
当调用对象方法时,通过 instance 的 isa 找到 class,最后找到对象方法的实现进行调用。class 的 isa 指向 meta-class
当调用类方法时,通过 class 的 isa 找到 meta-class,最后找到类方法的实现进行调用。
2、superclass 指针
(1) class 对象的 superclass 指针
对于 superclass 指针,假设有下面两个类:
1 | // Person 继承自 NSObject |
这时候会存在 3 个主要的 class 对象:Student、Person、NSObject,假设这时候,Student 的 instance 对象想要调用 Person 的某个对象方法时,对象是如何找到 Person 以及对应的对象方法呢?根据 superclass 名字也很容易猜到,superclass 指针指向自己父类的 class 对象:
- instance 对象中没有 superclass 指针。
- class 对象的 superclass 指针指向父类的 class 对象。
- 当 Student 的 instance 对象要调用 Person 的对象方法时,会先通过 isa 找到 Student 的 class,然后通过 superclass 找到 Person 的 class,最后找到对象方法的实现进行调用。
(2) meta-class 对象的 superclass 指针
还以上面 Student 、Person 为例,如果 Student 的 class 要调用 Person 的类方法时,那如何找到对应方法呢?
meta-class 对象的 superclass 指针指向父类的 meta-class 对象,当 Student 的 class 要调用 Person 的类方法时,会先通过 isa 找到 Student 的 meta-class,然后通过 superclass 找到 Person 的 meta-class,最后找到类方法的实现进行调用。
3、总结
上面是一个非常经典的图,从上图可知:
- 在实现中,Root Class 就是 NSObject
- NSObject 的 meta-class 父类是 NSObject 类
- instance 的 isa 指向 class
- class 的 isa 指向 meta-class
- meta-class 的 isa 指向基类的 meta-class
- 基类的 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 找父类
三、isa 的结构及实现
1、isa 的结构
在 Objc 2.0 之前,objc_class 源码如下:
1 | struct objc_class { |
2006 年发布 Objc 2.0 之后,objc_class 的定义如下:
1 | typedef struct objc_class *Class; |
objc_class 继承于 objc_object。所以在 objc_class 中也会包含 isa_t 类型的结构体 isa。至此,可以得出结论:Objective-C 中类也是一个对象。在 objc_class 中,除了 isa 之外,还有 3 个成员变量,分别是:
- super_class
指向当前类的父类 - cache
用于缓存指针和 vtable,加速方法的调用 - bits
就是存储类的方法、属性和遵循的协议等信息的地方
对 objc_class 进行简化,其结构体实际上就是下面样子:
1 | struct objc_class : objc_object { |
前面提到 NSObject 中也有个 isa,NSObject 中 isa 与 objc_class 中 isa 关系如下图:
objc_class 中的 isa 是 isa_t 类型的,isa_t 是一个联合体(union)
联合体
联合体与结构体非常类似,主要区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
位域
位域定义与结构定义相仿,其形式为:
struct 位域结构名
{ 位域列表 };
其中位域列表的形式为:
类型说明符 位域名:位域长度
例如:
1 | struct bits |
说明 data 为 bits 结构体变量,共占两个字节(1 个字节存储 8 位无符号数)。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。
isa_t 的定义如下:
1 | union isa_t { |
也就是说,在 64 位环境下,isa_t 就是下面这个样子(后续都将以 64 位为例):
1 |
|
所以,也可以理解为 isa_t 的结构是联合体+位域。
isa_t 内存结构如下图:
各参数解释如下:
2、isa_t 的实现
以下是 objc_object 结构体的部分内容:
1 | struct objc_object { |
当系统通过 alloc 为一个对象分配内存时,会同时初始化 isa。对于对象来说,isa 的基础作用就是将对象和类进行绑定,告诉系统对象的归属。
初始化 isa 主要是调用下面这两个方法(已精简处理):
1 | inline void |
可以看到,initIsa 中间首先有个断言,如果对象是 Tagged Pointer 就不继续执行了,这里涉及到了 Tagged Pointer,那什么是 Tagged Pointer 呢?
以 NSNumber 对象为例,一个 NSNumber 对象,值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。
如果没有 Tagged Pointer 对象,32 位和 64 位下这个 NSNumber 内存占用情况如下:
可以看出,如果没有 Tagged Pointer 对象,64 位设备相较于 32 位设备,NSNumber、NSDate 这样的对象所占用的内存会翻倍。不仅仅是内存上的浪费,作为一个对象,我们还需要在堆上为其分配内存、维护它的引用计数、管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了 Tagged Pointer 的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。
由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。
所以可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer 对象之后,64 位 CPU 下 NSNumber 的内存占用变成了下面这样:
所以,Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
接下来看一个 Tagged Pointer 例子,下面代码,执行结果是什么?
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
执行上面代码发现会必现 crash,因为这里 self.name 是正常 NSString 对象,其本质在 setter 方法中必然会对旧值有 release 操作,由于是在子线程赋值,很大概率出现同时 release,这就导致 crash 发生。
而下面代码执行却不会出现 crash:
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
因为在上面代码中,给 self.name 赋的值是一个 Tagged Pointer,不需要 release,所以不会出现前面同时 release 引发的 crash。
以上就是关于 Tagged Pointer 相关内容,接下来回到对 initIsa 方法的分析,再看下 initIsa 函数的源码:
1 | inline void |
可以看到在判断了非 Tagged Pointer 之后,又判断了 nonpointer 是否为 true,前面提到 nonpointer 含义是是否开启了 isa指针 优化,1 代表优化过,0 代表未优化。
根据源码也可以看到,在早期,也就是未进行 isa 指针优化前,isa 直接指向了 class 的地址。优化后,isa 内部存储了更加多的信息,并且不再直接指向 class 地址(isa 地址与 ISA_MASK 进行位运算后,才是 class 地址)。
在 initInstanceIsa 方法中,调用 initIsa 方法的时候 nonpointer= true,所以我们可以将方法简化为:
1 | inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) |
ISA_MAGIC_VALUE = 0x000001a000000001ULL 对应二进制是11010000000000000000000000000000000000001,当对 bits 赋值之后,isa_t 的变化如下图:
从上图可知,这一步对 nonpointer 和 magic 进行了赋值。可以看到 nonpointer 被赋值为 1。
在设置 nonpointer 和 magic 值之后,会设置 isa 的 has_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。
1 | isa.has_cxx_dtor = hasCxxDtor; |
最后就要将当前对象对应的类指针存入 isa 结构体中了:
1 | isa.shiftcls = (uintptr_t)cls >> 3; |
由于类的指针要按照字节(8 bits)对齐内存(关于字节对齐下篇文章会专门分析),其指针后三位肯定都是没有意义的 0。将当前地址右移三位的就是为了将 Class 指针中无用的后三位清除,以减小内存的浪费,为 isa 留下更多空间用于性能的优化。
对于 isa 和对应的 Class 对象之间关系,我们都知道可以使用 object_getClass(obj) 获取 Class 对象,object_getClass(obj) 源码如下:
1 | Class object_getClass(id obj) |
由以上源码可知,isa 地址,经过与 ISA_MASK 进行位运算,就是对应 class 或 meta-class 地址。
实际上,从 64bit 开始,isa 不再直接指向 class 或 meta-class 地址,而是需要 isa 地址与 ISA_MASK 进行一次位运算后,才是 class 或 meta-class 的地址。
3、拾遗:objc_class 中的 cache、bits
再回头看 objc_class 的结构:
1 | struct objc_class : objc_object { |
其中 bits 就是存储类的方法、属性和遵循的协议等信息的地方,在 objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。
1 | class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags |
它为我们提供了便捷方法用于返回其中的 class_rw_t * 指针:
1 | class_rw_t* data() { |
将 bits 与 FAST_DATA_MASK 进行位运算,转换成 class_rw_t * 返回。ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:
1 | struct class_rw_t { |
其中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议:
1 | struct class_ro_t { |
使用下图表示其关系:
对于 objc_class 结构体中的 cache,作用主要是为了优化方法调用的性能。当对象 receiver 调用方法 message 时,首先根据对象 receiver 的 isa 指针查找到它对应的类,然后在类的 methods 中搜索方法,如果没有找到,就使用 super_class 指针到父类中的 methods 查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有 20% 的方法经常被调用,占总调用次数的 80%。所以使用 Cache 来缓存经常调用的方法,当调用方法时,优先在 Cache 查找,如果没有找到,再到 methods 查找。
- 本文章采用 知识共享署名 4.0 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。