在看 objc-msg-arm64.s 的源码时,从对象中获取其 class
地址的方式有些没看明白。它是通过将对象地址和 0xffffffff8
进行与运算
来获取。如下所示,其中 $0
是 self
的地址。
// ISA_MASK = 0x0000000ffffffff8ULL
// p16 & 0x0000000ffffffff8ULL,获取真正的类地址
and p16, $0, #ISA_MASK
在查看了 objc-runtime
的源码之后,弄懂了其中的原理。因为在最新 objc_object
的定义中,isa 不再是 Class,而是 isa_t
联合体类型。
isa 布局
下面我们来看一下 isa 的布局。isa 是 isa_t
类型,它是个联合体。
struct objc_object {
private:
isa_t 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_BITFIELD
的定义,在 isa.h
中,这里是获取 class
的关键。它主要定义了一些位域来存储不同的信息。
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
重点关注第 4 项 shiftcls
,它占 33
位,存放的是 cls
的地址。ISA_MASK
,这个掩码就是用来取出中间的 33 位。
不知道大家有没有跟我一样的疑问?虽然照这样是取出了 33 位,可末尾还有 3 位是 0。那么按照常规思路,不是应该右移 3 位将其去除吗?
但事实上这样计算是没错的。因为 shiftcls
在赋值时,就将地址右移了 3 位。由于是 8 字节对齐,最后 3 位肯定为 0,右移之后也无影响,到时再补上 0 即可。不过也看到其他文章说这样做是为了节省空间
。所以按照上述方式取出,正好是原始地址。
到底对不对,我们看下源码就知道了。从下面代码中可以看到,最后一行,的确是将 cls 右移了 3 位。
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
// 省略部分代码
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;
// 这里已经右移了 3 位
newisa.shiftcls = (uintptr_t)cls >> 3;
}
实例验证
下面我们通过以下实例验证一下,每句都添加了详细的注释。
NSObject *obj = [NSObject new];
// p 的值是 obj 的地址
void *p = (__bridge void *)(obj);
// 由于 isa 是 struct objc_object 的第一个元素,那么将其转换为 long 64 位,对 p1 进行解引用后,则可得到 isa 的地址。
long *p1 = p;
// 对 p1 进行解引用后,得到 isa 的地址。
long isaAdress = *p1;
// 在 arm64 上,isa 不再是类的指针,而是联合体。需通过 isa & 0xffffffff8 获取类地址
// 也可以通过 lldb,输入 p/x obj->isa, 可查看其 class 地址。
long classAdress = isaAdress & 0xffffffff8;
// object_getClass(obj) 得到 obj 对应的类地址
NSLog(@"obj address:%p\n use object_getClass to get class address:%p\n use pinter to get obj address:0x%lx\n union isa address:0x%lx\n use isa address and 0xffffffff8 to get class address:0x%lx\n", obj, object_getClass(obj), (long)p, isaAdress, classAdress);
主要思路是,通过解引用 obj 地址,获取 isa 的值。然后进行与运算,得到 class 地址。
demo 中的实例 obj 实际上是指向 objc_object
的指针,而 objc_object 的第一个成员是 isa,那么也就是说 isa 自身的存储地址也就是 obj 的地址。而要想得到 isa 的内容,需要解引用该地址。
打印结果:
obj address:0x280394210
use object_getClass to get class address:0x1f0d9af08
use pinter to get obj address:0x280394210
union isa address:0x1000001f0d9af09
use isa address and 0xffffffff8 to get class address:0x1f0d9af08
从结果中可以看到,isaAdress & 0xffffffff8
的值跟通过 object_getClass
获取到的值相等,都为 0x1f0d9af08
。这样也就验证了上述计算方式的正确性。
另外,也可在 lldb 中输入 p/x obj->isa
,查看 class 的地址。