稍微精深一点的IOS开发都听说过isa指针。它在OC的类中起到了指示自身类型的作用,是runtime实现的基础。那么isa指针到底是如何实现的呢,让我们从源码的层面进行分析。
NSObject -> Class -> objc_class -> objc_object
新建一个最简单的空类:
@interface Person : NSObject
@end
@implementation Person
@end
command点击NSObejct,我们可以看到cls的实现:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
那么这个Class又是什么数据呢?
typedef struct objc_class *Class;
我们可以看到Class实际上是objc_class结构体的指针。而objc_class,就是OC类的元类。
objc_class在objc源码中有三处定义:
// in runtime.h
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
.....
} OBJC2_UNAVAILABLE;
#if __OBJC2__
#include "objc-runtime-new.h"
#else
#include "objc-runtime-old.h"
#endif
// in objc-runtime-old.h
struct objc_class : objc_object {}
// in objc-runtime-new.h
struct objc_class : objc_object {}
我们当前使用的版本多是 objc2,所以起作用的是 最后一个 objc-runtime-new.h中定义的objc_class。objc_class则继承了objc_object。
struct objc_class : objc_object
objc_object同样在两个位置有定义:
in objc.h
#if !OBJC_TYPES_DEFINED
....
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
#endif
in objc-private.h
#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif
#define OBJC_TYPES_DEFINED 1
struct objc_object {
private:
isa_t isa;
.....
}
根据objc-private.h中的宏定义我们可以看到,起作用的实际上是objc-private.h中的定义。
对一个继承了NSObject的类而言,isa最后能定位到objc_object中。而用来存储类信息的,是objc_object的 isa_t isa;
isa_t
** isa_t是一个union**聚合体。聚合体和结构体最大的不同是它的内存存储方式。对聚合体而言,所有的成员变量的起始地址相同,对每一个成员的修改都会影响所有的变量。这样做最大的好处就是可以节约内存空间。但是也会导致每次可用的变量只有一个。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls; // 初始化是不会使用的
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 这才是关键
};
#endif
};
isa_t 是一个联合体,这里所占空间为 8字节,共64位 ,内存布局从低位到高位情况如下图:
struct {
ISA_BITFIELD; // 这才是关键
};
# 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
注意 在 struct 的变量A后面加 :(数字n), 是指定该变量A在内存中占用 n 位。
nonpointer(存储在第0字节) : 是否为优化isa标志。0代表是优化前的isa,一个纯指向类或元类的指针;1表示优化后的isa,不止是一个指针,isa中包含类信息、对象的引用计数等。现在基本上都是优化后的isa。
has_assoc (存储在第1个字节): 关联对象标志位。对象含有或者曾经含有关联引用,0表示没有,1表示有,没有关联引用的可以更快地释放内存。
has_cxx_dtor(存储在第2个字节): 析构函数标志位,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速地释放对象。
shiftcls :(存储在第3-35字节)存储类的指针,其实就是优化之前 isa 指向的内容。在arm64架构中有33位用来存储类指针。x86_64架构有44位。
magic(存储在第36-41字节):判断对象是否初始化完成, 是调试器判断当前对象是真的对象还是没有初始化的空间。
weakly_referenced(存储在第42字节):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)。
deallocating(存储在第43字节):标志对象是否正在释放内存。
has_sidetable_rc(存储在第44字节):判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。
- extra_rc(存储在第45-63字节。):存放该对象的引用计数值减1后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为 9。
isa的初始化
对OC类而言,isa的初始化是在 alloc中完成的。具体来说是在
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls,...) {
...
id obj; // id 是objc_object的指针
// 3: ?
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
obj->initIsa(cls);
}
...
}
initInstanceIsa与initIsa最后都会统一到一个函数里:
(去除一些宏定义,断言以及条件判断等,我们直接将代码减少到它执行的代码)
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
isa_t newisa(0);
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;
isa = newisa;
}
- 首先对整个bits进行赋值,传入 ISA_MAGIC_VALUE ,在arm64架构下,该值为
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
将该值转换为2进制
对应 isa_t 中内存布局的位置,可以看出对 bits 赋值 就是对 nonpinter 和 magic 赋值的过程。
2.其次对has_cxx_dtor赋值。
- 最后对shifcls赋值
shiftcls详解
shiftcls 里面存放着类的指针。众所周知指针是64位的,那么为什么可以放置在33位或或者44位的shiftcls中呢?在需要指针时又如何还原呢?
首先我们看下shiftcls的赋值:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
....
isa_t newisa(0);
....
newisa.shiftcls = (uintptr_t)cls >> 3;
....
isa = newisa;
}
}
首先我们将cls的尾部三位去掉,这是因为对类而言,它至少包含一个isa指针,那么它的长度一定是8的整数倍。这里我们可以使用runtime计算下Person的长度来认证下:
#import <objc/runtime.h>
size_t insSize = class_getInstanceSize([Person class]);
NSLog(@"PP Size:%zd",insSize);
然后,我们会看到源码中shiftcls的宏定义后面有一个数据 MACH_VM_MAX_ADDRESS:
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
这是最大虚拟寻址空间,也就是内存空间能够分配的最大地址。以ARM32为例,它最大的虚拟寻址空间为 0x10,0000,0000。而33 + 3 (右移的三位) = 36 = 4 * 9。正好是虚拟寻址空间的位数 - 1。正好可以覆盖虚拟寻址空间。也就是说64位的数据中,前面64 - 36 = 18位是无用的。
因此,只要存储64位指针中间的33位就可以表示了。只要在使用的时候与上mask:0x0000000ffffffff8,就可以得到对应的类指针。