对象的本质

alloc底层原理这篇文章主要介绍了,当我们创建一个NSObject的子类的时候,调用alloc方法的流程、类创建的对象实际占用的内存大小分析以及系统分配的内存大小分析、影响对象内存大小的因素等问题。

结构体内存对齐这篇文章主要介绍了,当我们创建结构体的时候对于相同的成员数以及成员类型,顺序不同导致占用内存大小不一致的原因分析、系统对于alloc方法的防Hook操作、alloc函数内存开辟过程。

那么今天这篇文章一起探索下,当我们创建一个NSObject的子类的时候,调用alloc方法的时候,系统是如何把创建的类对象和isa进行绑定的。

initIsa函数

通过alloc底层原理这篇文章我们可以发现,类clsisa进行绑定是在initIsa函数中实现的,那么我们就看下initIsa函数的实现,代码如下:

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

然后继续进入initIsa(cls, false, false);的实现函数中,发现函数实现中有条件分支,先看下条件分支的判断条件,然后进行代码简化,SUPPORT_INDEXED_ISA的定义如下:

// 在将类存储在isa字段中的平台上定义SUPPORT_INDEXED_ISA=1作为类表的索引。注意,要与任何定义它的.s文件保持同步。确保也编辑objc-abi.h。
#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

可以看到默认走的是SUPPORT_INDEXED_ISA=1也就是说分支里面走的是true的判断。所以剔除多余代码后,精简代码如下:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic是ISA_MAGIC_VALUE的一部分
        // isa.nonpointer是ISA_MAGIC_VALUE的一部分
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
        newisa.extra_rc = 1;
    }
    // 在某些情况下,这个写入必须在单个存储中执行(例如,当实现一个类时,因为其他线程可能同时尝试使用这个类)。Fixme使用原子来保证单存储和保证内存顺序。但不要太原子,因为我们不想破坏实例化
    isa = newisa;
}

当简化完成函数实现后,我们开始分析函数中的代码,首先就是创建了一个联合体(又称共用体)对象newisa,那么先去看下这个联合体isa_t的结构,代码如下:

union isa_t {
    // 析构函数,无参数
    isa_t() { }
    // 析构函数,有参数,设置位域信息
    isa_t(uintptr_t value) : bits(value) { }
    
    uintptr_t bits;

private:
    // 访问这个类需要自定义的ptrauth操作,所以强制客户端通过私有来访问setClass/getClass。
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 在isa.h文件中进行了定义,isa位域中每个位置锁代表的含义进行了定义
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

在分析函数中代码前,我们先做下扩展知识,也就是俗话说的备战,了解相关知识点,才能更快更高效的读懂源码。

补充:

  1. uintptr_t:在很多地方都能看见使用这个关键字去定义变量,那么它到底是什么呢?带着好奇心,经过一顿操作,找到这么一段定义,而且是在Linux平台的usr/include/stdint.h头文件中进行的定义。具体代码如下:
    #if __WORDSIZE == 64
    # ifndef __intptr_t_defined
    typedef long int        intptr_t;
    #  define __intptr_t_defined
    # endif
    typedef unsigned long int   uintptr_t;
    #else
    # ifndef __intptr_t_defined
    typedef int         intptr_t;
    #  define __intptr_t_defined
    # endif
    typedef unsigned int        uintptr_t;
    #endif
    

    解读下来就以下几点:

    • 64位系统下long int类型起个别名叫intptr_tunsigned long int类型起个别名叫uintptr_t

    • 32位系统下int类型起个别名叫intptr_tunsigned int类型起个别名叫uintptr_t

    • 至于这么做的原因,应该就是为了提高程序在不同的系统环境下的可移植性。

  2. 位域:在C语言中允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单元的成员称为位域。

结构体和联合体(共用体)的异同

  • 同:结构体联合体都是拥有一个或多个成员的结构型数据类型,而且可以相互嵌套包含。
  • 异:结构体中所有成员之间是可以共存的,能够同时赋值取值。联合体中所有成员之间是互斥的,某一时刻只能有一个值是真实有效的,当给一个成员赋值时其他成员立马就会变成垃圾数据,不是有效值。

MASK(面具)

在iOS开发中经常能够看见一些&(与)运算,~(取反)运算等等,在位域运算中经常会遇到&(与)运算或者位移运算,从而达到访问指定位置的值。运行objc源码的时候都是在mac运行,所以我们主要看下x86架构下的ISA_BITFIELD,如下所示:

#   define ISA_BITFIELD                                                        
      uintptr_t nonpointer        : 1;                                         
      uintptr_t has_assoc         : 1;                                         
      uintptr_t has_cxx_dtor      : 1;                                         
      uintptr_t shiftcls          : 44;
      uintptr_t magic             : 6;                                         
      uintptr_t weakly_referenced : 1;                                         
      uintptr_t unused            : 1;                                         
      uintptr_t has_sidetable_rc  : 1;                                         
      uintptr_t extra_rc          : 8

x86架构下的ISA_BITFIELD中每个位置代表的信息如图所示:

x86架构下示意图:

image.png

arm64架构下示意图:

image.png

nonpointer在0位,表示是否对isa指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等。

has_assoc在1位,表示关联对象标志位,0:没有,1:有。

has_cxx_dtor在2位,表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。

shiftclsx86架构中占用3~46位,表示存储类指针的值。开启指针优化的情况下,在arm64架构中占用3~35位。

magicx86架构中占用47~52位,在arm64架构中占用36~41位,用于调式器判断当前对象是真的对象还是没有初始化的空间。

weakly_referencedx86架构中占用53位,在arm64架构中占用42位,标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。

unusedx86架构中占用54位,在arm64架构中占用43位,标志对象是否正在释放内存。

has_sidetable_rcx86架构中占用55位,在arm64架构中占用44位,表示当对象引用计数大于10时,则需要借用该变量存储进位。

extra_rcx86架构中占用56~63位,在arm64架构中占用45~63位,当表示该对象的引用计数值时,实际上是引用计数值减1,例如:如果对象的引用计数为10,那么extra_rc9,如果引用计数大于10,则需要使用到has_sidetable_rc

initIsa函数实现

当了解这些扩展知识后,回到开始的isa_t结构体,主要是setClass函数,函数有一段注释,大概意思就是在isa中设置类字段。接受要设置的类和指向最终将使用isa的对象的指针。这是使指针正确签名所必需的。注意:此方法不支持设置索引的isa。当使用索引isa时,它只能用于设置原始isa的类。然后这个函数中重点代码为这一句uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));,因为我们要将结果指针作为函数指针调用,因此需要对其进行签名,也就是对传入的newCls进行签名。

然后回到开始的initIsa函数,当创建一个联合体对象newisa后,先进行了判断传入的nonpointer是否为false,从函数执行流程跟进来的时候,就能看见调用initIsa的时候写死的传入false,所以条件分支走进newisa.setClass(cls, this);,然后继续来到函数setClass的实现中,因为条件分支比较多,最笨的方法那就是挨个添加断点,看走的那个,如下图:

image.png

断点走的是最后的shiftcls = (uintptr_t)newCls >> 3;这句代码,上面补充中说了,这个值是用来存储类指针的值的。

你可能感兴趣的:(对象的本质)