iOS原理 alloc核心步骤3:initInstanceIsa详解

iOS原理 文章汇总

前言

前面介绍了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,表示该对象的引⽤计数值。若extra_rc溢出时, 则需要使⽤Sidetable来存储引用计数。

二、分析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,在计算器中转换二进制显示为:

iOS原理 alloc核心步骤3:initInstanceIsa详解_第1张图片
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,转换为二进制显示如下:

iOS原理 alloc核心步骤3:initInstanceIsa详解_第2张图片

从图中可看到,地址转换为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源码中可看到,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,转换为二进制显示为:
iOS原理 alloc核心步骤3:initInstanceIsa详解_第3张图片
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详解)