iOS开发 — isa的结构&指向分析

307 阅读5分钟

在探索alloc流程的那一篇文章中,我们留下了一个initInstanceIsa()没有探索,这次我们就来全面的探索一下isa。

isa的结构分析

首先我们先跟源码,可以看到initInstanceIsa()的源码:

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

继续跟进:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#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
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don‘t want to hurt instantiation
        isa = newisa;
    }
}

通过跟进isa可以发现:

isa是一个isa_t的类型,再跟进isa_t,可以看到源码:

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_t结构我们可以得知isa其实一个联合体,联合体中各成员共享内存,联合体内存的大小取决于内部成员内存最大的那个。可以看到在联合体中还存在一个结构体,点进去查看:

根据不通的架构具体数值也不一样,这里以arm64为例。通过阅读源码可以发现这其实是一个位域。

这里要补充一下位域相关的信息:

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

在C语言中,位段(位域)的声明和结构(struct)类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际储存在一个或多个整型变量中。在声明时,位段成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。位域不能是静态类型。不能使用&对位域做取地址运算,因此不存在位域的指针,编译器通常不支持位域的引用(reference)。

知道了位域的概念,我们再来看ISA_BITFIELD每一位代表的具体含义:

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

将所有相加可得64位,即8字节,再结合联合体中其他成员可知isa为8字节,而联合体和位域的配合使用也使得isa不用添加过多的属性去存储信息,节省了很多空间。

isa的指向分析

1.对象与类的关联

我们了解了isa的结构,那么isa是如何将对象与类关联起来的呢?我们可以通过api objc_getClass来查看:

DZPerson *person = [DZPerson alloc];
object_getClass(person);

查看源码并跟进:

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
inline Class 
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];
    }
}
inline Class 
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);

由此可以得知将isa与ISA_MASK进行位运算后得到的就是类,也就证明了是isa将对象与类关联起来的。

isa的走向

我们可以通过lldb来查看isa的走向:

(lldb) x/4xg person
0x101a74ce0: 0x001d8001000011c9 0x0000000000000000
0x101a74cf0: 0x0000000101a74dc0 0x0000000101a75000
(lldb) p/x 0x001d8001000011c9 & 0x00007ffffffffff8
(long) $1 = 0x00000001000011c8
(lldb) po $1
DZPerson (类)

(lldb) x/4xg $1
0x1000011c8: 0x001d8001000011a1 0x0000000100aff140
0x1000011d8: 0x00000001003a2260 0x0000000000000000
(lldb) p/x 0x001d8001000011a1 & 0x00007ffffffffff8
(long) $2 = 0x00000001000011a0
(lldb) po $2
DZPerson (元类)

(lldb) x/4xg $2
0x1000011a0: 0x001d800100aff0f1 0x0000000100aff0f0
0x1000011b0: 0x0000000101a72fc0 0x0000000100000007
(lldb) p/x 0x001d800100aff0f1 & 0x00007ffffffffff8
(long) $3 = 0x0000000100aff0f0
(lldb) po $3
NSObject (根元类)

(lldb) x/4xg $3
0x100aff0f0: 0x001d800100aff0f1 0x0000000100aff140
0x100aff100: 0x0000000101a73080 0x0000000300000007
(lldb) p/x 0x001d800100aff0f1 & 0x00007ffffffffff8
(long) $4 = 0x0000000100aff0f0
(lldb) po $4
NSObject (根元类)

由此可以得出isa的指向关系:

对象 -> 类 -> 元类 -> 根元类 -> 根元类(根元类的isa指向自己)

这时就需要官方给出的isa走位图:

这里虚线就是isa的指向关系,实线则是类的继承关系。

到这里我们就分析完了isa的结构和走位指向,笔者能力有限,如有错误请在评论区指出。