iOS探索 -- isa的初始化和指向分析

1,166 阅读7分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

前言

在之前的研究中我们大概知道了对象创建和分配内存的过程, 但是在这中间有一个东西还没有去注意。那就是对象的 isa 属性, 只是知道在过程中有伴随着它的一系列初始化操作, 但是这个过程中为什么要创建 isa 呢 ?

isa 的初始化 和 结构分析

1. isa 初始化

在之前的 alloc流程分析 中我们从 objc 源码中看到过对 isa 的初始化过程, 下面我们回过去重新看一下当时的代码:

// callAlloc 方法
obj->initInstanceIsa(cls, dtor);
// 
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.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;
    }
}

从对象创建过程中方法的调用我们不难发现, nonpointer 被默认传的 true 。然后下面的一些判断操作可以理解为对 isa 的一系列的赋值操作。还有重要的一点, 可以看到 isa 的类型为 isa_t 类型, 下面我们来看一看这个类型到底是什么。

补充: 关于Tagged Pointer

可以在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  1. Tagged Pointer 专门用来存储小的对象,例如 NSNumber, NSDate, NSString。

  2. Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free 。

  3. 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

有兴趣的可以去看看这两篇文章 iOS Tagged Pointer , 深入理解 Tagged Pointer

2. isa 的结构分析

在分析结构之前, 我们首先了解两个概念, 联合体位域

  1. 联合体 (union)

在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。(出自百度百科联合体)

1. 联合体是一个结构
2. 它的所有成员相对于首地址的偏移量都为 0
3. 长度取决于各成员中最长的长度
4. 各变量是“互斥”的 —— 共用一个内存首地址,联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值
  1. 位域

位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。 (选自:百度百科)

// 联合体
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 的定义
//  位域
# 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)

可以发现, isa_t 就是一个 联合体 类型, 至于 ISA_BITFIELD 从后面的宏定义可以看出是一个 位域, 所以 isa 的实际类型是一个 联合体, 并且在联合体内部包含一个 位域, 用来存储不同的信息。

下面来展开分析一下:

  1. 首先是两个构造函数 isa_t(), isa_t(unitptr_value)

  2. 然后是三个数据成员 Class cls, unitptr_t bits, struct 。其中 unitptr_t 被定义为 typedef unsigned long uintptr_t , 占64位。

    关于 uintptr_t bitsstruct , 这两个其实可以看成一个成员, 首先他们都占据 64 位内存空间, 并且联合里面的空间是重叠的, 所以他们两个的内存空间是完全重叠的。即 uintptr_t bitsstruct 是一个东西的两种表现形式。

    实际上在 runtime 中,任何对 struct 的操作和获取某些值,如 extra_rc,实际上都是通过对 uintptr_t bits 做位操作实现的。uintptr_t bitsstruct 的关系可以看做,uintptr_t bits 向外提供了操作 struct 的接口,而 struct 本身则说明了 uintptr_t bits 中各个二进制位的定义。

    所以 isa_t 其实可以看做有两个可能的值, Class cls 或者 struct

    内容出处:(Objective-C runtime机制(5)——iOS 内存管理)

  3. 当作为 Class cls 使用时, 就符合了我们一惯的认知, isa 是一个指向对象所属类的指针。但是一个 64位 的指针很显然不划算。所以大多数情况下苹果采用的是优化的 isa 策略, 即 struct

  4. 下面来看一下 struct 里面各个成员所起到的作用

成员作用
nonpointer1bit表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中还包含了类信息、对象的引用计数等
has_assoc1bit关联对象标志位,0没有,1存在
has_cxx_dtor1bit该对象是否有 C++ 或者 Objc 的析构器, 如果有析构函数, 则需要做析构逻辑, 如果没有, 则可以更快的释放对象
shiftcls33bit存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic6bit固定为 0x1a, 用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced1bit指对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放。
deallocating1bit标志对象是否正在释放内存
has_sidetable_rc1bit当对象引用计数大于 19 位时,则需要借用该变量存储进位
extra_rc19bit当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 19 位, 则需要使用到上面的 has_sidetable_rc

以上就是对 isa 结构的分析, 具体的 struct 内部成员分析可以去看一下上面分享的链接。

isa 的指向分析

1. class , objc_getClass () 与 object_getclass () 的区别 ?

开始分析 isa 指向之前, 我们首先了解一下上面这三个方法的区别是什么, 下面我们从 objc 源码里直接找到了上面方法的实现 :

// class ()
+ (Class)class {
    return self;
}
- (Class)class {
    return object_getClass(self);
}

// objc_getClass ()
/***********************************************************************
* objc_getClass.  Return the id of the named class.  If the class does
* not exist, call _objc_classLoader and then objc_classHandler, either of 
* which may create a new class.
* Warning: doesn't work if aClassName is the name of a posed-for class's isa!
**********************************************************************/
Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;

    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}

// object_getclass () 
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
  // ISA_MASK 掩码, 通过 & 运算来获取到 isa 的指向信息
  // #   define ISA_MASK        0x00007ffffffffff8ULL
    return (Class)(isa.bits & ISA_MASK); 
#endif
}

从以上的源码实现我们可以总结如下:

  1. class 方法
// 1. 当为实例方法时, 其返回与 object_getClass () 一致, 即当前对象的 isa 指向的类对象
// 2. 当为类方法时, 返回该类对象本身
  1. objc_getClass ( ) 方法
// 该方法的参数是一个字符串, 当我们传进去一个类名的字符串, 就会返回这个类的类对象
  1. object_getclass ( ) 方法
// 根据传入的参数获取 isa 指向, 需要说明的一点在于
// 当传入的是一个实例对象时, 获取到的是 类对象; 当传入的是一个类对象时, 获取到的是 元类

2. isa 指向分析

首先放上来自官方的指向图:

isa 的指向流程

从标注可以看出, 实线箭头表示继承关系, 虚线箭头表示 isa 指向关系 , 然后从上图我们可以得出 isa 的指向顺序依次为: 对象 --> 类 --> 元类 --> 根元类 --> 根元类 。下面我们来一起验证一下这个过程 (注: 此过程需要用到 lldb 相关打印命令, 有兴趣的可以去自己查询一下。) :

// 定义两个类
// 父类 : YJPerson
// 子类 : YJTeacher
    // YJPerson类
    Class class = object_getClass(person);
    // YJPerson元类
    Class metaClass = object_getClass(class);
    // YJPerson根元类
    Class rootMetaClass = object_getClass(metaClass);
    // YJPerson根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
        
    NSLog(@"\n%p 实例对象 %@\n%p 类 %@\n%p 元类 %@\n%p 根元类 %@\n%p 根根元类 %@",person,person,class,class,metaClass,metaClass,rootMetaClass,rootMetaClass,rootRootMetaClass,rootRootMetaClass);
// 注意: 模拟器情况下是不会对 isa 进行优化的, 所以我们这里选择真机调试

说明: 通过上面的 object_getclass ( ) 方法的内部实现, 我们可以了解到 可以直接通过 isa & ISA_MASK(掩码) 来获取所指向的类, 所以我们下面的验证是通过采用这种方式进行的。

isa 指向验证

  1. 首先通过上面介绍的方法一次获取到 person 对象的 类, 元类, 根元类和根根元类并将结果打印出来
  2. 然后通过 lldb 命令去读取 person 对象的内存情况, 我们知道内存中前 8 位存储的就是我们的 isa, 所以通过 isa & ISA_MASK 方式来获取到 isa 指向的地址并将其打印
  3. 最后得出的结果如上图所示, 通过内存读取的结果与第1步打印的结果完全一致
  4. 还有最重要的一点, 可以发现我们的 根元类根根元类 的地址其实是完全一样的, 然后查看流程图右上角的箭头指向得出结论: 根元类以后的 isa 都是指向根元类本身

总结

通过此篇文章, 主要研究了 isa 相关的一些内容, 包括 isa 的结构组成和指向关系等, 期间也包含了一些内容的补充, 比如: Tagged Pointer、联合体、位域等。这次文章的内容到这里就结束了, 希望内容能对你有所帮助和启发, 如有不对的地方也请大佬们指正, 我们一起探讨共同进步。