阅读 467

OC源码分析之isa

前言

想要成为一名iOS开发高手,免不了阅读源码。以下是笔者在OC源码探索中梳理的一个小系列——类与对象篇,欢迎大家阅读指正,同时也希望对大家有所帮助。

  1. OC源码分析之对象的创建
  2. OC源码分析之isa
  3. OC源码分析之类的结构解读
  4. OC源码分析之方法的缓存原理
  5. 未完待续...

1. isa介绍

1.1 isa是什么

OC源码分析之对象的创建 一文中,我们知道alloc底层会调用calloc分配内存,接着就是initInstanceIsa(cls, hasCxxDtor),顾名思义是初始化对象的isa,其关键代码如下

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

    // 留意这里的true
    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;
    }
}

复制代码
  1. 关于Tagged Pointer

据说,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入Tagged Pointer后,相关逻辑能减少一半的内存占用,以及 3倍 的访问速度提升,100倍 的创建、销毁速度提升。

Tagged Pointer首次应用于iPhone 5s设备上,现在几乎都应用Tagged Pointer了。想了解更多关于Tagged Pointer的内容可戳 深入理解 Tagged Pointer

  1. isa_t类型

点击objc_object::initIsa()中的isa,发现isaisa_t类型

struct objc_object {
private:
    isa_t isa;
    
    ... // 一些公有、私有方法
};
复制代码

isa_t实际上是一个union(即联合体,也叫共用体)

这里先普及一下structunion的区别

  1. 两者都可以包含多个不同类型的数据,如intdoubleClass等。
  2. struct中各成员有各自的内存空间,一个struct变量的内存总长度大于等于各成员内存长度之和;而在union中,各成员共享一段内存空间,一个union变量的内存总长度等于各成员中内存最长的那个成员的内存长度。
  3. struct中的成员进行赋值,不会影响其他成员的值;对union中的成员赋值时,每次只能给一个成员赋值,同时其它成员的值也就不存在了。

isa_t包含了clsbits两个成员变量,其结构如下

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
};
复制代码

1.2 isabits成员变量

  1. 位域

这里普及一下位域的概念

位域是一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。

  • 优点:
    • 节省储存空间;
    • 可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。
  • 缺点:
    • 其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位域在本质上是不可移植的。

isabits成员变量类型是uintptr_t,它实质上是个unsigned long

typedef unsigned long           uintptr_t;
复制代码

64位CPU架构下bits长度为64位,也就是8字节,其各个位的存储就使用了位域,即ISA_BITFIELD

  1. ISA_BITFIELD

接下来看一下ISA_BITFIELD的源码(由于笔者是用macOS项目研究OC底层源码,所以这里以x86_64架构为例)

# 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)
复制代码

首先明确一点,在64位CPU架构下isa指针的长度也是8字节,它可以存储足够多的内容,苹果为了优化性能,存储类地址只用了一部分位(x86_64下是44位,arm64下是33位),剩下的位用来存储一些其它信息。

具体分析一下ISA_BITFIELD位域各成员的表示意义:

  • nonpointer:表示是否对 isa指针 开启指针优化。
    • 0:不优化,是纯isa指针,当访问isa指针时,直接返回其成员变量cls
    • 1:优化,即isa 指针内容不止是类地址,还包含了类的一些信息、对象的引用计数等。
  • has_assoc:是否有关联对象。
  • has_cxx_dtor:该对象是否有C++或Objc的析构器。
    • 如果有析构函数,则需要做一些析构的逻辑处理;
    • 如果没有,则可以更快的释放对象。
  • shiftcls:存储类地址。开启指针优化的情况下,在 x86_64 架构有 44位 用来存储类地址,arm64 架构中有 33位
  • magic:用于调试器判断当前对象是真的对象,还是一段没有初始化的空间。
  • weakly_referenced:用于标识对象是否被指向或者曾经被指向一个ARC的弱变量,没有弱引用的对象释放的更快。
  • deallocating:标识对象是否正在释放内存。
  • has_sidetable_rc:对象的引用计数值是否有进位。
  • extra_rc:表示该对象的引用计数值。extra_rc只是存储了额外的引用计数,实际的引用计数公式:实际引用计数 = extra_rc + 1。这里占了8位,所以理论上可以存储的最大引用计数是:2^8 - 1 + 1 = 256arm64CPU架构下的extra_rc占19位,可存储的最大引用计数为2^19 - 1 + 1 = 524288)。
    • has_sidetable_rc的关联:当对象的最大引用计数超过界限后,has_sidetable_rc的值为1,否则为0

1.3 isacls成员变量

分析完isa的位域,接下来就只剩下cls,它是Class类型,同样上源码

typedef struct objc_class *Class;
// 顺便了解一下id的类型,显然id是个指针变量,它的值只有一个isa变量
typedef struct objc_object *id;

struct objc_class : objc_object {
    // Class ISA; 
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits;

    class_rw_t *data() { 
        return bits.data();
    }
    ... // 一些方法
};

struct objc_object {
private:
    isa_t isa;
    
    ... // 一些公有、私有方法
};

复制代码

从源码得知,Class实际上是objc_class结构体的指针变量,而objc_class又继承自objc_object(说明类本质上也是一个对象),因此Class这个结构体指针变量的值内部有一个isa成员变量(类型为isa_t),这个isa成员变量在64位CPU架构下是8字节,且排在objc_class结构体的前8字节。

1.4 isa的作用

objc_object的结构可以说明,当系统为一个对象分配好内存,并初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是isa

同时,通过对isa的位域说明,我们知道shiftcls存储的是类地址。在x86_64架构下,shiftcls占用44位,也就是第[3, 46]位。将 [3, 46]位 全部填充1,[0, 2]位 和 [47, 63]位 都补0,得到0x7ffffffffff8,也就是ISA_MASK的值。故,isa & ISA_MASK会得到shiftcls存储的类地址。这也就是所谓MASK的作用。

如下图所示

下面用一个例子说明isa的作用

此时通过lldb命令调试

说明:

  1. 0x001d800100001129对象pisa值,通过isa & ISA_MASK运算得到的0x0000000100001128就是Person类的地址
  2. 证明【1】:通过p/x Person.class直接打印Person类地址,显然得到的是0x0000000100001128,如此【1】证明成立!

结论:isa将对象和类关联起来,起到了中间桥梁的作用。

注意:对 shiftcls 的分析是建立在 nonpointer 为 1 的情况下,如果 nonpointer 为0,整个 isa 存储的是 cls 。

思考:如果不用ISA_MASK,那么如何证明isa的这个作用呢?——答案将在文末补充。

1.5 isa的初始化补充

最后补充一下isa的初始化。还记得初始化isa的入口吗?是initIsa(cls, true, hasCxxDtor);,此时nonpointer的值是true,再看SUPPORT_INDEXED_ISA的定义

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif
复制代码

x86_64下,SUPPORT_INDEXED_ISA是0,所以isa的初始化最终会来到

isa_t newisa(0);

// 使用ISA_MAGIC_VALUE(0x001d800000000001ULL)赋值给bits
// nonpointer为1,magic为1d,其他变量为零
newisa.bits = ISA_MAGIC_VALUE;
// hasCxxDtor是从类的isa中取出的
newisa.has_cxx_dtor = hasCxxDtor;
// 将cls右移3位后赋值给shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;

isa = newisa;
复制代码

碍于篇幅,这里不继续深入hasCxxDtor

2 isa指向图

通过以上的源码分析,我们认识到对象的isa指针指向了对象所属的类。而类本身也有一个isa指针,它指向的又是什么呢?

此时要引入meta class(即元类)的概念了。我们先了解一下元类的信息

OC中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。

当对象的实例方法被调用时,它通过自己的isa来查找对应的类,然后在所属类的 class_data_bits_t结构体中查找对应方法的实现。同时,每一个objc_class 也有一个指向自己的父类的指针superclass用来查找继承的方法。

而当调用 类方法 时,它的查找流程是怎样的呢?对此OC的解决方案就是引入元类,来保证类方法也能通过相同的机制查找到。也就是说,类的isa指向的是元类。

苹果官方有个isa指向图,即

接下来我们来验证一下吧。

2.1 准备工作

创建Teacher类、Person类,其中Person类继承于NSObjectTeacher类继承于Person类。

对比isa指向图,对号入座后就是,Teacher类相当于SubclassPerson类相当于SuperclassNSObject相当于Root class

2.2 验证过程

  1. 获取teacher对象的类(结果是Teacher类,地址为0x0000000100001230
(lldb) x/4gx teacher
0x100f59400: 0x001d800100001231 0x0000000000000000
0x100f59410: 0x636f72504b575b2d 0x70756f7247737365
(lldb) p/x 0x001d800100001231 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $3 = 0x0000000100001230      // 对象teacher的类地址
(lldb) po $3
Teacher     // 对象teacher的类
复制代码
  1. 获取Teacher类的元类(结果是Teacher元类,地址为0x0000000100001208
(lldb) x/4gx Teacher.class
0x100001230: 0x001d800100001209 0x00000001000011e0
0x100001240: 0x0000000100f61150 0x0000000100000003
(lldb) p/x 0x001d800100001209 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $5 = 0x0000000100001208      // Teacher类的元类地址
(lldb) po $5
Teacher     // Teacher类的元类
复制代码

Teacher类 和 Teacher元类 地址不一样

  1. 获取person对象的类(结果是Person类,地址为0x00000001000011e0),以及类的元类(结果是Person元类,地址为0x00000001000011b8
(lldb) x/4gx person
0x100f60a30: 0x001d8001000011e1 0x0000000000000000
0x100f60a40: 0x0000000000000002 0x00007fff9b855588
(lldb) p/x 0x001d8001000011e1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $8 = 0x00000001000011e0      // 对象person的类地址
(lldb) po $8
Person      // 对象person的类

(lldb) x/4gx Person.class
0x1000011e0: 0x001d8001000011b9 0x0000000100b38140
0x1000011f0: 0x0000000100f61030 0x0000000100000003
(lldb) p/x 0x001d8001000011b9 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $10 = 0x00000001000011b8     // Person类的元类地址
(lldb) po $10
Person      // Person类的元类
复制代码
  1. 获取object对象的类(结果是NSObject类,地址为0x0000000100b38140),以及类的元类(结果是NSObject元类,地址为0x0000000100b380f0
(lldb) x/4gx object
0x100f5cc50: 0x001d800100b38141 0x0000000000000000
0x100f5cc60: 0x70736e494b575b2d 0x574b57726f746365
(lldb) p/x 0x001d800100b38141 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $12 = 0x0000000100b38140     // 对象object的类地址
(lldb) po $12   
NSObject    // 对象object的类

(lldb) x/4gx NSObject.class
0x100b38140: 0x001d800100b380f1 0x0000000000000000
0x100b38150: 0x0000000101913060 0x0000000200000003
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $14 = 0x0000000100b380f0     // NSObject类的元类地址
(lldb) po $14
NSObject    // NSObject类的元类
复制代码
  1. 获取Teacher元类的元类,Person元类的元类,以及NSObject元类的元类
(lldb) x/4gx 0x0000000100001208     //  Teacher元类
0x100001208: 0x001d800100b380f1 0x00000001000011b8
0x100001218: 0x000000010186f950 0x0000000400000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $16 = 0x0000000100b380f0     // NSObject元类
(lldb) po $16
NSObject    // NSObject元类

(lldb) x/4gx 0x00000001000011b8     // Person元类
0x1000011b8: 0x001d800100b380f1 0x0000000100b380f0
0x1000011c8: 0x0000000101905a50 0x0000000300000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $17 = 0x0000000100b380f0     // NSObject元类
(lldb) po $17
NSObject    // NSObject元类

(lldb) x/4gx 0x0000000100b380f0     // NSObject元类
0x100b380f0: 0x001d800100b380f1 0x0000000100b38140
0x100b38100: 0x0000000101903820 0x0000000500000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8  // isa & ISA_MASK
(long) $18 = 0x0000000100b380f0     // NSObject元类
(lldb) po $18
NSObject    // NSObject元类
复制代码

2.3 isa指向结论

基于【2.2】的验证过程,可以得出结论:

  • 对象的isa指针 指向 对象的所属类(如person对象的isa指向Person类)
  • 类的isa指针 指向 类的元类(如Person类的isa指向Person元类
  • 元类的isa指针 指向 根元类(如Person元类isa指向NSObject元类
    • NSObject类的元类是根元类
    • NSObject元类isa指针 指向自身(是个圆圈)

思考:如果Person类继承的是NSProxy,相关isa指向是怎样的呢?感兴趣的可以去试试。

2.4 继承关系的证明

类的继承关系证明过程:(以 -> 表示 继承自)

(lldb) p class_getSuperclass(Teacher.class)
(Class) $19 = Person    // Teacher类 -> Person类

(lldb) p class_getSuperclass(Person.class)
(Class) $20 = NSObject  // Person类 -> NSObject类

(lldb) p class_getSuperclass(NSObject.class)
(Class) $21 = nil       // NSObject类 -> nil
复制代码

元类的继承关系证明过程:(以 -> 表示 继承自)

// 0x0000000100001208 是 Teacher元类
(lldb) p/x class_getSuperclass((Class)0x0000000100001208)
(Class) $17 = 0x00000001000011b8    // Person元类
(lldb) po $17
Person      // Teacher元类 -> Person元类

// 0x00000001000011b8 是 Person元类
(lldb) p/x class_getSuperclass((Class)0x00000001000011b8)
(Class) $22 = 0x0000000100b380f0    // NSObject元类(根元类)
(lldb) po $22
NSObject    // Person元类 -> 根元类

// 0x0000000100b380f0 是 根元类
(lldb) p/x class_getSuperclass((Class)0x0000000100b380f0)
(Class) $23 = 0x0000000100b38140 NSObject   // NSObject类(根类)
(lldb) po $23
NSObject    // 根元类 -> 根类
复制代码

根元类继承自根类(NSObject元类 -> NSObject类),根类继承自nil(NSObject类 -> nil)

2.5 isa指向图涂鸦版

把上面的例子涂在isa指向图上,就得到了下图

3. 总结

  1. isaisa_t结构,采用 联合体+位域 的搭配来设计:在不同的位上显示不同的内容,以此来节省储存空间,进而优化内存。
  2. isa包含了clsbits两个成员变量,这两个成员变量在64位CPU架构下的长度都是8字节,所以isa64位CPU架构下的长度也是8字节。
  3. isa的位域上存储了一些对象与类的信息,并将对象与类关联起来,起到中间桥梁的作用。
  4. isa指向图相关结论:
    • 对象的isa指针 指向 对象的所属类(如person对象的isa指向Person类)
    • 类的isa指针 指向 类的元类(如Person类的isa指向Person元类
    • 元类的isa指针 指向 根元类(如Person元类isa指向NSObject元类
      • 根元类isa指针 指向自身(是个圆圈)
    • 元类的继承关系向上传递(如Teacher元类 继承自 Person元类
      • 根元类 继承自 根类
      • 根类 继承自 nil

4. 补充

4.1 isa的作用的证明2

Q:如果不用ISA_MASK,那么如何证明isa关联了对象和类的作用呢?

A:具体思路是,shiftclsx86_64架构下长度是44位,存储在isa的 [3, 46]位上,所以可以通过将isa的 [0, 2]位、[47, 63]位清零,同样能得到shiftcls的值,进而确定类。

如图所示,经过对isa的一番运算,成功得到与Person类相同的地址。

4.2 NSProxyisa指向

Q:如果Person类继承的是NSProxy,相关isa指向是怎样的呢?

A:跟NSObject一样,两者都是根类

参考资料

从 NSObject 的初始化了解 isa(by Draveness

PS

关注下面的标签,发现更多相似文章
评论