本文代码运行在 Mac OS 上,也就是运行在 x86_64 架构下,对于运行在 arm64 架构下的代码大同小异

1、了解 isa 是什么

如果大家对 ObjC 的底层实现有一定了解的话,应该知道 Objective-C 对象都是 C 语言结构体,所以的对象都包含一个类型为结构体的 isa_t,这个结构体中包含了当前对象指向的类的信息

  1. struct objc_object {
  2. isa_t isa;
  3. }

ObjC 为一个对象分配内存并初始化变量后,在这些对象的实例变量的结构体中第一个就是 isa。所有继承自 NSObject 的类实例化后的对象都会包含一个类型为 isa_t 的结构体。

image.png

从上图可以看出,不只是 实例对象 会包含一个 isa 结构体,所以的 类对象 也会包含这个一个 isa。在 ObjCClass 的定义也是一个名为 objc_class 的结构体。

  1. struct objc_class : objc_object {
  2. isa_t isa;
  3. Class superclass;
  4. cache_t cache;
  5. class_data_bits_t bits;
  6. }

由于 objc_class 结构体继承自 objc_object,所以在上诉代码中显示地写出了 isa_t isa 这个成员变量。

到这里,我们就可以得出一个结论:Objective-C 中类也是一个对象

2、特殊结构体 isa_t

我们以 x86_64 架构下为例,可以在 ObjC 源码中可以看到这样的定义:

  1. # define ISA_MASK 0x00007ffffffffff8ULL
  2. # define ISA_MAGIC_MASK 0x001f800000000001ULL
  3. # define ISA_MAGIC_VALUE 0x001d800000000001ULL
  4. # define ISA_BITFIELD \
  5. uintptr_t nonpointer : 1; \
  6. uintptr_t has_assoc : 1; \
  7. uintptr_t has_cxx_dtor : 1; \
  8. uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
  9. uintptr_t magic : 6; \
  10. uintptr_t weakly_referenced : 1; \
  11. uintptr_t deallocating : 1; \
  12. uintptr_t has_sidetable_rc : 1; \
  13. uintptr_t extra_rc : 8
  14. # define RC_ONE (1ULL<<56)
  15. # define RC_HALF (1ULL<<7)
  16. union isa_t {
  17. isa_t() { }
  18. isa_t(uintptr_t value) : bits(value) { }
  19. Class cls;
  20. uintptr_t bits;
  21. #if defined(ISA_BITFIELD)
  22. struct {
  23. ISA_BITFIELD; // defined in isa.h
  24. };
  25. #endif
  26. };

isa_t 是一个 union 的结构体,也就是说其中的 clsbits 共用同一块内存地址空间,而 isa 总共会占据 64 位的内存空间。

如果对 union 不太熟悉,可以参考一下这篇文章 结构体(struct)和联合体(union)的区别

image.png
以下是 isa 64位 bits 所代表的意思。

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

3、isa 的初始化

我们可以通过 isa 初始化的方法 initIsa 来初步了解这 64 位的 bits 的作用:

  1. inline void
  2. objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
  3. {
  4. initIsa(cls, true, hasCxxDtor);
  5. }
  6. inline void
  7. objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
  8. {
  9. if (!nonpointer) {
  10. isa = isa_t((uintptr_t)cls);
  11. } else {
  12. isa_t newisa(0);
  13. newisa.bits = ISA_MAGIC_VALUE;
  14. newisa.has_cxx_dtor = hasCxxDtor;
  15. newisa.shiftcls = (uintptr_t)cls >> 3;
  16. isa = newisa;
  17. }
  18. }

3.1 nonpointer 和 magic

当我们对一个 ObjC 对象分配内存时,其方法调用栈中包含了上述的两个方法,这里关注的重点是 initIsa 方法,由于在 initInstanceIsa 方法中传入了 nonpointer = true,所以,我们简化一下这个方法的实现:

  1. inline void
  2. objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
  3. {
  4. newisa.bits = ISA_MAGIC_VALUE;
  5. newisa.has_cxx_dtor = hasCxxDtor;
  6. newisa.shiftcls = (uintptr_t)cls >> 3;
  7. }

对整个 isa 的值 bits 进行设置,传入 ISA_MAGIC_VALUE

  1. #define ISA_MAGIC_VALUE 0x001d800000000001ULL

我们先在工程中进行源码调试,得到以下结果

image.png

nonpointer 值为1,相信大家都知道,那么 magic 的值为什么为59呢,大家就可能迷糊了,我们可以把 ISA_MAGIC_VALUE 的值输入到计算器中

image.png

然后我们输入十进制的59,然后看二进制

image.png

看到这,大家明白为什么 magic 的值为 59 了吗?

所以我们可以把它转换成二进制的数据,然后看一下哪些属性对应的位被这行代码初始化了(标记为红色):

image.png

从图中了解到,在使用 ISA_MAGIC_VALUE 设置 isa_t 结构体之后,实际上只是设置了 nonpointer 以及 magic 这两部分的值。

  • 其中 nonpointer 表示 isa_t 的类型

    • 0: 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。

      1. union isa_t {
      2. isa_t() { }
      3. isa_t(uintptr_t value) : bits(value) { }
      4. Class cls;
      5. uintptr_t bits;
      6. };
    • 1: 表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。

      1. union isa_t {
      2. isa_t() { }
      3. isa_t(uintptr_t value) : bits(value) { }
      4. Class cls;
      5. uintptr_t bits;
      6. #if defined(ISA_BITFIELD)
      7. struct {
      8. ISA_BITFIELD; // defined in isa.h
      9. };
      10. #endif
      11. };

3.2 has_cxx_dtor

在设置 nonpointermagic 值之后,会设置 isahas_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。

  1. newisa.has_cxx_dtor = hasCxxDtor;

image.png

3.3 shiftcls(重点)

在为 nonpointermagichas_cxx_dtor 设置之后,我们就要将当前对象对应的类指针存入 isa 结构体中了。

  1. newisa.shiftcls = (uintptr_t)cls >> 3;

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

我们继续通过跟踪源码来说明 shiftcls 存储的就是类的指针信息

image.png

上图的输出结果表明,shiftclscls 已经绑定在一起了,我们继续跟踪断点来进一步说明

image.png

上图红框打印的地址,其实就是当前类的 isa 地址,继续往下走

image.png

这次我们将 isa & ISA_MASK 得到了我们的 Class。这也就验证了我们之前对于初始化 isa 时对 initIsa 方法的分析是正确的。它设置了 nonpointermagic 以及 shiftcls

3.4 getIsa() 方法

通过掩码的方式获取类指针:

  1. Class object_getClass(id obj)
  2. {
  3. if (obj) return obj->getIsa();
  4. else return Nil;
  5. }
  6. inline Class
  7. objc_object::getIsa()
  8. {
  9. return ISA();
  10. }
  11. inline Class
  12. objc_object::ISA()
  13. {
  14. return (Class)(isa.bits & ISA_MASK);
  15. }

3.5 其它 bits

isa_t 的其他 bits,我们在前面已经简要介绍过了,这里就不再详细展开分析。

4、参考资料

Objective-C Runtime Programming Guide