联合体位域和isa分析

592 阅读6分钟

一、联合体位域

先定义一个结构体

struct AppleCar1 {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};

打印大小

struct AppleCar1 car1;
NSLog(@"car1 == %zd",sizeof(car1));

结果

001-联合体位域[6268:355746] car1 == 4

上面可知,如果用结构体存储4个方向,需要4字节,接下来我们通过位域方式存储

struct AppleCar2 {
    BOOL front  :1;
    BOOL back   :1;
    BOOL left   :1;
    BOOL right  :1;
};

运行打印一下

struct AppleCar1 car1;
struct AppleCar2 car2;
NSLog(@"car1 ==%zd car2 ==%zd",sizeof(car1),sizeof(car2));

结果

001-联合体位域[6333:361078] car1 ==4 car2 ==1

可知,位域更加节省空间 接下来看一个结构体

struct ApplePerson {
    char    *name;
    int     age;
    double  height;
};

添加断点

image.png

打印一次往下走一步

(lldb) p person1
(ApplePerson1) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person1
(ApplePerson1) $1 = (name = "库某", age = 0, height = 0)
(lldb) p person1
(ApplePerson1) $2 = (name = "库某", age = 18, height = 0)
(lldb) 

说明结构体里面的成员变量都可以赋值,再看一个数据类型共用体

union ApplePerson2 {
    char    *name;
    int     age;
    double  height;
};

下断点

image.png

继续上面相同步骤,打印一次往下走一步

(lldb) p person2
(ApplePerson2) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person2 //给name赋值成功,age和height两个成员变量里面是脏数据
(ApplePerson2) $1 = (name = "库某", age = 15936, height = 2.1220036643954044E-314)
(lldb) p person2//再跑一次,给age赋值成功,name数据是空的,height里面是脏数据
(ApplePerson2) $2 = (name = "", age = 18, height = 2.1219957998584539E-314)
(lldb) 

结论:虽然共用体的成员变量和结构体是一样的,但是结构体的成员变量是共存的,但共用体的成员变量是互斥的。struct特点就是全分配,不管用不用,而union特点就是只分配一个,节省内存。

二、isa分析

之前文章分析到initIsa是对类分配内存空间时候进行了类的绑定

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

initIsa进去

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

再进initIsa

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
      isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}

可以看到 isa = isa_t((uintptr_t)cls);点进去

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_t是一个共用体,再点回去看

if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
} else {
    ...
}

参考:

itpcb.com/a/176630

这里就涉及到nonpointerisa,nonpointer_isa不是一个单纯的类指针,还附带有类的其他信息,因为一个类的指针有8字节,但是类对象的指针并不需要8字节*8位这么大的地址空间,剩下的空间就装了其他的数据记录,比如引用计数、是否被弱引用关联对象析构函数等等关键信息...,再回到isa_t里面

#include "isa.h"

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

点击struct里面的ISA_BITFIELD,这里贴出了isa源码中arm64位架构,查看位域

# 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)

参考:

www.jianshu.com/p/643847183…

参数名用法位域大小位置
nonpointer表示是否对纯isa指针
0:单纯isa指针
1:非单纯isa指针,不止存类对象地址,isa 中包含了类信息、对象的引用计数等等信息
10
has_assoc关联对象
0:没有
1:存在
11
has_cxx_dtor该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象12
shiftcls存储类指针的值。x86_64架构用44位c存储,arm64架构中用33位用存储类指针。333~35
magic用于调试器判断当前对象是真的对象还是没有初始化的空间636~41
weakly_referenced是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。142
deallocating判断是否正在释放内存143
has_sidetable_rc是否需要用到外挂引用计数,当对象引用技术大于 10 则需要借用该变量存储进位144
extra_rc表示引用计数1945~63

从上面看,最重要的数据就是shiftcls,类的指针地址,接下来我们通过对象打印地址来获取isa地址,打个断点

image.png

打印一下

(lldb) x/4gx p
0x106017d20: 0x01000001000082e1 0x0000000000000000
0x106017d30: 0x0000000000000000 0x0000000000000000
(lldb) 

可以看到isa地址是0x01000001000082e1
再打印一下类的地址

(lldb) p/x ApplePerson.class
(Class) $17 = 0x00000001000082e0 ApplePerson

这里我们要从nonpointer_isa地址0x01000001000082e1得到类的地址0x00000001000082e0,这就要用到isa掩码

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL

把isa地址和isa掩码&一下

(lldb) p/x 0x01000001000082e1 & 0x0000000ffffffff8ULL
(unsigned long long) $1 = 0x00000001000082e0

用掩码得出了类的地址,都是0x00000001000082e0p/x ApplePerson.class 得出的地址是一致的
我们通过非纯isa还原shiftcls的反过程,重跑一次断点

image.png

(lldb) x/4gx p
0x106017d20: 0x01000001000082e1 0x0000000000000000
0x106017d30: 0x0000000000000000 0x0000000000000000
(lldb)  

接下来看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)

shiftcls前面有3位,后面28位,我们可以通过平移得到中间的33位,先右移3位,再左移31位,最后右移28位回到原来位置得出33位的指针地址

(lldb) x/4gx p
0x1005ba390: 0x01000001000082e1 0x0000000000000000
0x1005ba3a0: 0x75636f44534e5b2d 0x69766552746e656d
(lldb) p/x 0x01000001000082e1 >> 3
(long) $12 = 0x002000002000105c
(lldb) p/x 0x002000002000105c << 31
(long) $13 = 0x1000082e00000000
(lldb) p/x 0x1000082e00000000 >> 28
(long) $14 = 0x00000001000082e0

通过指针平移,我们可知shiftcls地址是0x00000001000082e0,po 0x00000001000082e0 可以得到类

(lldb) po 0x00000001000082e0
ApplePerson

接下来我们再验证一下

(lldb) p/x ApplePerson.class
(Class) $15 = 0x00000001000082e0 ApplePerson

同样得到 0x00000001000082e0 这个地址,完全一致