isa的本质分析

462 阅读4分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

在文章iOS之alloc底层实现分析 里提到过,alloc有一个核心方法_class_createInstanceFromZone,这个方法做了3件事情:

  • instanceSize:申请要开辟的内存空间大小
  • calloc:根据instanceSize返回值开辟内存空间
  • initInstanceIsa:将类和开辟的空间进行isa绑定 今天,我们就针对第三点的isa,做进一步的分析。 找到_class_createInstanceFromZone方法里,绑定isa的代码段:
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

一步步点击进入initInstanceIsa -> initIsa ->

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);
    ...
    ...
}

我们看到这里有个isa_t,点进去:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;
    
private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
    ...
    ...
 }

我们可以看到isa_t是一个联合体位域,那么为什么它后面要跟上_t呢,说到这里,我们先引出另一个概念:nonPointerIsa。
我们都知道,64位一个isa占用8个字节(8*8=64位),但实际上存储类的内存地址,不需要这么多存储空间,所以苹果对这个情况做了优化,并且提出了TaggedPointerNonpointerIsa的概念:

  • TaggedPointer可以用来将一些小数据量的值,直接写在isa里面。因为我们知道,32位可以存储的数据位2^31(约21亿),而像NSDateNSNumber,这些类型的数据,不太会超过20亿,如果分配内存来存储、读取它们,性能上来说不如存放在栈区来的更快。 补充:为什么是2的31次方,而不是32次方,因为最高位为符号位,1代表负,0代表正,所以这里31次方,结果是2147483648。

  • NonpointerIsa表示isa存储的不只是类的内存地址,还包含了其它信息,譬如:引用计数,析构状态,被其他 weak 变量引用情况。

  • 我们要研究的就是isa_t里位域bits的内容。为了增加可读性,苹果定义了ISA_BITFIELD,我们点开它(点不进去的同学,command+B先编译一下),可以看到这样一段代码:

# elif __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)

为了方便阅读,我这边附上解释:

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

接下来,我们写点案例来证明一下:

01.png

那么,怎么通过isa找到类的内存地址呢?这里需要加上一个面具ISA_MASK值:

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                      ...
         ...
#   endif

由此可以得到ISA_MASK为0x007ffffffffffff8ULL,再加上之前的isa地址:

02.png 由此可以得出结论:对象通过isa+掩码,得到类的信息。

我们继续回到上面那个objc_object::initIsa方法:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
    //如果是纯isa,就进入这里
        newisa.setClass(cls, this);
    } else {
    //如果是不纯的isa,就进入这里
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

    ///进行位域
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    ...
    isa = newisa;
}

通过上述代码,我可以知道,如果是纯isa会直接走setClass方法,如果是不纯的isa则需要先进行位域赋值,然后再走setClass方法。接下来我们看看setClass方法都干了什么:

inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    ...
    ...

    shiftcls_and_sig = signedCls >> 3;

#elif SUPPORT_INDEXED_ISA
    // Indexed isa only uses this method to set a raw pointer class.
    // Setting an indexed class is handled separately.
    cls = newCls;

#else // Nonpointer isa, no ptrauth
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}

简单来说,就是通过isa的位运算,把不是shiftcls的部分给挤出去,缺失位置补0(前面在分析isa位域的时候知道了类的信息保存在uintptr_t shiftcls)。 由于我的手机连接的是iPhone11,所以架构是arm64,找到isa位域对应代码块

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)

03.png isa内存地址左移3位,右移12位,左移9位恢复原位,就可以得到类的内存地址,下面直接上代码:

04.png

至此,成功找到了类的信息。

补充:

通过控制台打印,可以看到用64位只保存内存地址,真的很浪费。

05.png