现在对象在内存中已经分配好内存空间了,但对象和类是怎么关联上的呢,这就是 isa 的工作了。
isa 联合体
我们可以看一下对象的表现形式:
struct objc_object {
private:
isa_t isa;
}
所以每一个对象必然有一个 isa。
然后我们看一下 isa 的结构:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
可以看到,isa 是一个 联合体 。
isa 的结构
首先我们来分析一下 isa 的结构:
isa 有三个成员:Class 、 bits 和一个结构体。
以下以 64 位架构为例分析:
-
Classtypedef struct objc_class *Class;Class是一个结构体指针,所以Class占 8 字节。 -
bitstypedef unsigned long uintptr_t;bits是一个无符号长整形,占 8 个字节。 -
结构体
struct { ISA_BITFIELD; // defined in isa.h };结构体的内容是一个宏定义,宏定义在编译时替换成定义好的内容,这样就可以区分不同架构(如
__x86_64__与__arm64__)。
isa.png
其实这里的实现,就是上面我们提到的位域。
从图中我们可以看出,两个架构上结构体的字段相同,只是分布不同。
结构体的大小为 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_bits.png
关联 isa
现在我们来看一下关联 isa 的代码实现:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
...
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
代码稍微精简了一下。注意这里的 SUPPORT_INDEXED_ISA 宏:
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
查阅到 资料,__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
isa__.png
(lldb) p/t 0x00007ffffffffff8ULL (unsigned long long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000也就是取
64后面的47位,也就是cls的值。 -
位运算:
shiftcls存在3~46位上,所以可以做如下运算:
isa_cal.png
也就是取中间的
44的值,然后在后面补3个0之后就是cls的地址了。
-
cls 的地址已经存放到 isa 里面了,后面我们就可通过 isa 找到类,然后进行方法的调用等动作了。
isa 的走位
是时候来一张经典的 isa 走位图了:
isa
这张图怎么理解呢?我们先来看一个例子:
isa_examp.png
在 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_test.png
这里考察的就是对 isa 的理解:
首先我们看下 isKindOfClass 和 isMemberOfClass 的源码:
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
这两个方法的实现都不复杂,isKindOfClass 里面有一个循环,会通过 superclass 一直找到 NSObject。
上面的面试题中:
-
re1, re2, re3, re4考察的是类与元类,根元类与根类之间的关系 -
re5, re6, re7, re8考察的是对象与类之间的关系














网友评论