美文网首页
iOS底层之isa结构分析及关联类

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

作者: K哥的贼船 | 来源:发表于2020-09-12 22:36 被阅读0次

    iOS底层之alloc、init探究这篇文章,我们可以知道,alloc一个对象的过程,主要是计算所需内存大小cls->instanceSize、申请内存空间calloc、将指针与类进行关联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是一个宏定义,分别在macOS的x86_64架构iPhone真机的arm64架构是这样的:

    isa的bits位域

    bits的64位存储分布图:

    bits位域分布

    其中存储的成员信息:

    • nonpointer一般自定义的类都是这个类型的,而系统类才会有纯isa指针的情况,占1位。
      0:纯isa指针。
      1:不只是类对象地址,isa中包含了类信息、对象的引用计数等。
    • has_assoc关联对象标志位,0代表没有关联对象,1代表存在关联对象,占1位。
    • has_cxx_dtor 该对象是否有C++OC的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快释放对象,占1位。
    • shiftclx存储类指针的值, 也就是类信息,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位。
    • magic 用于调试器判断当前对象是真的对象还是没有初始化的空间,占6位。
    • weakly_refrenced是指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
    • deallocating 标志对象是否正在释放内存。
    • has_sidetable_rc当对象引用计数大于219次方(x86_64架构为28次方)时,则需要存储到散列表,这时该变量值变为true
    • extra_rc表示该对象的引用计数值,最大为219次方(x86_64架构为28次方),实际上是引用计数值减1,,如果大于最大容量,就需要取一半计数存到散列表中,真机上最多有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 <objc/runtime.h>

    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结构分析及关联类

          本文链接:https://www.haomeiwen.com/subject/ytxvektx.html