一、isa_t 的结构
下面是我们对类中的 isa 的探究,来看一下 isa 的源码实现:
objc_object 的 isa 是一个联合体,不是一个指针。这时候有个概念:非指针isa(nonPointerIsa)。
二、ISA_BITFIELD
isa_t 中有一个结构体成员,结构体成员中有一个宏定义:ISA_BITFIELD,点进去看一下:
在iOS中的架构是arm64,框红的地方,是真机模式下的。并且,ISA_BITFIELD 用到了位域。
以下是各个成员的注释:
nonpointer: 表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。has_assoc: 关联对象标志位,0没有,1存在。has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced: 指对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。unused: 标志对象是否正在释放内存。has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量存储进位。extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到has_sidetable_rc。
先对 shiftcls 有一个概念的认识,在下面的探索中,主要会用到它。
三、isa 推导 class
通过 [SHPerson class] 可以拿到 SHPerson 的类对象。每个对象都有一个 isa,这个 isa 指向的是类对象,但通过 lldb 打印 SHPerson 实力对象的内存分布发现,第一块内存地址(也就是isa)和 [SHPerson class] 拿到的类对象的内存地址不一样。
isa: 0x000021a100008175
class对象: 0x0000000100008170
这是因为苹果做了一个掩码的操作,在 isa.h 文件中有一个 ISA_MASK 的宏定义(系统架构不一样,值也不一样,调试的时候根据运行环境选取不同的值),用 isa 的地址 & 上 ISA_MASK,就可以得出 class 对象的内存地址。
1. ISA_MASK 获取 calss 对象的内存地址
lldb打印如下:
(lldb) x/4gx person
0x1007170c0: 0x000021a100008175 0x0000000000000000
0x1007170d0: 0x0000000000000000 0xce7ed407c2c98f1d
(lldb) p/x [SHPerson class]
(Class) $3 = 0x0000000100008170 SHPerson
(lldb) p/x 0x000021a100008175 & 0x0000000ffffffff8ULL
(unsigned long long) $10 = 0x0000000100008170
isa 的地址 & 上 ISA_MASK后得出地址值和 [SHPerson class] 得出的地址值是一样的,都是 0x0000000100008170。
2. isa 的位运算获取 calss 对象的内存地址
通过分析 isa_t 得知 ISA_BITFIELD 的 shiftcls 是用来存储类指针的值,所以只要拿到 shiftcls,看看里面存放的值就可以知道类对象的内存地址。
观察到在 arm64(真机模式) 下,shiftcls 占33位,在前面共有3位(1+1+1),后面共有28位(6+1+1+1+19)。
所以,运算的步骤如下:
- 左移3位 -- 顶掉前面的3位,这时后面会空出3位,不足用0补。
- 右移31位 -- 后面本来有28位,加上用0补的3位,一共31位
- 左移28位 -- 将 shiftcls 归位。
平移的数位不是固定的,根据 ISA_BITFIELD 中的规定得算出来的。
lldb打印如下:
(lldb) x/4gx person
0x100722a40: 0x000021a100008175 0x0000000000000000
0x100722a50: 0x000000010080db90 0xcdb2bd35671d6859
(lldb) p/x 0x000021a100008175 >> 3
(long) $1 = 0x000004342000102e
(lldb) p/x 0x000004342000102e << 20
(long) $2 = 0x4342000102e00000
(lldb) p/x 0x4342000102e00000 >> 17
(long) $3 = 0x000021a100008170
(lldb) p/x [SHPerson class]
(Class) $4 = 0x0000000100008170 SHPerson
(lldb)
通过这种方式拿到类对象的内存地址和[SHPerson class]拿到的内存地址也是一样的,都是 0x0000000100008170。