iOS对象原理探究:isa结构分析

Clang

我们知道iOS中对象对应到底层都是结构体。那么我们怎么才能分析底层的对象源码呢。所以就引入了Clang

那么什么是Clang呢?

Clang是一个C语言、C++、Objective-C语言的轻量级编译器。它采用了LLVM作为其后端,而且由LLVM2.6开始,一起发布新版本。源代码发布于BSD协议下。

Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C的编译器。

Clang的目标是提供一个GUN编译器套装(GCC)的替代品,支持了GUN编译器大多数的编译设置以及非官方语言的扩展。(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异,并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。

常用的Clang指令

// 把目标文件编译成c++文件
clang -rewrite-objc main.m -o main.cpp 
编译后的文件夹结构
编译后的文件对比

通过编译后,我们可以看出,Person对象,其实对应到底层中其实就是一个结构体,那么我们加些属性,来验证是否真的是这样的。

增加属性编译后的文件对比

通过对比,我们可以看出:

  • Person类中的属性其实就是结构体中的数据成员。
  • Person类中属性的方法,对应到c++中都是对应的静态方法。
  • Person类中的setter方法其实都是统一调用了runtime中的objc_setProperty方法。

那么Person对象对应结构体中的第一个成员(struct NSObject_IMPL NSObject_IVARS;)是什么呢?其实它就是我们今天的重点isa了。

isa 结构

isa初始化流程

我们在iOS对象原理探究:alloc & init & new中,曾提到过一个方法initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->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);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

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

initInstanceIsa方法中直接调用了initIsa

isa_t

通过initIsa的源码可以知道,无论是那个条件中,都是isa_t的结构体。

  • !nonpointer 条件下:isa = isa_t((uintptr_t)cls);
  • nonpointer 条件下:isa_t newisa(0);

同时根据isa_t的结构我们也可以知道其实isa_t中的 clsbits 其实是互斥的。当我们的类型是nonpointer时,bits会有值,当不是nonpointer时,会直接返回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_BITFIELD 是什么呢?ISA_BITFIELD 其实就是我们NONPOINTER_ISA结构

NONPOINTER_ISA 结构(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)
arm64的isa分析图
  • nonpointer:表示是否对 isa 指针开启指针优化(0:纯 isa 指针;1:不止是类对象地址,isa中包含了类信息、对象的引用计数等)
  • has_assoc:关联对象标志位;0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器(dealloc等),如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的还是没有初始化的空间
  • weakly_referenced:指对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快的释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位。(是否有外挂的散列表)
  • extra_rc:额外的引用计数;表示该对象的引用计数值,实际上是引用计数值减1。(eg:如果对象的引用计数为10,那么 extra_rc 为9。如果应用计数大于 10,则需要使用到 has_sidetable_rc)。
__x86_64__ bits赋值

当使用 ISA_MAGIC_VALUE 设置 isa_t 结构体之后,实际上只是设置了 nonpointer 和 magic 这两部分的值。

shiftcls

在设置了 nonpointer、magic、和has_cxx_dtor 之后,我们要将对象对应的类指针存入到 isa 结构体中。

newisa.shiftcls = (uintptr_t)cls >> 3;
类的指针地址

将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。

_class_createInstanceFromZone 方法中打印了调用这个方法传入的类指针:

打印指针

可以看到,这里打印出来的所有类指针十六进制地址的最后一位都为 8 或者 0。也就是说,类指针的后三位都为 0,所以,我们在上面存储 Class 指针时右移三位是没有问题的。

而在此结构中,我们可以知道shiftcls其实就是存储类指针的值,那么我们来验证下到底是否是这样呢?

方式一:验证shiftcls

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

通过initIsa的源码分析,我们可以知道当是nonpointer指针是,我们会先给bits赋值(ISA_MAGIC_VALUE),通过注释其实我们可以知道,magicnonpointer都是ISA_MAGIC_VALUE的一部分,最后我们将cls右移三位就得到了shiftcls

接下来我们尝试将 NSObject 的类指针和对象的 isa 打印出来,具体分析一下:

类指针和isa

代码中打印出来的 isa 结构体中的内容如下:

isa的内容

其中红色部分为类指针,与上面的 [NSObject class] 指针右移三位的结果是完全相同的,这也就验证了我们之前对于初始化 isa 时对 initIsa 方法的分析是正确的。它设置了 indexed、magic 以及 shiftcls。

方式二:验证shiftcls

反推结果

通过图中的lldb调试,其实我们可以知道通过二进制的偏移,可以得出对象的指针值其实就是shiftcls中存储的值。(x86_64的架构)

那么这只是我们的反推结果,Apple是怎么实现的呢?

源码获取isa

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
}

通过源码可以知道,主要是通过 isa.bitsISA_MASK 进行了位与。那么 ISA_MASK 是什么呢,通过NONPOINTER_ISA 结构(ISA_BITFIELD)中我们可以看出 ISA_MASK 的定义。

arm64分析

#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

那么0x0000000ffffffff8ULL是什么呢?为何这个算法这么牛皮呢?

来来来,分析一波!!!

ISA()

0x0000000ffffffff8ULL 其实就是一个转换成二进制是

111111111111111111111111111111111000

其中前三位是0,中间的33位是1,前面都是0了

对应到我们的isa结构中,进行位与,其实就是将前三位和后17位进行了抹零留下了中间的33位。那么这33位其实就是我们的shiftcls的值了,通过上面的分析,其实我们知道类指针默认后三位都是0,所以不需要我们在右移三位就已经是我们得到的类指针了。

你可能感兴趣的:(iOS对象原理探究:isa结构分析)