03 - isa结构探索及关联类

今天的主题是探索isa的结构 在此之前我们需要先了解下什么是联合体

构造数据类型的方式有以下两种 :

  • 结构体 (struct)
  • 联合体 (union ,也称为共用体)

结构体

结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。

  • 缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费

  • 优点:存储容量较大包容性强,且成员之间不会相互影响

联合体

联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖

  • 缺点:,包容性弱

  • 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间

两者的区别

  • 内存占用情况

  • 结构体的各个成员会占用不同的内存,互相之间没有影响

  • 共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员

  • 内存分配大小

  • 结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙)

  • 共用体占用的内存等于最大的成员占用的内存

现在我们已经大致了解什么是联合体,下面进行isa探索,上代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        
        Person *objc = [Person alloc];

        NSLog(@"Hello, World!  %@",objc);
    }
    return 0;
}

alloc的源码跟进去到关联指针和类的步骤:

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

跟进obj->initInstanceIsa(cls, hasCxxDtor);

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

    initIsa(cls, true, hasCxxDtor);
}

主要做的事情是initIsa(cls, true, hasCxxDtor);

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls); // isa 初始化
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);  // isa 初始化
#if SUPPORT_INDEXED_ISA // !nopointmenter 执行的流程 ,即isa通过cls定义
        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 //bits 时执行的流程
        newisa.bits = ISA_MAGIC_VALUE;//bist进行赋值  为0x001d800000000001ULL
        // 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;
    }
}

可以看到不管!nonpointer条件是否满足,都会生成一个isa_t的类型。跟进去可以发现:

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类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

isa_t的定义中可以看出:

提供了两个成员,clsbits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式

通过cls初始化,bits无默认值

通过bits初始化,cls有默认值

还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 __arm64__(对应ios 移动端) 和__x86_64__(对应macOS),以下是它们的一些宏定义,如下图所

位域宏定义

其中存储的成员信息:

  • nonpointer一般自定义的类都是这个类型的,而系统类才会有纯isa指针的情况,占1位。
    0:纯isa指针。
    1:不只是类对象地址,isa中包含了类信息、对象的引用计数等。
  • has_assoc关联对象标志位,0代表没有关联对象,1代表存在关联对象,占1位。
  • has_cxx_dtor该对象是否有C++或OC的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快释放对象,占1位。
  • shiftclx存储类指针的值, 也就是类信息,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,`x86_64``架构中占44位。
  • magic 用于调试器判断当前对象是真的对象还是没有初始化的空间,占6位。
  • weakly_refrenced是指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
  • deallocating 标志对象是否正在释放内存。
  • has_sidetable_rc当对象引用计数大于2的19次方(x86_64架构为2的8次方)时,则需要存储到散列表,这时该变量值变为true
  • extra_rc表示该对象的``引用计数值,最大为2的19次方(x86_64架构为2的8次方),实际上是引用计数值减1,,如果大于最大容量,就需要取一半计数存到散列表中,真机上最多有8张散列表存储对象引用计数,x86_64则最多64张,这时上面的has_sidetable_rc值置为true。

针对两种不同平台,其isa的存储情况如图所示

bits位域分布

下面验证alloc过程中isa是如何跟进行关联的

上面 initIsa方法的源码实现我们可以发现逻辑主要分为两部分

  • 通过 cls 初始化 isa
  • 通过 bits 初始化 isa

运行源码 断点如图 打印newisa信息

调试结果

-继续执行,到newisa.bits = ISA_MAGIC_VALUE;下一行,表示为isa的bits成员赋值,重新执行lldb命令p newisa,得到的结果如下

调试结果

对比发现数据发生变化,可以看到赋值后的nonpointer已经是1了,magic为59。

  • 其中magic59是由于将isa指针地址转换为二进制,从47(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6位,再转换为十进制,如下图所示
计算器结果

此时此刻,可以得出结果,这magic的59确实是由0x001d800000000001ULL填进去的。

继续执行 过newisa.shiftcls = (uintptr_t)cls >> 3;这一步 打印newisa

赋值结果
  • shiftcls0变成了536871965即为 (uintptr_t)cls >> 3
为什么在shiftcls赋值时需要类型强转

因为内存的存储不能存储字符串,机器码只能识别0 、1这两种数字,所以需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解, 其中uintptr_t是long

为什么需要右移3位

主要是由于shiftcls处于isa指针地址的中间部分,前面还有3个位域,为了不影响前面的3位域的数据,需要右移将其抹零

进一步验证类与指针对象相关联

位运算流程图
    1. 控制台打印这个obj指针,并将isa的内存地址右移3位,再左移20位,再右移17位,这时再打印地址移动之后的isa的地址,可以看到,就是我们上面关联的类,也就是说这时对象指针和类关联上了, 如图
    1. 通过isa & ISA_MSAK 如图
位运算验证结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。