一、isa
- OC中, 每一个对象都有一个isa指针
- 实例对象的isa指向类对象, 类对象的isa指向元类对象, 元类对象的isa指向基类的元类对象, 基类元类对象的isa指向基类元类对象本身

- 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
- 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息

- 想要通过isa获取类对象和元类对象的地址, 就需要使用
isa & ISA_MASK

那么arm64之后的isa究竟是什么样的?
二、使用char类型的变量存储三个bool变量的值
- 准备代码, 定义
Person类, 并添加三个属性tall、rich、handsome

- 可以如下使用
Person类

- 在上面的代码中, 三个属性分别占用一个字节, 所以一共占用了三个字节
- 而bool的结果只有
0和1, 实际上用1bite的空间就可以存储bool值(1字节=8bite) - 所以我们可以设置一个
1字节的成员变量来存储这三个变量的值 - 修改
Person中的代码, 移除三个变量, 添加三对get和set方法, 添加一个char类型的成员变量_tallRichHandsome

char类型在内存中只占用一个字节0b0000 0000, 我们可以使用最后面的三个bite位来存储tall、rich和handsome的值
1、存值
- 首先我们初始化
_tallRichHandsome = 0b00000000

- 接着在
setTall:方法中, 将传进来的tall存到_tallRichHandsome中最低位中 - 当
tall的值是YES时, 我们将_tallRichHandsome的最低位设置为1 - 想要只改变二进制数据中的某一位为1, 只需要使用逻辑或(
|)即可, 例如
0110 0100
|0000 0001
---------- // 逻辑或, 只要不为1, 结果就是1
0110 0101
- 所以
tall的值是YES时, 代码如下
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome |= 1;
}else {
}
}
- 当tall的值为0时, 我们需要将
_tallRichHandsome的最低位设置为0, 其他位不变, 此时我们需要使用逻辑与, 例如
0110 0101
|1111 1110
---------- // 逻辑与, 只要全是1, 结果才是1
0110 0100
- 所以
tall的值是NO时, 代码如下
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome |= (0b00000001);
}else {
_tallRichHandsome &= (0b11111110);
}
}
- 同理,
rich和handsome存在第二位和第三位, 代码如下

- 我们知道
1向左位移可以获取如下的值
// 位移
1<<0 = 0b00000001
1<<1 = 0b00000010
1<<2 = 0b00000100
- 通过取反又能获取下面的值
// 取反
~0b00000001 = 0b111111110
~0b00000010 = 0b111111101
~0b00000100 = 0b111111011
- 所以上面代码可以如下简化

2、取值
- (BOOL)isTall方法中, 我们需要将_tallRichHandsome最低位取出来- 而取值, 只需要将
二进制数据 & 0b00000001即可, 例如
1001 1110
&0000 0001
----------- // 逻辑与, 只有全是1, 结果才是1
0000 0000
- 通过这种方式, 就可以取出
_tallRichHandsome中的最低位
- (BOOL)isTall
{
return _tallRichHandsome = (1<<0);
}
-
同理,
isRich和isHandsome方法也是一样 -
通过逻辑与取出的值, 每一位上如果是
1, 那么结果肯定大于0, 例如0b00000001、0b00000010和0b00000100 -
此时说明存的值是
YES, 否则就是NO -
所以存值取值的代码如下

- 再次运行程序, 有如下结果

- 我们可以发现
handsome的结果是4, 这是因为取出来的数是0b00000100 - 只要在取出的结果前加上两个
!即可, 此时运行的结果就是1

- 我们可以多试验几次, 可以发现结果没有问题, 此时说明值已经存取成功
三、使用位域存储三个bool变量的值
- 我们可以使用位域来存储是三个bool变量的值, 代码如下

- 定义了
_tallRichHandsome结构体,_tallRichHandsome中有三个成员变量, 这三个变量因为使用了位域, 所以每个变量只占用一个bite, 所以_tallRichHandsome的大小只有1个字节

-
因为使用了位域, 所以
tall只占用第一位,rich只占用第二位,handsome只占用第三位 -
所以代码就可以如下改进, 这样看就方便了许多

- 运行代码, 有如下结果

- 我们可以看到
rich的结果是-1, 并不是1 - 这是因为, 从位域取出来的
rich的结果只是0b1, 而系统为了补全二进制位, 将1当做了符号位, 就把rich变成了0b11111111, 所以结果就是-1

- 想要获取正确的结果, 只需要将三个变量分别占用
两个bite即可, 此时再取出的就是0b01或者0b00, 当再次补齐的时候, 符号位就是0而不是1

四、使用共用体存储三个bool变量的值
- 修改
Person.m中代码,_tallRichHandsome是一个共用体

- 可以看到
_tallRichHandsome中有一个char类型变量bits和一个结构体 bits占用一个字节,而结构体使用了位域, 也只占一个字节, 所以共用体_tallRichHandsome只占用一个字节- 使用
_tallRichHandsome存储三个bool值时, 与上面的二、使用char类型的变量存储三个bool变量的值方法相同

- 可以将共同点换成宏定义

- 然后使用宏替换掉位移代码

- 执行程序, 可以发现结果正确

五、查看isa源码
- 查看runtime源码, 可以看到isa的定义, 其中arm64就是iOS使用的

- 通过整理可以得出下面的代码

- 自从arm64开始,
isa就是一个共用体, 里面存储多个值, 而对象的地址也只是其中的一部分而已 - 在
isa共用体中,shiftcls描述的就是类型地址在bits中存储的位置, 即4-36位(isa共用体中的结构体没有实际意义,只是用来描述内存中每一部分的作用) - 已知
isa & ISA_MASK才能获取到类对象和元类对象的地址 - 而
ISA_MASK的值就是0x0000000ffffffff8, 具体的二进制是
0b00000000111111111111111111111111111111111000
- 所以
isa & ISA_MASK的值, 二进制结尾必然是三个0 - 我们打印一下
NSObject和Person的类对象以及元类对象的地址

- 可以发现类对象以及元类对象的地址中, 最后一位总是
0或8, 当16进制转为2进制时,8会转为1000,0会转为0000 - 之所以有这样的结果, 就因为类对象以及元类对象的地址存储在共用体
isa的4-36位中
总结: 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
六、isa位域解释
nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息
has_assoc
是否有设置过关联对象,如果没有,释放时会更快
has_cxx_dtor
是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
shiftcls
存储着Class、Meta-Class对象的内存地址信息
magic
用于在调试时分辨对象是否未完成初始化
weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快
deallocating
对象是否正在释放
extra_rc
里面存储的值是引用计数器减1
has_sidetable_rc
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中