现在对象在内存中已经分配好内存空间了,但对象和类是怎么关联上的呢,这就是 isa 的工作了。
isa 联合体
我们可以看一下对象的表现形式:
struct objc_object { |
所以每一个对象必然有一个 isa。
然后我们看一下 isa 的结构:
union isa_t { |
可以看到,isa 是一个 联合体 。
isa 的结构
首先我们来分析一下 isa 的结构:
isa 有三个成员:Class 、 bits 和一个结构体。
以下以 64 位架构为例分析:
Class
typedef struct objc_class *Class; |
Class 是一个结构体指针,所以 Class 占 8 字节。
bits
typedef unsigned long uintptr_t; |
bits 是一个无符号长整形,占 8 个字节。
结构体
struct { |
结构体的内容是一个宏定义,宏定义在编译时替换成定义好的内容,这样就可以区分不同架(如 __x86_64__ 与 __arm64__)。

其实这里的实现,就是上面我们提到的位域。
从图中我们可以看出,两个架构上结构体的字段相同,只是分布不同。
结构体的大小为 8 字节,即 64 bit。
arm64: 1 + 1 + 1 + 33 + 6 + 1 + 1 + 1 + 19 = 64__x86_64__: 1 + 1 + 1 + 44 + 6 + 1 + 1 + 1 + 8 = 64
下面给出isa图解:

关联 isa
现在我们来看一下关联 isa 的代码实现:
inline void |
代码稍微精简了一下。注意这里的 SUPPORT_INDEXED_ISA 宏:
查阅到 资料,__ARM_ARCH_7K__ >= 2 应该是表示手表的宏。
__ARM_ARCH_7K__ 架构上的 isa 的位域结构体具体的字段有所不同,分布也不同,这里就不在展开了。
在 isa 的实现代码里,一般继承 NSObject 的类都支持 isa 指针优化,如果不支持,isa 的 64 位都用来保存 class 或者 metaclass 的内存地址。
支持指针优化的 isa:
isa作为一个联合体,对bits赋值后,isa的值为(程序运行在模拟器,所以是基于__x86_64__的架构):newisa.bits = ISA_MAGIC_VALUE;
/**
* 0x 0b
* 0x001d800000000001ULL 0b0000000000011101100000000000000000000000000000000000000000000001
*/此时位域的最低位即
nonpointer为1,表示支持指针优化。hasCxxDtor放在联合体位域的has_cxx_dtor,此时此处指为false,所以值为0newisa.has_cxx_dtor = hasCxxDtor;
/**
* 0x 0b
* 0x001d800000000001ULL 0b0000000000011101100000000000000000000000000000000000000000000001
*/cls的内存地址放在isa的shiftcls区间newisa.shiftcls = (uintptr_t)cls >> 3; // 此处 cls 的值为 0x00000001000029f0
/**
* 0x 0b
* 0x001d8001000029f1 0b0000000000011101100000000000000100000000000000000010100111110001
*/注意此时的
shiftcls对cls的地址进行了右移3位的计算,所以后面再去的时候,也是需要计算的。或许在这里你会有个疑问,地址经过计算之后,存储的地址不会发现变化吗?
这就是实现精妙的地方了 – 还记得上面讲的字节对齐吗,OC 对象的内存地址首先进行了
8字节的对齐,那么对象的内存地址肯定是8的倍数。虽然针对的是对象,但是类和元类在编译时创建同样也是经过了8字节对齐的。所以内存地址也是8的倍数。8的 二进制表示为0b1000,右移3位之后位0b1,所以后面再取值的时候,在左移3位补齐后面的0就能得到真正的地址。我们二进制计算一下:
(lldb) p/t 0x00000001000029f0
(long) $0 = 0b0000000000000000000000000000000100000000000000000010100111110000
(lldb) p/t $0 >> 3
(long) $1 = 0b0000000000000000000000000000000000100000000000000000010100111110
(lldb) p/t $1 << 3
(long) $2 = 0b0000000000000000000000000000000100000000000000000010100111110000
(lldb) po $0 == $2
true现在
cls的地址已经放到isa的shiftcls段里面了,在arm64里面占了33位,但是cls的地址是64位能存下吗?类和元类只需要创建一个,而且是在编译时期就已经完成了的(我们可以从
machO文件中看到这些类信息,所以他们在编译时期就已经确定了)。但是从实际问题出发,真的需要这么多的类吗?shiftcls在arm64架构下有33位,加上右移的3位,所以分配的内存空间为2^34 bit = 2 * 2^33 G = 2G,我们的程序类信息(代码段)一般都不会这么大(我们打包完的程序还包含其他一下资源文件),所以是完全足够的。接下来就是如何将
cls的地址如何从shiftcls中取出来了。上面的分析都是依据arm64的,下面的我们从程序中跑一下,犹如我们是在模拟器上运行的,shiftcls取的44位。取出
cls地址的方法有两种:利用掩码
ISA_MASK进行 与运算:define ISA_MASK 0x00007ffffffffff8ULL

(lldb) p/t 0x00007ffffffffff8ULL
(unsigned long long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000也就是取
64后面的47位,也就是cls的值。位运算:
shiftcls存在3~46位上,所以可以做如下运算:
也就是取中间的
44的值,然后在后面补3个0之后就是cls的地址了。
cls 的地址已经存放到 isa 里面了,后面我们就可通过 isa 找到类,然后进行方法的调用等动作了。
isa 的走位
是时候来一张经典的 isa 走位图了:

这张图怎么理解呢?我们先来看一个例子:

在 OC 的继承链中,万物皆对象,所以他们都有 isa 指针,而 isa 的存储的值,就是上面走位图中虚线的走向:
- 对象的
isa-> 类 - 类的
isa-> 元类 - 元类的
isa-> 根元类 - 根元类的
isa-> 根元类
其中比较特殊的点在于 NSObject,NSObject因为是 OC 对象的根类的原因:
NSObject对象的isa指向NSObject类NSObject类的isa指向NSObject元类NSObject元类的isa指向自己,即根元类的isa指向自己
上面的图中还要另外一条线,即 superclass 的继承链:
- 类的继承,
superclass指向父类 - 元类的继承,
superclass指向父元类 - 根类
NSObject的superclass指向 nil - 根元类的
superclass指向根类
根据这个有个很有意思的面试题:

这里考察的就是对 isa 的理解:
首先我们看下 isKindOfClass 和 isMemberOfClass 的源码:
+ (BOOL)isKindOfClass:(Class)cls { |
+ (BOOL)isMemberOfClass:(Class)cls { |
这两个方法的实现都不复杂,isKindOfClass 里面有一个循环,会通过 superclass 一直找到 NSObject。
上面的面试题中:
re1, re2, re3, re4考察的是类与元类,根元类与根类之间的关系re5, re6, re7, re8考察的是对象与类之间的关系