iOS-isa结构分析

954 阅读6分钟

前言

在oc对象alloc的过程中,使用calloc函数开辟内存空间后,需要把实例和类关联起来,这一步到底是如何实现的呢?

预备知识

联合体(union)

语法定义

联合体(union),与结构体(struct)有极为相似的语法结构,跟struct一样可以有多种数据类型和成员:

union {
    int i;
    double x;
    char str[16];
} myData;

unoin的成员共享同一内存空间

不同于struct的成员在结构体中具有独立的内存位置,union的成员共享同一内存位置,也就是说,union中的所有成员都是从相同内存地址开始的。unoin的本质就是同一内存地址的数据,可以使用不同的方式来解读

union {
    int i;
    double x;
    char str[16];
} myUnion;
struct {
    int i;
    double x;
    char str[16];
} myStruct;

以上union和struct,成员的存储布局,可以用下面两图说明: unoin数据成员布局 struct数据成员布局-不考虑内存对齐 验证代码

union {
        int i;
        double x;
        char str[16];
    } myUnion;

    myUnion.i = 0x313233;
    NSLog(@"i=%d, str=%@", myUnion.i, [NSString stringWithUTF8String: myUnion.str]);

控制台输出:

 i=3224115, str=321

查看内存

(lldb) p 0x313233
(int) $1 = 3224115
(lldb) p &myUnion
((anonymous union) *) $2 = 0x00007ffeefbff448
(lldb) x $2
0x7ffeefbff448: 33 32 31 00 00 00 00 00 78 f4 bf ef fe 7f 00 00  321.....x.......
0x7ffeefbff458: 7b 00 c4 ce 76 e1 d1 a4 78 f4 bf ef fe 7f 00 00  {...v...x.......
  1. 0x31323316进制转10进制,即3224115
  2. str[0]-str[2], 即从首地址开始的三个字节,0x33,0x32,0x31, 正好是字符'3'、'2'、'1'的ascii码

union的size

unoin的size要符合以下条件

  1. 所有成员都是从offset为0的地方开始
  2. 大小要足够容纳最宽的成员
  3. 大小要是所有基础类型成员大小的整数倍

验证代码

union {
    int i;
    double x;
    char str[11];
} myUnion;
NSLog(@"size=%ld", sizeof(myUnion));

控制台输出:

size=16

分析: 根据原则1,由于char数组,至少需要11位;根据原则2,需要是double的整数倍,即8的整数倍,最终是16

unoin与struct的异同

  • 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
  • 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”,即任何两个成员不会同时有效;但优点是内存使用更为精细灵活,也节省了内存空间。 当多个数据需要共享内存或者多个数据每次只取其一时,可以考虑使用union

位域(Bit field)

在一个结构体中以位为bit单位来指定其成员所占内存长度,即为位域。这样的设计有以下优势:

  • 节省存储空间,尤其是成千上万数据的时候;
  • 可以访问数据的指定部分内容

验证代码

struct {
    unsigned short a : 1;
    unsigned int b : 2;
    int c;
}myStruct;

myStruct.a = 0b11;
myStruct.b = 0b110;
myStruct.c = 0xffffffff;
NSLog(@"a=%d, b=%d, size=%d", myStruct.a, myStruct.b, sizeof(myStruct));

控制台输出:

 a=1, b=2, size=8

查看内存:

(lldb) p &myStruct
((anonymous struct) *) $0 = 0x00007ffeefbff430
(lldb) x/t $0
0x7ffeefbff430: 0b00000000000000000000000000000101
(lldb) x/gx $0
0x7ffeefbff430: 0xffffffff00000005
(lldb) 

实例和对象的关联

initInstanceIsa()

在实例对象alloc过程中,关键步骤在于函数_class_createInstanceFromZone:

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
	// 1. 计算需要的空间大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    // 2. 开辟空间,返回地址,赋值为obj
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    // 3. 关联class
    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);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

在第三步执行前,po obj,输出:

(lldb) po obj
0x0000000100684960

obj中并没有class信息,在第三步执行后,po obj,输出:

(lldb) po obj
<LGPerson: 0x100684960>

也就是说经过obj->initInstanceIsa(cls, hasCxxDtor);,之后对象与class关联起来。

initIsa

继续跟踪initInstanceIsa:

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

    initIsa(cls, true, hasCxxDtor);
}

追踪到initIsa:

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

分析源码可知,该函数主要实现了对变量isa的赋值。那么isa这个变量就应该是关键点,该变量中应该存储了类信息相关数据。

成员变量isa

jump to define, 可以看到:

struct objc_object {
private:
    isa_t isa;

public:

isa变量的类型isa_t,是实例对象的私有成员变量。由此可知:实例对象通过私有成员变量isa关联到对应的Class

isa的结构以及初始化

isa_t 结构

  1. isa是一个联合体union,包含成员cls, bits, 还有一个struct
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
};
  1. cls是一个结构体指针
typedef struct objc_class *Class;
  1. bits是一个unsigned long, 64位
typedef unsigned long           uintptr_t;
  1. struct里面使用了位域,每段都有不同的含义
# 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)
  1. isa_t利用结构体、位域,在一个8字节有限空间内存储了多种信息

如果单纯的只是存储类对象指针,根本用不上64位(64位如果作为地址空间,远远超出了手机的物理空间大小,1T才40位呢,), 所以存储其他信息,空间高效利用

isa中具体字段含义

字段含义表

isa,8字节长度,从低字节到高字节,存储信息如下

名称长度意义
nonpointer1表示isa指针是否开启指针优化。0:纯指针 1:不止存储类对象地址,还包含了其他信息,具体见各行。也就是下面的几个字段必须在nonpointer为1时才有
has_assoc1关联对象标志位,0-没有,1-有
has_cxx_dtor1标记该对象是否有c++或oc的析构器,如果有,则在释放对象时需要做析构处理,如果没有,则可以更快释放掉
shiftcls33存储类对象指针
magic6用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced1标记是否有ARC的weak变量指向或者曾经指向该对象,如果没有,则可以更快的释放
deallocating1标记改对象是否正在释放
has_sidetable_rc1标记该对象的是否使用了引用计数表存储引用计数
extra_rc19该对象的引用计数值,实际上是引用计数值减1

关于引用计数存储

根据上表的has_sidetable_rc、extra_rc可知:引用计数是分两部分存储的:

  1. 当引用计数值未超过extra_rc可以表示的范围时,存储在extra_rc中,最大范围为0x7FFFF(524287),已经是一个很大的值了!
  2. 当引用计数继续增长时,就需要使用sidetable的refcnts引用计数表,具体见下图源码分析 retain中引用计数的处理
  3. 关于引用计数的处理,可以这样理解:**用一个桶和一个池子装水,桶满了后,倒一半进入池子,然后继续在第一桶中装水,满了以后再次到一半进入池子。**这样的好处就是减少对引用表的访问,减少地址跳转操作,提高性能。
  4. 关于RC_HALF:因为extra_rc是19位的宽度,满了以后,(extra+1)的一半,正好就是(1ULL<<18)。以3位宽度为例,extra_rc就是最大0b111(即7),再次增加后7+1=8,一半为4,即0b100,即1<<2.
  5. 关于RC_ONE: 因为extra_rc是从46位开始的,所以extra_rc加1的话,应该是在46位加1,即 (isa.bits + 1<<45)即可。

isa初始化

对象空间开辟完成,需要初始化isa,源码如下:

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;
    }
}
  1. 纯指针isa的设置: 直接bits设置为cls即可
isa = isa_t((uintptr_t)cls);
  1. nonpointer的isa,设置shiftcls: 将cls左移三位
newisa.shiftcls = (uintptr_t)cls >> 3;

左移难道不怕丢失数据吗?毕竟左移后,前3位bit就丢失了。猜测这三位应该都是0才对。po一下,看看:

(lldb) po cls
0x00000001003f00f0

果然。另外,正好也保证了纯指针isa时的nonpointer位的值是0,设计真是巧妙

总结

  1. oc对象利用isa变量存储类对象指针
  2. isa是一个union类型,利用了结构体位域技术,定义了多个数据段,除了class指针还存储额外信息,提高了空间利用率
  3. nonpointer标记了是否存储额外信息
  4. 利用extra_rc和has_sidetable_rc以及sidetable共同通管理引用计数