Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同。
Objective-C的动态性是由Runtime API来支撑的。Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写。
一、isa详解
要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针。
前面的章节有简单介绍过isa指针,比如isa的指向(实例对象isa -> 指向父类对象isa -> 元类对象isa -> 基类对象isa -> 指向基类对象自己),以及64bit之前isa是直接指向内存的,64bit之后的isa指向地址是需要经过位运算得出的。
在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。
从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
疑问:64bit后的isa为什么要经过位运算(ISA&ISA_MASK
)?这样设计的目的是什么?
1.1. 按位存储
示例代码:
1 | @interface DBPerson : NSObject |
上面代码中的person对象至少需要16个字节(isa(8) + red(1) + green(1) + blue(1) = 11,由于内存对齐是8,所以最终打印16个字节)。
DBPerson类中有三个BOOL类型的属性,每一个属性的值只有0和1两种结果,但占用了1个字节。这种情况下1个二进制位就可以表示1个属性的值,三个BOOL类型属性只需要1个字节中的3个二进制位就可以表示各自的值,相比之下更加节省内存。按照这个思路,我们尝试改进上面的代码。
char类型只占1个字节,因此使用char记录三个BOOL属性的值。例如:char类型变量color
存储的二进制是0b0000 0000
,三个属性的值在低二进制位存储,假设red = 1,green = 0,blue = 1
,变量color
最终存储的就是0b0000 0110
。
怎样操作二进制位呢?使用位运算可以让对应二进制位的值发生改变。
1 | @interface DBPerson : NSObject |
上面代码就把三个变量值合在一个字节中存储了。
注意:上面代码仅仅是为了研究isa底层,平时编写代码时不建议这样写,但是操作系统等底层源码会大量使用上面的写法,因为比较高效。
1.2. 位域
如果要存储的变量比较多,上面的代码就变得很难维护。使用位域可以解决这个问题,并且代码更加简洁。
位域格式:变量类型 变量名 : 变量占用位数(注意:变量类型只是访问类型,和实际存储没有关系)。
1 | struct { |
如上定义的_color
结构体里面有3个变量,分别占用1位,一共占用1个字节,变量在内存中的布局是0b0000 0bgr
。
1 | @interface DBPerson () |
上方代码中green
如果是YES,那么green的十六进制就是0xff
,二进制是0b11111111
,十进制是255,表示的是-1(有符号)。判断BOOL变量只需要判断是否有值即可,因此也需要加上!!
。
上面的代码使用位域可读性比较高,但是没有位运算的效率高。共用体不仅可以提高代码可读性/可维护性,还能使用位运算提高运行效率。
1.3. 共用体
共用体的意思就是结构体所有成员共享一段内存。
1 | union Date { |
int占用4个字节,char占用1个字节,所以共用体Date占用4个字节,成员year和month共用这4个字节。
使用共用体需要注意:由于是共用一段内存,存储时会覆盖之前存储的内容。
共用体结合位域:
1 | union { |
在共用体中,结构体的成员仅仅是为了可读性(red占1位,green占1位,blue占1位),没有任何实际意义(可以理解为:结构体仅仅是为了表示共用体的内存布局,可以不写),实际存储的内容还是自己控制的。
上面代码的含义:创建一个共用体变量_color
,里面有一个成员变量char bits
,相当于整个共用体占用1个字节,内存布局是0b0000 0bgr
。
使用共用体把之前1.1中DBPerson的代码进行改写:
1 | #define DBRedMask (1<<0) |
在objc源码中,isa是isa_t
类型的共用体。isa占用8个字节,在64bit之后,bits里面不仅存储着类、元类对象的内存地址,还有其他信息,这些信息共享这8个字节的内存(这就是64bit后isa的优化 — 充分利用存储空间)。
1 | union isa_t |
nonpointer
:0代表普通的指针,bits存储着Class、Meta-Class对象的内存地址。1代表isa优化过,使用位域存储更多的信息。has_assoc
:是否有设置过关联对象(如果设置过,即使又被设为nil,也算设置过),如果没有,释放时会更快。has_cxx_dtor
:是否有C++的析构函数(.cxx_destruct,类似于OC类中的dealloc
方法),如果没有,释放时会更快。shiftcls
:存储着Class、Meta-Class对象的内存地址信息。magic
:用于在调试时分辨对象是否未完成初始化。weakly_referenced
:是否有被弱引用指向过(如果被引用过,即使弱引用指针置为nil,也算被弱引用过),如果没有,释放时会更快。deallocating
:对象是否正在释放。has_sidetable_rc
:引用计数器是否过大导致无法存储在isa中。如果为1,那么引用计数会存储在一个叫SideTable的类的属性中。extra_rc
:里面存储的值是引用计数减1。
1.4. ISA_MASK
在64bit后,isa需要和ISA_MASK进行按位与(&)计算才能获得真实的类/元类地址。因为使用的是共用体的其中33位存储类、元类的地址值,所以需要进行按位运算才能取出(这也是使用ISA_MASK的原因)。
1 |
ISA_MASK对应的二进制:
ISA_MASK的二进制中有33个1,这33位就是用来存储shiftcls
的。最后3位是0,0和任意位进行按位与计算,结果都是0。所以在OC中,对象的二进制地址值最后三位一定是0(从共用体中结构体的定义也可以看出shiftcls
前面有3个位存放其他信息)。
假设一个对象的地址值是0x10051f360
,由于1个16进制位代表4个二进制,所以后3位一定是0。假设一个元类对象的地址值是0x100008228
,最后一位是8,十六进制的8对应的二进制就是1000,所以后3位也是0。
面试题:对isa的理解?
解答:isa是一个指针,实例对象isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向基类对象。在arm64之前,isa就是一个普通的指针,直接存储了类和元类的地址。但是在arm64之后,对isa进行了优化,采用共用体结构把一个64bit的数据分开存储不同的信息,而其中的33位是用来存储类和元类的具体地址。如果要想取出类和元类的地址,就需要对isa的地址和ISA_MASK(ISA_MASK是isa的掩码,确定了类/原来在共用体中存储的位置)进行按位运算,计算出的结果就是真实的类/元类地址。另外由于isa在共用体中存储位置的原因,导致计算的二进制内存地址后3位一定是0(十六进制下以0和8结尾的内存地址)。