iOS alloc底层原理:ISA

339 阅读4分钟

接着iOS alloc 底层原理继续进行后续的探索,在此之前需要先了解一下isa指针。

isa的结构分析

isa分为两种:普通的纯地址指针(cls)、被优化过的nonpointer指针。

相关内容:结构体&联合体&位域

isa_t是一个联合体,包含了cls与bits,其中cls就是普通的纯指针,而bits其实是被优化过的nonpointer指针,是一个结构体位域,记录了类地址以及其他信息。

union isa_t {
    uintptr_t bits;对应与ISA_BITFIELD,
    ...
    Class cls;
    ...,
};

isa内存分布

不同架构下,isa的分布也不相同。

arm64:

define ISA_MASK        0x0000000ffffffff8ULL  //掩码,isa指针与之做&操作后才能获取到类(元类)对象的地址
#     define ISA_MAGIC_MASK  0x000003f000000001ULL 
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL  //初始化isa时,用来设置magic与nonpointer
#     define ISA_HAS_CXX_DTOR_BIT 1
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45) //用来快速将指针平移到extra_rc位置
#     define RC_HALF  (1ULL<<18) //extra_r容量的一半,在将引用计数移到散列表时用到

截屏2021-06-15 下午1.41.06.png

x86_64:

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

截屏2021-06-15 下午1.42.18.png

为什么要用结构体位域?

以在arm64架构下为例,isa是64位,也就是8字节,如果使用结构体,那能操作的最小单位是字节,而nonpointer、has_assoc等只需要一位就可以存储,这样的话对内存会造成很大的浪费,所以在这里使用了结构体位域进行优化,能直接对每一位进行操作。

isa拆分

  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。
  • has_assoc:关联对象标志位,0没有,1存在。
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • unused:以前是用来标志对象是否正在释放内存,现在变为了unused。
  • has_sidetable_rc:该变量用来标记是否挂载了散列表,如果对象的引用计数达到了ertra_rc能存储的最大值。则会将ertra_rc容量一半(RC_HALF)的引用计数转移到散列表中存储,并且将has_sidetable_rc置为1。
  • extra_rc:当表示该对象的引用计数值,实际值是引用计数值减 1,

ISA_MASK

以arm64架构为例,打印JLObject对象的内存,拿到开头的isa指针,并将isa指针使用po打印,发现竟然是一串数字,isa指向的不应该是类对象的地址吗。 截屏2021-06-17 下午10.59.05.png 截屏2021-06-17 下午11.00.58.png

我们再来使用ISA_MASK和isa做与操作,并将结果打印。 截屏2021-06-17 下午11.02.09.png

为什么会出现这种情况呢?

分析:isa前三位分别是nonpointer、has_assoc、has_cxx_dtor,从第四位开始,长度为33位是指向类/元类的地址shiftcls,而要拿到指向JLObject的指针,就需要只保留shiftcls,其他位清零。

截屏2021-06-17 下午11.26.57.png

ISA_MASK转换成二进制可以看到,对应shiftcls的33位都是1,其他位为0,按位与时,同为1得1,否则为0,以此来达到只保留shiftcls,其他位清零的目的。

我们还可以使用左移右移来达到清零的目的,有兴趣的小伙伴可以了解一下,这里就不进行具体的讲解了。

截屏2021-06-17 下午11.41.39.png

isa源码分析

在alloc三个重要步骤中,最后一步是初始化一个isa指针绑定到obj,下面是源码部分: 截屏2021-06-15 上午12.45.07.png

来看一下initInstanceIsa(Class cls, bool hasCxxDtor)

截屏2021-06-15 下午12.48.41.png

initIsa(Class cls)

截屏2021-06-15 下午12.47.51.png

发现它们两个内部都调用了initIsa(Class cls, bool nonpointer, bool hasCxxDtor)方法,

根据传入的参数可以断定initInstanceIsa用来生成nonpointer指针,而initIsa生成纯地址指针,在64位的环境下,只会执行initInstanceIsa。

/* nonpointer 标记是否为nonpointer指针
 * hasCxxDtor 是否有C++或Objc的析构器
 */
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    //判断是不是小类型指针
    ASSERT(!isTaggedPointer()); 
    isa_t newisa(0);//创建一个isa(isa_t是一个联合体)
    
    if (!nonpointer) {
        newisa.setClass(cls, this);//如果nonpointer为false,则直接生成一个纯地址指针
    } else {
        ...... 
        newisa.bits = ISA_MAGIC_VALUE;//设置isa中的magic与nonpointer
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
        newisa.extra_rc = 1;//引用计数设置为1
    }
    isa = newisa;
}
#define ISA_SIGNING_SIGN_NONE       1 // Sign no ISAs.
#define ISA_SIGNING_SIGN_ONLY_SWIFT 2 // Only sign ISAs of Swift objects.
#define ISA_SIGNING_SIGN_ALL        3 // Sign all ISAs.

截屏2021-06-15 下午2.36.53.png

通过断点可知,setClass中会执行isa.shiftcls = (uintptr_t)cls >> 3;且initInstanceIsa方法中传入的nonpointer为true,所以可以将代码简化一下:

objc_object::initIsa(Class cls, bool nonpointer,  bool hasCxxDtor)
{ 
    isa_t newisa(0);   //创建一个isa(isa_t是一个联合体)
    newisa.bits = ISA_MAGIC_VALUE;   //设置isa中的magic与nonpointer
    newisa.has_cxx_dtor = hasCxxDtor;
    newisa.shiftcls = (uintptr_t)cls >> 3   //newisa.setClass(cls, this);
    newisa.extra_rc = 1;//引用计数设置为1
}

总结

通过以上代码可以清晰的看到,生成一个isa指针所需的步骤:

  • 创建一个联合体类型的指针。
  • 使用ISA_MAGIC_VALUE为该指针的magic与nonpointer进行初始的赋值。
  • 设置用来标识当前对象是否有C++ 或者 ObjC 的析构器的has_cxx_dtor。
  • 将类对象地址右移三位,赋值到shiftcls。
  • 引用计数设为1