小码哥iOS学习笔记第十一天: isa详解

1,917 阅读7分钟

一、isa

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

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

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

那么arm64之后的isa究竟是什么样的?

二、使用char类型的变量存储三个bool变量的值

  • 准备代码, 定义Person类, 并添加三个属性tallrichhandsome

  • 可以如下使用Person

  • 在上面的代码中, 三个属性分别占用一个字节, 所以一共占用了三个字节
  • 而bool的结果只有01, 实际上用1bite的空间就可以存储bool值(1字节=8bite)
  • 所以我们可以设置一个1字节的成员变量来存储这三个变量的值
  • 修改Person中的代码, 移除三个变量, 添加三对getset方法, 添加一个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);
    }
}
  • 同理, richhandsome存在第二位和第三位, 代码如下

  • 我们知道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);
}
  • 同理, isRichisHandsome方法也是一样

  • 通过逻辑与取出的值, 每一位上如果是1, 那么结果肯定大于0, 例如0b000000010b000000100b00000100

  • 此时说明存的值是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
  • 我们打印一下NSObjectPerson的类对象以及元类对象的地址

  • 可以发现类对象以及元类对象的地址中, 最后一位总是08, 当16进制转为2进制时, 8会转为1000, 0会转为0000
  • 之所以有这样的结果, 就因为类对象以及元类对象的地址存储在共用体isa4-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的类的属性中