三:对象原理之isa的本质探究

608 阅读6分钟

  通过之前的介绍我们知道了对象是如何创建的,但是呢,这里还存在一个小细节,在_class_createInstanceFromZone方法中执行完obj = (id)calloc(1, size);这句之后obj还并不是我们要的对象,当执行完obj->initInstanceIsa(cls, hasCxxDtor);这句之后obj才是我们创建的MYPerson对象,当时说这是因为这里初始化了一个isa,并和类进行了关联,我们也知道通过对象的isa可以找到所属的类,今天我们就探究一下isa

一:isa是什么

  在开头我们先介绍一下,isa有两种,一种是单纯指针(Class类型),另一种是nonpointer还包含一些其他信息,以优化内存。我们都知道一个isa8字节64位,所以这每一位都包含一些类的相关信息,便于进行内存优化。

1. 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
}; 

2. Class的定义

  其实 Class 是一个 objc_class 的指针

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    .....此处省略
}    

3. bits定义

typedef unsigned long           uintptr_t;

从上面的定义中可以发现,isa其实是一个isa_t联合体,拥有clsbits两个成员。联合体又是什么?联合体会与其内部成员共用内存,内存的大小取决于成员中占用内存最大的那个,无论是结构体指针还是unsigned long,都是8字节,所以isa_t就是8字节。

二:关于isa的内存优化

  我们从isa_t联合体中的位域ISA_BITFIELD入手

1. 位域

以下是百度百科对位域的定义

信息的存取一般以字节为单位。实际上,有时存储一个信息不必用一个或多个字节,例如,“真”或“假”用0或1表示,只需1位即可。在计算机用于过程控制、参数检测或数据通信领域时,控制信息往往只占一个字节中的一个或几个二进制位,常常在一个字节中放几个信息

2. ISA_BITFIELD定义

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL // ULL代表16进制的后缀    掩码
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   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 deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

从代码中我们可以看到,有arm64x86_64两种架构,但是位域的大小都是 8 字节,64 位。

3. 每一位代表什么

  • nonpointer: 表示是否对 isa 指针开启指针优化
  • 0: 纯 isa 指针
  • 1: 不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  • has_assoc: 关联对象标志位,0 没有,1 存在
  • has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced: 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • deallocating: 标志对象是否正在释放内存
  • **has_sidetable_rc:**当对象引用技术大于10时,则需要借用该变量存储进位
  • extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1

从上面看出,位域中存储着很多的信息,已经将这8字节64位用到极致了。shiftcls需要注意下,它存储着类指针的值,其实也是通过它来找到所属的类

三:isa指向分析

类继承于NSObject,那么必然会有isa,而且isa必然在首位

我们通常用Class stuClass = [stu1 class]这种方式获取到对象所属的类,那么它是怎么获取的呢?我们通过源码来看一下
首先看class方法的内部实现,我们发现调用了object_getClass(self)方法

- (Class)class {
    return object_getClass(self);
}

object_getClass(self)的内部实现

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

getIsa内部实现

objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

这里关键的点就在于ISA(),看下其内部实现

objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

以上的源码是调用的过程,最终代码会走到return (Class)(isa.bits & ISA_MASK),这里返回的是进行按位与运算的数值强转为 Class 的结果。其中ISA_MASK是掩码,在上面位域中已经有定义 --> 0x0000000ffffffff8ULL

我们来验证我们的猜想
先打印对象stu1的内存结构,那么0x001d8001000014a9代表的就是isa

(lldb) x/4xg stu1
0x1018131d0: 0x001d8001000014a9 0x0000000000000000
0x1018131e0: 0x72726f43534e5b2d 0x65546e6f69746365
(lldb)

这时候我们打印0x001d8001000014a9是什么都看不出来的

(lldb) po 0x001d8001000014a9
8303516107936937

我们尝试下这么操作(isa.bits & ISA_MASK),加上掩码。

(lldb) p/x 0x001d8001000014a9 & 0x0000000ffffffff8
(long) $7 = 0x00000001000014a8
(lldb) po 0x00000001000014a8
WYStudent

此时已经验证了我们的猜想,通过isa我们获取到了类的信息。

通过上面同样的方式来验证,类是isa指向元类,元类的isa指向根元类,根元类的isa指向自己.

最后放一张官方的图大家体会下