美文网首页iOS进阶专栏
iOS原理 alloc核心步骤3:initInstanceIsa

iOS原理 alloc核心步骤3:initInstanceIsa

作者: 东篱采桑人 | 来源:发表于2020-09-23 18:15 被阅读0次

    前言

    前面介绍了alloc流程中的前两个核心步骤:instanceSize方法calloc方法,接下来分析最后一个核心步骤--initInstanceIsa方法。在这一步,isa将类信息和之前系统为对象分配的内存空间关联起来,即完成了对象的实例化。

    一、isa的结构

    在分析之前,我们先来看下isa的结构。从objc源码中可看到,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
    };
    

    isa本质上是一个isa_t的共用体(联合体):

    • isa_t()isa_t(uintptr_t value) : bits(value)是isa的两个初始化方法。
    • clsbits是两个成员变量。
    • struct没有定义变量,因此其内部成员即为母结构isa_t的成员变量。

    数据结构 -- 共用体Union 中可知,isa_t的内存大小为最大成员的内存大小,所以内存为8字节。isa_t里的各成员是互斥的,同一时刻只能保存一个成员的值,如果对某个成员赋值,会影响到其他成员的值。

    strcut里有个宏定义ISA_BITFIELD,在源码中可以看到,在不同的处理器架构下,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
    #   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
    
    • __arm64__:表示64位arm处理器的指令集,对应于iPhone、iPad真机。
    • __x86_64__:针对x86架构的64位处理器,是Mac处理器的指令集,对应于macOS和模拟器。

    两种架构的主要区别在于shiftcls的位数不同,由于objc源码不能真机调试,所以这里针对x86_64架构来分析。

    1.1 分析x86_64架构下的isa_t结构
    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
        struct {
            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
        };
    }
    

    从结构可看出,为了优化内存空间,struct采用了『位域』的结构,由数据结构 -- 位域一文可知,struct里的各成员在isa的内存空间依次紧挨存放,中间没有缝隙,总共占用64位内存。接下来看下各成员的作用:

    • nonpointer:存放在内存空间的位置0,表示是否对 isa 指针开启指针优化。
      0:表示纯isa指针,只包含了类对象地址,
      1:不止包含了类对象地址,isa 中还包含了是否有析构函数、对象的引⽤计数等其他信息。
      自定义的类,创建对象时,nonpointer为1,而系统的类大多数为0

    • has_assoc:存放在内存空间的位置1,表示关联对象标志位,0没有,1存在。

    • has_cxx_dtor:存放在内存空间的位置2,表示对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。(析构函数如OC的dealloc方法)

    • shiftcls:存放在内存空间的位置3-46,用于存储类指针的值

    • magic:存放在内存空间的位置47-52,⽤于调试器判断当前对象是真的对象还是没有初始化的空间。

    • weakly_referenced:存放在内存空间的位置53,指对象是否被指向或者曾经指向⼀个 ARC 的弱变量, 没有弱引⽤的对象可以更快释放。

    • deallocating:存放在内存空间的位置54,标志对象是否正在释放内存。

    • has_sidetable_rc:存放在内存空间的位置55,当extra_rc不足以保存引用计数时,标记为true,需要借⽤该变量存储进位。

    • extra_rc:存放在内存空间的位置56-63,表示该对象的引⽤计数值,实际上是引⽤计数值减 1, 即如果对象的引⽤计数为 10,那么 extra_rc 为 9。若extra_rc不足以保存引用计数时, 则需要使⽤到上⾯的 has_sidetable_rc来进行管理。

    二、分析initInstanceIsa流程

    objc源码中进行断点跟踪可知,initInstanceIsa方法的底层实现如下:

    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        ASSERT(!cls->instancesRequireRawIsa());
        ASSERT(hasCxxDtor == cls->hasCxxDtor());
         
        initIsa(cls, true, hasCxxDtor);
    }
    

    这里直接调用initIsa方法,完成isa的初始化。initIsa方法的底层实现如下:

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
    { 
        ASSERT(!isTaggedPointer()); 
        //step1:判断是否为nonpointer
        if (!nonpointer) {
            isa = isa_t((uintptr_t)cls);
        } else {
            ASSERT(!DisableNonpointerIsa);
            ASSERT(!cls->instancesRequireRawIsa());
            //step2:创建newisa
            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
           //step3:对newisa进行初始化设置
            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
            
           //step4:保存newisa
            isa = newisa;
        }
    }
    

    initIsa方法中,总共经历了4步:

    step1:判断是否为nonpointer

    在第一步,会先判断是否为nonpointer

    • nonpointer==false,则直接通过isa_t((uintptr_t)cls)方法初始化isa,isa的64位内存中保存的是cls类对象的地址,这样对象就可以通过isa访问到类信息。一般系统的类在创建对象时nonpointer会为false。
    • nonpointer==true,则会走下面的step2流程,自定义的类在创建对象时nonpointer会为true。

    类对象只会生成一个,在运行时由系统创建。

    step2:创建newisa

    在第二步,创建了一个newisa,打断点后在LLDB中打印newisa可以看到各成员都为空值。

    //在创建newisa后打印
    (lldb) p newisa
    (isa_t) $3 = {
      cls = nil
      bits = 0
       = {
        nonpointer = 0
        has_assoc = 0
        has_cxx_dtor = 0
        shiftcls = 0
        magic = 0
        weakly_referenced = 0
        deallocating = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
    (lldb) 
    
    step3:对newisa进行初始化赋值

    第三步,先判断宏定义SUPPORT_INDEXED_ISA,由于在MacOS上运行objc源码,所以此时SUPPORT_INDEXED_ISA为0。

    #if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
    #   define SUPPORT_INDEXED_ISA 1
    #else
    #   define SUPPORT_INDEXED_ISA 0
    #endif
    

    判断结束后会对下面三个成员赋值:

    ① newisa.bits = ISA_MAGIC_VALUE

    对bits赋值,宏定义ISA_MAGIC_VALUE的值为0x001d800000000001ULL,在计算器中转换二进制显示为:

    ISA_MAGIC_VALUE值的二进制显示

    从图中可看到,分别在第1位和在第47-52位上有值,在上面分析isa_t的结构可知,在内存空间位置0上存放的是成员nonpointer,值为1,在位置47-52位上存放的是成员magic,值为111011,即59,所以经由这步赋值后:

    • nonpointer==1:表示newisa不止包含了类对象地址,还包含了是否有析构函数、对象的引⽤计数等其他信息。
    • magic==59:表示当前对象不再只是一个内存空间,已经被初始化。

    在对bits赋值后,再在LLDB中打印newisa,即可验证成员值的变动。

    //在赋值newisa.bits = ISA_MAGIC_VALUE后打印newisa
    (lldb) p newisa
    (isa_t) $4 = {
      cls = 0x001d800000000001
      bits = 8303511812964353
       = {
        nonpointer = 1
        has_assoc = 0
        has_cxx_dtor = 0
        shiftcls = 0
        magic = 59
        weakly_referenced = 0
        deallocating = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
    (lldb) 
    
    ② newisa.has_cxx_dtor = hasCxxDtor

    对has_cxx_dtor赋值,由于对象有析构函数(dealloc方法),所以此处为true,has_cxx_dtor存放在内存中的位置2,所以在LLDB中打印newisa为:

    //在赋值newisa.has_cxx_dtor = hasCxxDtor后打印newisa
    (lldb) p newisa
    (isa_t) $5 = {
      cls = 0x001d800000000005
      bits = 8303511812964357
       = {
        nonpointer = 1
        has_assoc = 0
        has_cxx_dtor = 1
        shiftcls = 0
        magic = 59
        weakly_referenced = 0
        deallocating = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
    (lldb) 
    
    ③ newisa.shiftcls = (uintptr_t)cls >> 3

    对shiftcls赋值,这一步是将类对象地址右移3位,然后赋值给shiftcls成员,即可完成关联,这样对象就能通过isa访问到类信息。在LLDB中打印结果为:

    //先打印cls的地址(将类型强转为uintptr_t)
    (lldb) p (uintptr_t)cls
    (uintptr_t) $11 = 4294975960
    //再将地址右移三位,打印计算结果
    (lldb) p $11 >> 3
    (uintptr_t) $12 = 536871995
    //最后在shiftcls赋值后打印newisa
    (lldb) p newisa
    (isa_t) $13 = {
      cls = LGPerson
      bits = 8303516107940317
       = {
        nonpointer = 1
        has_assoc = 0
        has_cxx_dtor = 1
        shiftcls = 536871995
        magic = 59
        weakly_referenced = 0
        deallocating = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
    (lldb) 
    

    从打印结果可以看到,类对象地址先经过类型强转,然后右移3位,再进行赋值,最终保存在isa里的shiftcls值(536871995)和最开始的cls地址(4294975960)是不一样的。在这步处理中,可能会有下面几个疑问:

    • 1.为什么要强转成uintptr_t类型?

    uintptr_tunsigned long类型,由于机器只能识别0 、1这两种数字,即二进制数据,所以将地址存储在内存空间时需要先转换为uintptr_t类型。

    • 2.为什么要右移3位?

    位运算是直接对内存中的二进制数进行操作,cls的地址为4294975960,转换为二进制显示如下:


    从图中可看到,地址转换为64位二进制数后,其低3位和高位均是0,所以为了优化内存,可以舍掉这些0 ,只保留中间部分有值的位。所以右移3位,其实是舍掉低位3个0 ,再以中间44位或33位保存在isa中,在读取的时候再左右补0还原成类对象地址。这样isa的64位内存空间不仅可以保存类对象地址,还可以保存引用计数等其他信息。
    step4: 保存newisa

    isa = newisa:在完成newisa的初始化后,再将指针赋值给对象的成员isa,这样实例对象可以通过isa来访问类信息。

    至此,在经历了上面4个步骤后,就通过isa完成了对象和类信息的关联。

    三、访问类信息

    通过上面的分析可知,isa里的shiftcls成员保存了类对象地址,再来看看对象是如何通过isa访问到类对象的。在创建对象后,可以通过object_getClass方法来访问类对象,调用方法时需要先#import <objc/runtime.h>。老规矩,在objc源码中可看到,object_getClass方法的底层实现如下:

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

    通过流程跟踪可知,会依次执行object_getClass -> getIsa() -> ISA()这三个方法,最终是在ISA()方法里处理,来看下底层实现:

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

    因为是在MacOS上运行objc源码,所以会执行(Class)(isa.bits & ISA_MASK)。这里将isa的bits成员和ISA_MASK先进行与运算,再将结果强转成Class类型返回。

    • bits:由于isa_t是个共用体,所以成员bit和位域成员共享内存,因此bits的值即为isa内存空间存储的值。
    • ISA_MASK:在x86_64架构下值为0x00007ffffffffff8ULL,转换为二进制显示为:
    ISA_MASK值的二进制显示

    由此可知,将bits和ISA_MASK进行与运算,其实是对bits的低3位和高17位作清零处理,只保留了中间44位的值,相当于将shiftcls的值左边补17位0,右边补3位0,就得到了类对象的地址。这样对象就可以访问到类信息,来看下object_getClass的打印结果:

    //在实例化Person对象后,再打印object_getClass的结果。
    LGPerson *person = [[LGPerson alloc] init];
    Class cls = object_getClass(person);
    NSLog(@"cls = %lu", (uintptr_t)cls);
    
    //打印结果
    cls = 4294975960
    

    可以看到,打印结果和之前在LLDB中打印的类对象地址一致。

    四、总结

    isa在底层是一个isa_t的共用体,占用8字节内存,成员shiftcls里保存了类对象的地址,这样实例对象可通过isa访问到类信息,即完成了对象和类的关联。

    本文详细讲解了alloc流程的最后一步- initInstanceIsa方法的底层实现,若想了解整个alloc流程的底层实现逻辑,可以参考下面的推荐阅读。

    推荐阅读

    1.iOS原理 OC对象的实例化
    2.iOS原理 alloc核心步骤1:instanceSize详解
    3.iOS原理 alloc核心步骤2:calloc详解
    4.数据结构 -- 共用体Union
    5.数据结构 -- 位域

    相关文章

      网友评论

        本文标题:iOS原理 alloc核心步骤3:initInstanceIsa

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