iOS进阶专栏

iOS原理 alloc核心步骤3:initInstanceIsa

2020-09-23  本文已影响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的共用体(联合体):

数据结构 -- 共用体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

两种架构的主要区别在于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位内存。接下来看下各成员的作用:

二、分析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

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

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)是不一样的。在这步处理中,可能会有下面几个疑问:

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

位运算是直接对内存中的二进制数进行操作,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类型返回。

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.数据结构 -- 位域

上一篇 下一篇

猜你喜欢

热点阅读