iOS底层之isa结构分析及关联类

从iOS底层之alloc、init探究这篇文章,我们可以知道,alloc一个对象的过程,主要是计算所需内存大小cls->instanceSize、申请内存空间calloc、将isa与类进行关联obj->initInstanceIsa

那么指针和类是怎么关联的呢?isa到底是什么结构?保存了什么信息?下面来一一解惑。


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        
        BKPerson *objc = [BKPerson alloc];

        NSLog(@"Hello, World!  %@",objc);
    }
    return 0;
}

alloc的源码跟进去到关联指针和类的步骤:

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);
    }

跟进obj->initInstanceIsa(cls, hasCxxDtor);

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

    initIsa(cls, true, hasCxxDtor);
}

主要做的事情是initIsa(cls, true, hasCxxDtor);

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)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;
    }
}

可以看到不管!nonpointer条件是否满足,都会生成一个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是一个union联合体,Class cls代表isa关联的类的类型,uintptr_t bits是一段保存着isa指针优化、是否关联对象标志位、对象是否有析构函数、类相关信息、引用计数等信息的8字节大小的无符号长整型数据。要知道联合体和结构体的区别是:

  • 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。结构体的类型大小大于等于内部所有变量的类型大小总和,最终的类型大小是最大成员的类型大小的倍数,不足补齐。

  • 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间。联合体类型的大小等于最大成员类型大小。

就是说联合体采用内存覆盖机制,只有一块变量存储区,只能存一个变量的值,新的成员赋值会把原本存储的成员信息替换掉。也就是Class clsuintptr_t bits只能set赋值其中一个。而内存使用的精细灵活,体现在以位域(即二进制中每一位均可表示不同的信息)存储成员数据,也就是以计算机二进制存储的方式,位bit为单位,用10标记数据,数据的尺寸大小是以占用多少bit,而不是以每个数据成员数据类型的尺寸大小(多少字节byte)存储。isabits占用的内存大小是8字节,即64位,可以存储足够多的信息,很大节省了内存。

isabits成员的位域,定义在isa.h源文件中

struct {
       ISA_BITFIELD;  // defined in isa.h
   };

ISA_BITFIELD是一个宏定义,分别在x86_64架构arm64架构是这样的:

isa的bits位域

bits的64位存储分布图:

bits位域分布

其中存储的成员信息:

  • nonpointer是否开启isa指针优化。上面介绍isa_t联合体的两个成员(Class cls;
    uintptr_t bits;),当nonpointer=1时,则代表对象isa不单指class了,而是使用了优化的bits保存类的信息和其他内存管理的信息。一般自定义的类都是1的,而系统类才会有纯isa指针的情况,占1位。
    0:纯isa指针,即表示class地址。
    1:不只是类对象地址,isa中包含了类信息、对象的引用计数等。
  • has_assoc关联对象标志位,占1位。当关联对象标志位被设置为 1 时,表示该对象具有关联对象(Associated Objects)。关联对象允许开发者将额外的数据与一个对象相关联,而无需修改该对象的类结构。这对于给现有的类添加属性或附加其他数据非常有用,特别是在无法修改类定义的情况下。没有关联对象的对象释放的更快。
  • has_cxx_dtor 该对象是否有C++或Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快释放对象,占1位。
  • shiftclx存储类指针的值, 也就是类信息,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位。
  • magic 用于调试器判断,在调试时区分对象是否已经初始化,占6位。固定为0x1a。
  • weakly_refrenced是用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。
  • deallocating 标志该对象是否正在被释放。
  • has_sidetable_rc用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。
  • extra_rc表示该对象的引用计数值-1,比如,一个object对象的引用计数为9,则此时extra_rc的值为8。 如果大于最大容量,就需要取一半计数存到散列表中,真机上最多有8张散列表存储对象引用计数,x86_64则最多64张,这时上面的has_sidetable_rc值置为true

了解完isa内部结构之后,我们来验证一下alloc的过程中isa跟类是如何关联的。
在执行BKPerson *objc = [BKPerson alloc];时跟进到initIsa的方法中:


可以看到!nonpointer条件为false,说明BKPerson类并不是一个纯isa指针,需要开启指针优化,所以走到下面的初始化流程。

打印出这个newisa

这时的isa的成员clsnilbits默认为0bits的位域信息都是初始值0

往下执行


这一句是给bits赋值一个初始值,这是一个系统宏定义

#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

这时再打印newisa,可以看到赋值后的nonpointer已经是1了,magic为59。
上面我们了解到magicx86_64架构下的bits位域分布在47-52位,占据6位,用计算器验证下这个59是不是0x001d800000000001ULL里的:

0x001d800000000001ULL的二进制

可以看到第一位是1,跟我们的打印结果一致,nonpointer值变为1,第47位往后数6位是111011,那么59的二进制是:
59的二进制

此时此刻,可以得出结果,这magic59确实是由0x001d800000000001ULL填进去的。

再往下执行



has_cxx_dtor赋值为false,表示没有自定义的析构函数。

newisa.shiftcls = (uintptr_t)cls >> 3;

表示将cls类地址右移3位,赋值给shiftcls,上面我们知道x86_64下,shiftclsbits64位内存中占用44位,从3-46位
通过打印的信息,cls = BKPerson能看出来已经将类信息关联上指针了,也就是这个shiftcls = 536871965这个信息保存着类的信息。


打印cls这个类,并手动将其地址右移3位,可以得出536871965,确实等于shiftcls的数值。

那么为什么要右移3位呢?而不直接赋值过去呢?

cls右移3位的原因

从图可以清晰解释,为什么需要右移3位?因为bits的成员shiftclsx86_64下占据44位,而类cls内存存储的类信息是在第3位47位,所以需要右移3位后开始存储,存到44位满了就停止存储。这样才能准确的存储到类的信息。

那我们怎么证明得出的这个类就是已经关联上了我们的对象指针?

我们将断点的堆栈回退到obj的关联类的地方。


控制台打印这个obj指针,并将isa的内存地址右移3位,再左移20位,再右移17位,这时再打印地址移动之后的isa的地址,可以看到,就是我们上面关联的类,也就是说这时对象指针和类关联上了

这个过程可以用下图清晰表现出来:


从指针获取类信息的操作过程

获取对象的类这个操作其实在我们日常开发中经常用到,我们通过导入#import

BKPerson *objc = [BKPerson alloc];
        
NSLog(@"%@", object_getClass(objc));  

结果为BKPerson

查看这个函数的源码,

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

继而查找getIsa()

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

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

再查找ISA()

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
}

查看if里的条件的宏定义,

#   define SUPPORT_INDEXED_ISA 0

可以知道走的是这行代码return (Class)(isa.bits & ISA_MASK);
也就是取出对象的isa里的bits与运算ISA_MASK
这个宏的定义是:

我们再通过lldb命令验证这个过程。取出对象objisa地址 & ISA_MASK,可以得出就是对象的类。
那么这个算法,其实就简化了我们上面对isa地址的一顿左移右移操作,直接一步到位得出类。
通过计算器查看这个ISA_MASK宏的二进制

ISA_MASK的二进制

可以看到,从第4位47位,一共44位,都为1,其他位都为0,而与运算,就是两个数只有相同位上都为1,才会得出1,所以这个与运算,就是为了取出中间44位的类信息的算法,其他位补0,得出一个64位的数,表示这个类。
至此,我们了解了isa结构,及其位域的分布和成员作用,并探索了对象指针关联类的过程并验证结果。
感谢阅读~

你可能感兴趣的:(iOS底层之isa结构分析及关联类)