iOS进阶之路 (三)OC对象的原理 - isa 结构 & 走位

1,583

学习之前,我们先补充下位域和联合体的知识。

1. 位域

1.1 位域的定义

所谓位域就是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作——这样就可以把几个不同的对象用一个字节的二进制位域来表示。位域是C语言一种数据结构。

使用位域的好处是:

  • 有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。这样节省存储空间,而且处理简便。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
  • 可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间。

1.2 位域的使用

在C语言中,位域的声明和结构体(struct)类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际储存在一个或多个整型变量中。

在声明时,位域成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。

位域不能是静态类型。不能使用&对位域做取地址运算,因此不存在位域的指针,编译器通常不支持位域的引用(reference)。

// 结构体
struct Struct {
    // (数据类型 元素);
    char a; // 1字节 0 补1 2 3
    int b;  // 4字节 4 5 6 7
} Str;

// 位域
struct BitArea {
    // (数据类型 位域名: 位域长度);
    char a: 1;
    int b: 3;
} Bit;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Struct:%lu——BitArea:%lu", sizeof(Str), sizeof(Bit));
    }
    return 0;
}

2.联合体

2.1 联合体的定义

联合体(union,又叫共用体):使几种不同类型的变量存放到同一段内存单元中。即使用覆盖技术,几个变量互相覆盖重叠。

union  MyValue{
    // (数据类型 元素)
    int x;
    int y;
    double z;
};

void main(){
    union MyValue d1;
    d1.x = 90;
    d1.y = 100; 
    d1.z = 23.8; // 最后一次赋值有效

    printf("%d,%d,%lf\n",d1.x,d1.y,d1.z);
}

// 输出结果:
-858993459,-858993459,23.800000

联合体定义使用时注意点:

  • union中可以定义多个成员,union的大小由最大的成员的大小决定。
  • union成员共享同一块大小的内存,一次只能使用其中的一个成员。
  • 对某一个成员赋值,会覆盖其他成员的值,因为他们共享一块内存。
  • union中各个成员存储的起始地址都是相对于基地址的偏移都为0。

2.2 联合体和结构体区别:

  1. 结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
  2. 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉

3. isa 的结构

在之前的iOS探索alloc流程中,我们提了一句obj->initInstanceIsa(cls, hasCxxDtor)在内部调用initIsa(cls, true, hasCxxDtor)初始化isa,今天就分析下isa。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        } else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
}

3.1 isa 的初始化

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = 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;
    }
}
  • TaggedPointer 专门用来存储小的对象(8-10),例如NSNumber和NSDate。
  • 创建对象跟着断点发现nonpointer为true
  • else流程走SUPPORT_INDEXED_ISA,表示isa_t中存放的 Class信息是Class 的地址,还是一个索引(根据该索引可在类信息表中查找该类结构地址)

3.2 isa_t 的结构

union isa_t {
    isa_t() { }                                 // 初始化方法1
    isa_t(uintptr_t value) : bits(value) { }    // 初始化方法2

    Class cls;                                  // 成员1
    uintptr_t bits;                             // 成员2
#if defined(ISA_BITFIELD)
    struct {                                    // 成员3
        ISA_BITFIELD;  // defined in isa.h      // 位域宏定义
    };
#endif
};

通过源码我们发现isa它一个联合体,8个字节,它的特性就是共用内存,或者说是互斥(比如说如果cls赋值了,再对bits进行赋值时会覆盖掉cls)。在isa_t联合体内使用宏ISA_BITFIELD定义了位域,我们进入位域内查看源码。

3.3 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

  1. nonpointer:是否对isa指针开启指针优化
  • nonpointer = 0:不优化,纯isa指针,当访问isa指针时,直接通过isa.cls和类进行关联,返回其成员变量cls
  • nonpointer = 1:优化过的isa指针,指针内容不止是类对象地址,还会使用位域存放类信息、对象的引用计数,此时创建newisa并初始化后赋值给isa指针。 如果没有,则可以更快的释放对象。
  1. has_assoc:是否有关联对象,0没有,1存在。
  2. has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  3. shiftcls:存储类对象和元类对象的指针的值,在开启指针优化的情况下,在 arm64 架构中用 33 位用来存储类指针
newsisa.shiftcls = (uintptr_t)cls >> 3
  1. magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  2. weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
  3. deallocating:标志对象是否正在释放内存
  4. has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位(rc = retainCount
  5. extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下has_sidetable_rc

isa_t联合体有3个成员(Class cls、uintptr_t bits、联合体+位域ISA_BITFIELD),3个成员共同占用8字节的内存空间,通过ISA_BITFIELD里面的位域成员,可以对8字节空间的不同二进制位进行操作,达到节省内存空间的目的。

联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型

3.4 shiftcls关联类

在shiftcls中存储着类对象和元类对象的内存地址信息,我们重点看一下newisa.indexcls = (uintptr_t)cls->classArrayIndex()和uintptr_t shiftcls : 33这两行源码。

上篇文章中我们提到,在Person实例对象里面可能因为内存优化,属性的位置可能发生变换(比如ch1和ch2)。但是对象内存的第一个属性必然是isa。因为isa来自于NSObject类,是继承过来的,根本还没有编辑属性列表(关于ro/rw我们后续章节会提到)。

我们就测试下,person的第一个属性是不是isa。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        objc_getClass();
    }
    return 0;
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

inline Class 
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}
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 __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

打印出isa & mask的值,与class第一段地址比较。

实例对象首地址一定 是isa。实例对象通过isa & isa_mask关联类。

3.5 思考

如果我们把联合体中的位域换成基本数据类型来表示,结合内存对齐原则,ISA_BITFIELD占用24个字节。 通过位域,每一个继承自NSObject的对象都至少减少了16字节的内存空间。

// 在arm64下将位域换成基本数据类型
struct isa_t_bitFields {
    unsigned char nonpointer;           // 1字节  0
    unsigned char has_assoc;            // 1字节  1
    unsigned char has_cxx_dtor;         // 1字节  2 补3 4 5 6 7
    unsigned long shiftcls;             // 8字节  8 9 10 11 12 13 14 15
    unsigned char magic;                // 1字节  16
    unsigned char weakly_referenced;    // 1字节  17
    unsigned char deallocating;         // 1字节  18
    unsigned char has_sidetable_rc;     // 1字节  19
    unsigned int extra_rc;              // 4字节  20 21 22 23
};

4. isa走位

4.1 类在内存中只存在一个

Class class1 = [Person class];
Class class2 = [Person alloc].class;
Class class3 = object_getClass([Person alloc]);
Class class4 = [Person alloc].class;

NSLog(@"\n%p\n%p\n%p\n%p", class1, class2, class3, class4);
0x1000020f0
0x1000020f0
0x1000020f0
0x1000020f0

类在内存中只会存在一个,而实例对象可以存在多个。

4.2 通过对象/类查看isa走位

  • 实例对象由类实例化出来。实例对象 和 类 通过isa关联
  • 类本质上也是对象,通过元类实例化出来。类对象 和 元类通过isa关联:
  • 实例对 - isa - 类 - isa - 元类

我们模仿object_getClass,通过isa & isa_mask,得到对象通过isa关联的类。

  1. 打印AKPerson类取得isa
  2. 由AKPerson类进行偏移得到AKPerson元类指针,打印AKPerson元类取得isa
  3. 由AKPerson元类进行偏移得到NSObject根元类指针,打印NSObject根元类取得isa
  4. 由NSObject根元类进行偏移得到NSObject根元类本身指针
  5. 打印NSObject根类取得isa
  6. 由NSObject根类进行偏移得到NSObject根元类指针

4.3 通过NSObject查看isa走位

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // NSObject实例对象
        NSObject *object1 = [NSObject alloc];
        // NSObject类
        Class class = object_getClass(object1);
        // NSObject元类
        Class metaClass = object_getClass(class);
        // NSObject根元类
        Class rootMetaClass = object_getClass(metaClass);
        // NSObject根根元类
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1, class, metaClass, rootMetaClass, rootRootMetaClass);
    }
    return 0;
}
0x103239120 实例对象
0x7fff9498b118 类
0x7fff9498b0f0 元类
0x7fff9498b0f0 根元类
0x7fff9498b0f0 根根元类

1.实例对象-> 类对象 -> 元类 -> 根元类 -> 根元类(本身)
2.NSObject(根类) -> 根元类 -> 根元类(本身)
3.指向根元类的isa都是一样的

4.4 类、元类是由系统创建的

1.对象是程序猿根据类实例化的。
2.类是代码编写的,内存中只有一份,是系统创建的。
3.元类是系统编译时,系统编译器创建的,便于方法的编译

4.5 isa走位图

isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
继承关系(实现):子类 -> 父类 -> NSObject -> nil。 根元类的父类为NSObject。

5. 参考资料

带你深入理解iOS-位域

结构体、联合体、枚举