iOS进阶 — 类的底层初探

前言

在前三篇文章中,我们从以下几个角度探索类 OC 对象相关的底层原理。

从本篇文章开始,我们进入 OC 类的篇章,本篇就是第一篇关于 OC 类的初探。本文将从以下几个角度进行探索,下面我们一起开始吧。

  • 类的底层结构 objc_class 分析
  • 类的 isa关系链 及 继承关系链
  • 类的数据 class_rw_t 分析(属性及实例方法)

一、类的底层结构 objc_class 分析

对象的本质 这篇文章中探索 Class 时发现,Class 其实是一个 objc_class 指针,那么 objc_class 到底是什么呢?其内部结构是怎样的呢?我们现在就来探索一下。

首先,我们打开 libobjc源码,全局搜索 struct objc_class 结果如下:

Snip20210620_2.png

可以发现在源码中,有多个地方对 struct objc_class 有定义,例如 runtime.h、objc-runtime-old.h、objc-runtime-new.h ,通对比发现 objc-runtime-new.h 才是目前正在使用的定义,其定义的代码如下:

Snip20210620_3.png
Snip20210620_4.png

由代码可以发现 objc_class 继承于 objc_object,其包含成员有 ISA(在objc_object中)、superclass、cache、bits,其余均为方法,不会存储在该结构体中,这几个成员的定义的代码如下:

    // Class ISA;  // 在objc_object中
    Class superclass;  // 父类指针
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
复制代码

我们分别分析下这几个成员所占用的大小及意义:

  • ISA:Classl类型,即一个 objc_class 指针,占用8个字节,这里表示 isa 所指向的类
  • superclass:与 ISA 属于同一种类型,故也占用 8 个字节,这里表示继承的父类
  • cache:是一个 cache_t 类型的结构体,表示类的缓存数据,比如缓存的方法等。这一点我们在后面篇章会探索到,本篇就先分析下 cache 所占用的大小,其在源码中的定义如下图

Snip20210620_5.png
可以发现,cache_t 中大部分为 static 修饰的成员和函数,分别存储在静态全局区和代码区,实际占用空间的成员只有 _bucketsAndMaybeMask 和 一个匿名共用体,因此分析这两个成员的大小即可。

_bucketsAndMaybeMask 为 explicit_atomic<uintptr_t> 类型,其大小与泛型类型 uintptr_t 相同,即为8个字节。 另一个成员为共用体,其内部包含一个结构体成员和一个指针,大小均为 8 字节,所以这个共用体成员大小为 8 字节。故 cache_t cache 占用大小为 16 字节。

  • bits: bits 存储类的数据,包括方法、属性、协议等,是本篇文章探究的重点。其内部只包含一个 uintptr_t bits; 成员,故占用大小为 8 字节。

以上即为 objc_class 内部结构的初步分析,下一节我们来具体分析一下 ISAsuperclass

二、isa关系链及继承关系链

objc_class 的成员中有两个成员都表示类的指向关系即 ISAsuperclass,这两个指向有什么区别呢?本小节我们就来探究一下。

2.1 isa 关系链

2.1.1 元类初现

在探究对象本质时,我们已经通过对象的 isa 找到了对象所属的类,这里我们依然使用相同的方式继续探索。首先在 libobjc源码工程中新建一个tartge,这里为了方便,选择 macOS下的 command line Tool 即可之后新建一个类 BPPerson。在 main.m 的测试方法及结果如下:

Snip20210620_7.png

观察结果可以发现图中 a 步骤就是通过 isa 找到对象 person 所属的类的过程。 a 和 b 之间其实做了同样的操作,不过操作的对象换成了 BPPerson 类。但是结果令人惊奇, BPPerson 类指向了一块内存地址,po 该地址得到的结果也是 BPPerson。不过很明显这两个 BPPerson 并不是同一个东西,因为它们的地址并不一样,而通过 c 步骤打印的结果看,地址为0x00000001000081c8BPPerson 才是真正的类。那么地址为 0x00000001000081a0BPPerson 又是什么呢?难道在内存中 BPPerson 不止一份吗?

为了验证这个问题,我们写一个方法,通过对比得到的 Class 来验证一下,代码如下:

void compareBPPersonClass(int a) {
    
    Class class1 = [BPPerson class];
    Class class2 = [BPPerson alloc].class;
    Class class3 = object_getClass([BPPerson alloc]);
    
    NSLog(@"\n\nclass1为%@ == 地址%p,\nclass2为%@ == 地址%p,\nclass3为%@ == 地址%p,\n", class1, class1, class2, class2, class3, class3);
}
复制代码

打印结果如下:

Snip20210620_9.png

通过这个结果我们发现 BPPerson 打印的内存地址都是一样的,也就是说在内存中 BPPerson 只有一份。我们在运行时发现了另外存在的一份也叫做 BPPerson 的内存,虽然我们目前无法证实它是什么,但是我们可以通过查看Mach-O文件,看看是否有两个 BPPerson 的符号。
通过 MachOView 工具打开工程的Mach-O,并在 Symbol Table 中搜索 BPPerson ,结果如下:

Snip20210620_10.png

我们发现在编译时期,系统就帮我们生成了一个 METACLASS。那么 METACLASS 的作用是什么呢?其实在前面的探索中,我们发现对象通过 isa 找到了所属的类,那么类通过 isa 找到的肯定也是是自己的所属,但是我们称之为 元类,即METACLASS。其实本质上类也是一个对象,被称为类对象,类对象的isa指向其元类,下一小节,我们将探索类的isa关系链。

2.1.2 isa关系链的探索

在上一节的探索中,我们知道了什么叫做元类,那么对象、类、元类的isa指向有什么关系呢?本小节我们接着上一小节的Demo继续探索,上次探索到类的 isa 指向了元类,那么元类的 isa 指向哪里呢?下图是探索结果:

Snip20210620_11.png

  • 图中 a 和 b 与上一小节相同,证明了类的 isa 指向了其元类;
  • c 步骤则是对元类做了 a、b 的操作,发现元类的 isa 指向了地址为 0x000000010036a0f0NSObject
  • 但是通过 d 步骤,我们发现 NSObject 类的地址为 0x000000010036a140,即 0x000000010036a0f0 指向的为 NSObject 的元类,称之为根元类。
  • e步骤继续操作了根元类,我们发现根元类的isa 指向依然是根元类。

由此得出结论:

对象isa -> 类,类isa -> 元类,元类isa -> 根元类,根元类isa -> 根元类自己

以上探究还未考虑有子类的情况,我们创建一个新的类 BPProgramer,继承于 BPPerson。继续根据上述方法探索其 isa 指向,结果如下:

Snip20210620_12.png

  • a、跟 BPPerson 一样,BPProgramer的isa一样指向自己的元类
  • b、BPProgramer元类的isa指向根元类,不会指向 BPPerson 的元类
  • c、根元类的isa指向自己,这一点不受影响

2.2 继承关系链

探索完isa的指向关系,接下来我们来探索类的继承关系链。

2.2.1 类的继承关系

首先,根据 BPProgramerBPPerson写一个函数,代码如下:

void compareSuperclass(void) {
    Class class1 = [BPProgramer class];
    Class superclass = class_getSuperclass(class1);
    Class sSuperclass = class_getSuperclass(superclass);
    Class ssSuperclass = class_getSuperclass(sSuperclass);
    NSLog(@"BPProgramer == %@\n superclass == %@\n sSuperclass == %@\n ssSuperclass == %@\n", class1, superclass, sSuperclass, ssSuperclass);
}
复制代码

执行结果如下图所示:

Snip20210620_13.png

由图中结果可以看出,类的继承关系为 子类 -> 父类 -> … -> 根类NSObject -> nil

2.2.2 元类的继承关系

类的继承关系就这么简单吗?显然不是,上诉结果并没有包含元类,元类有没有什么特殊情况呢?我们再写一个元类的方法来探索一下,代码如下:

void compareMetaSuperclass(void) {
    // NSObject类
    Class class = object_getClass([NSObject alloc]);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // BPPerson元类
    Class pMetaClass = object_getClass(BPPerson.class);
    Class pmSuperClass = class_getSuperclass(pMetaClass); //获取到BPPerson元类的父类
    NSLog(@"%@ - %p",pmSuperClass,pmSuperClass);
    
    // BPProgramer -> BPPerson -> NSObject
    Class proMetaClass = object_getClass(BPProgramer.class);
    Class promSuperClass = class_getSuperclass(proMetaClass);//获取到BPProgramer元类的父类
    NSLog(@"%@ - %p",promSuperClass,promSuperClass);
    
    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}
复制代码

代码执行结果如下:

Snip20210620_14.png

上图结果中分别打印了NSObject类和元类的地址,可以用于分析元类继承的到底是 NSObject类 还是 NSObject元类 根据图中结果分析如下:

  • BPPerson的元类 的父类为 NSObject的元类
  • BPProgramer的元类 的父类为 BPPerson的元类
  • 根元类 的父类为 NSObject 类,NSObject类 的父类为nil

2.3 isa关系及继承关系总结

其实苹果已经很清晰的解释这些关系,如图所示为苹果官方提供的一张解析图:

isa流程图.png

这里做下总结:

  • 对象之间既没有isa关系,也不存在继承关系
  • isa关系: 对象isa -> 类,类isa -> 元类,元类isa -> 根元类, 根元类isa -> 根元类自己
  • 继承关系:分两条线,类:子类 -> 父类 -> 根类(NSObject) -> nil;元类:子类元类 -> 父类元类 -> 根元类 -> 根类NSObject -> nil

三、类的数据 class_rw_t 分析 — 属性及实例方法探索

前两节分别探究了类的底层结构以及指向关系链(isa和继承),本节探索底层类结构中存储的数据。这里所说的数据指的是类的属性、方法、成员变量等信息。

3.1 class_rw_t分析

在第一节分析类的底层结构时,我们知道类的数据是存储在 class_data_bits_t bits 中的。在 libobjc源码 中的定义如下:

Snip20210621_15.png

class_data_bits_t bits是一个结构体,只包含了一个成员 uintptr_t bits;,占用8个字节。除了 bits 这一成员外,其余就是函数了,在众多函数中我们发现如下两个函数:

Snip20210621_16.png

很显然这是一对 setter/getter 函数,根据 bits 来修改和取出数据。根据函数的返回值和参数类型,我们进入 class_rw_t,得到其定义如下:

Snip20210621_17.png
Snip20210621_18.png
class_rw_t 中,除了其成员外,我们发现了几个我们很熟悉的函数,很显然这就是用来存储类的方法、属性及协议的地方。

3.2 类数据底层存储源码分析

在上一小节中探索到了 class_rw_t 是用来存储属性、方法、协议的地方,不过具体是如何存储的还未探索,本小节将以属性为例,探索一下源码中是如何存储的。

在源码中,用来获取属性的函数是 properties() ,其定义代码如下:

// 获取属性,返回值为property_array_t
const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }
复制代码

我们发现函数的返回值为 property_array_t,跟进去看一下其定义如下:

Snip20210621_19.png

property_array_t 是继承自 list_array_tt,我们继续跟进去看下,发现其采用的是C++的模板 template 来定义了 list_array_tt,使得我们可以使用泛型类型,从外部传入我们需要的类型,其代码如下:

Snip20210621_21.png

我们可以在注释中看到模板中定义的 Element 是基础元数据类型,List 是元数据的列表类型。在 list_array_tt 中还定义了一个迭代器 iterator 及共用体,分别用来遍历列表和为外界提供一个访问的 List 接口,代码定义如下:

 protected:
    class iterator {
        const Ptr<List> *lists;
        const Ptr<List> *listsEnd;
        typename List::iterator m, mEnd;

     public:
        iterator(const Ptr<List> *begin, const Ptr<List> *end)
            : lists(begin), listsEnd(end)
        {
            if (begin != end) {
                m = (*begin)->begin();
                mEnd = (*begin)->end();
            }
        }
      /*
      迭代器 iterator 的其他函数
      */
      
  public:
    union {
        Ptr<List> list; // 提供给外部调用的接口
        uintptr_t arrayAndFlag;
    };
  /*
  其他函数
  */
复制代码

通过代码可以发现,iteratorprotectd 的,但是其内部函数是 public,所以在外部也可以访问。其实属性在源码中的存储方式可以简化为如下图所示:
Snip20210621_23.png
property_array_t 是最外层的List,property_array_t 包含的是 property_list_t, 而 property_list_t 包含 property_t

Tip:这里对C++中的模板做一下补充,模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。使用模板的目的就是能够让程序员编写与类型无关的代码。具体可参照此教程 C++教程

我们再回到 property_array_t 的定义,发现我们传入的 Elementproperty_t 类型,Listproperty_list_t 类型。两部分定义如下:

struct property_t {
    const char *name;  // 属性名称
    const char *attributes; // 属性的属性,如类型等
};

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
// 这里是一个空定义,但是继承自 entsize_list_tt
};
复制代码

property_t 的定义简单明了,不需要太多解释,但是 property_list_t 却是一个空的定义,不过它继承自 entsize_list_tt,我们继续跟进去看下,发现其代码如下:

Snip20210621_22.png

我们发现 entsize_list_tt 也是定义了一个模板,我们同样可以从外部传入我们需要的类型。在 entsize_list_tt 内部,我们发现两个函数如图中红圈所示,可以传入下标 i ,获取到对应的元素,返回值为 Element 的指针。

entsize_list_tt 提供了 get 函数用于访问元素,不过如果不想调用函数,我们还可以通过地址偏移的方式来获取,不过步长为 Element 类型的大小。

以上探索了源码上是如何存储属性的,不过在查看的 methods()protocols() 后,可以发现
method_array_tprotocol_array_t 也是继承于 list_array_tt,所以其存储方式与属性大同小异,看来这也是使用模板的原因。

3.3 读取属性和实例方法

我们知道了源码中是如何存储属性、方法及协议的,本小节我们通过 LLDB 指令来读取相关的数据,验证下上一节的结论是否正确。
首先我们给 BPPerson 和 BPProgramer 添加几个属性和方法,其类定义代码如下图:

Snip20210621_1.png

Snip20210621_3.png

3.3.1 读取属性

我们先探索下属性如何获取,打开工程运行如下代码,进行断点调试:
Snip20210621_4.png
BPProgramer 为例,通过如下步骤来进行获取,具体步骤及执行结果如下图:

Snip20210621_6.png
Snip20210621_7.png
因为结果较长,所以分为2张图,下面我们分析下每一步骤的意义:

  • p/x BPProgramer.class
    • 16进制打印 BPProgramer 类的首地址
  • p/x 0x0000000100008428 + 0x20
    • 因为 bits 是第四个成员,前面的 isa、superclass、cache共占用32个字节,所以需要类的首地址 + 0x20(32的十六进制),得到 bits
  • p (class_data_bits_t *)0x0000000100008448
    • 强转得到 bits 的指针地址
  • p *$2
    • 取出指针指向的数据 bits
  • p $3.data()
    • 通过data()函数获取到 class_rw_t
  • p *$4
    • 取出指针下的 class_rw_t
  • p $5.properties()
    • 取出属性,protocol_array_t
  • p $6.list
    • 获取 list,这里得到的是 RawPtr<property_list_t>
  • p $7.ptr
    • 通过上一步得到的 RawPtr 进一步取到 property_list_t指针
  • p *$8
    • 获取指针的内容,即 property_list_t
  • p 9.get(0)p9.get(0)和p 9.get(1)
    • 分别获取到 bpTitlebpLanguage

通过这个执行结果,可以发现在执行 *p $8 后得到的 properties 的数量为 2 ,也就是说并不会获取到父类的属性,由此得出结论类所存储的属性只是本类的属性,不会存储父类的属性。

BPProgramer 类还包含一个成员变量 programerAge,但是通过 properties() 也未获取到,说明成员变量并非存储在属性里,这一点在下一个篇章中会继续探索成员变量的存储位置及如何获取。

3.3.2 读取方法

其实通过属性的探究,我们已经了解了获取类的数据的大致步骤,对于方法的获取就比较容易上手了。但是与属性又一点不同,method_t 并非直接取到方法名称等信息,而是有一个结构体 big ,如下图所示:

Snip20210621_8.png

在调用 get 方法后,需要再调用 big ,才能得到方法信息,具体读取方法的步骤和结果如下图:

Snip20210621_9.png

Snip20210621_10.png

获取方法的步骤分析如下

  • 在 p 5.ptrp5.ptr 和 p *6 之前的操作与获取属性是一样的
  • 在 p *$6 之后,结果显示一共有 5 个方法
  • 之后通过 p 7.get(0).bigp7.get(0).big ~ p 7.get(4).big 获取到方法分别为 [BPProgramer bpTitle]、[BPProgramer bpLanguage]、[BPProgramer setBpTitle:]、[BPProgramer setBpLanguage:]、[BPProgramer .cxx_destruct]。除了最后一个为析构方法外,其他为属性的 setter/getter 方法。至此我们就获取到了 BPProgramer 方法,但是我们发现类也是只会存储自己的方法,继承自父类的方法并不会被存储。而且我们发现没有获取到类方法 + (void)discussWithProductManager;,具体是什么原因也留在下一篇章继续探索。

总结

本篇是类的底层初探,包含了类的结构、类的isa指向关系及继承关系、类的属性及方法的获取,我们简单总结一下

  • 类的结构
    • 类在底层是 objc_class 结构体,继承于 objc_object,包含四个成员 isa、superclass、cache、bits
  • isa链
    • 对象isa -> 类,类isa -> 元类,元类isa -> 根元类, 根元类isa -> 根元类自己
  • 继承链
    • 类:子类 -> 父类 -> 根类(NSObject) -> nil;
    • 元类:子类元类 -> 父类元类 -> 根元类 -> 根类NSObject -> nil
  • 类的属性及方法获取
    • 类的信息存储在 class_data_bits_t 中,其内部方法 data() 会返回一个 class_rw_t 结构体,通过这个结构体可以调用 methods、properties()、protocols() 函数,获取到方法、属性、协议的信息。具体的获取方式可以回看第三节。

以上即为关于类的初探,后续还会继续探索类的底层,欢迎大家继续关注,也希望大家批评指正。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享