前言
在前三篇文章中,我们从以下几个角度探索类 OC 对象相关的底层原理。
从本篇文章开始,我们进入 OC 类的篇章,本篇就是第一篇关于 OC 类的初探。本文将从以下几个角度进行探索,下面我们一起开始吧。
- 类的底层结构 objc_class 分析
- 类的 isa关系链 及 继承关系链
- 类的数据 class_rw_t 分析(属性及实例方法)
一、类的底层结构 objc_class 分析
在 对象的本质 这篇文章中探索 Class 时发现,Class 其实是一个 objc_class 指针,那么 objc_class 到底是什么呢?其内部结构是怎样的呢?我们现在就来探索一下。
首先,我们打开 libobjc源码,全局搜索 struct objc_class 结果如下:
可以发现在源码中,有多个地方对 struct objc_class 有定义,例如 runtime.h、objc-runtime-old.h、objc-runtime-new.h ,通对比发现 objc-runtime-new.h 才是目前正在使用的定义,其定义的代码如下:
由代码可以发现 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 所占用的大小,其在源码中的定义如下图
可以发现,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 内部结构的初步分析,下一节我们来具体分析一下 ISA 与 superclass。
二、isa关系链及继承关系链
在 objc_class 的成员中有两个成员都表示类的指向关系即 ISA 和 superclass,这两个指向有什么区别呢?本小节我们就来探究一下。
2.1 isa 关系链
2.1.1 元类初现
在探究对象本质时,我们已经通过对象的 isa 找到了对象所属的类,这里我们依然使用相同的方式继续探索。首先在 libobjc源码工程中新建一个tartge,这里为了方便,选择 macOS下的 command line Tool 即可之后新建一个类 BPPerson。在 main.m 的测试方法及结果如下:
观察结果可以发现图中 a 步骤就是通过 isa 找到对象 person 所属的类的过程。 a 和 b 之间其实做了同样的操作,不过操作的对象换成了 BPPerson 类。但是结果令人惊奇, BPPerson 类指向了一块内存地址,po 该地址得到的结果也是 BPPerson。不过很明显这两个 BPPerson 并不是同一个东西,因为它们的地址并不一样,而通过 c 步骤打印的结果看,地址为0x00000001000081c8的 BPPerson 才是真正的类。那么地址为 0x00000001000081a0 的 BPPerson 又是什么呢?难道在内存中 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);
}
复制代码
打印结果如下:
通过这个结果我们发现 BPPerson 打印的内存地址都是一样的,也就是说在内存中 BPPerson 只有一份。我们在运行时发现了另外存在的一份也叫做 BPPerson 的内存,虽然我们目前无法证实它是什么,但是我们可以通过查看Mach-O文件,看看是否有两个 BPPerson 的符号。
通过 MachOView 工具打开工程的Mach-O,并在 Symbol Table 中搜索 BPPerson ,结果如下:
我们发现在编译时期,系统就帮我们生成了一个 METACLASS。那么 METACLASS 的作用是什么呢?其实在前面的探索中,我们发现对象通过 isa 找到了所属的类,那么类通过 isa 找到的肯定也是是自己的所属,但是我们称之为 元类,即METACLASS。其实本质上类也是一个对象,被称为类对象,类对象的isa指向其元类,下一小节,我们将探索类的isa关系链。
2.1.2 isa关系链的探索
在上一节的探索中,我们知道了什么叫做元类,那么对象、类、元类的isa指向有什么关系呢?本小节我们接着上一小节的Demo继续探索,上次探索到类的 isa 指向了元类,那么元类的 isa 指向哪里呢?下图是探索结果:
- 图中 a 和 b 与上一小节相同,证明了类的 isa 指向了其元类;
- c 步骤则是对元类做了 a、b 的操作,发现元类的 isa 指向了地址为 0x000000010036a0f0 的 NSObject。
- 但是通过 d 步骤,我们发现 NSObject 类的地址为 0x000000010036a140,即 0x000000010036a0f0 指向的为 NSObject 的元类,称之为根元类。
- e步骤继续操作了根元类,我们发现根元类的isa 指向依然是根元类。
由此得出结论:
对象isa -> 类,类isa -> 元类,元类isa -> 根元类,根元类isa -> 根元类自己
以上探究还未考虑有子类的情况,我们创建一个新的类 BPProgramer,继承于 BPPerson。继续根据上述方法探索其 isa 指向,结果如下:
- a、跟 BPPerson 一样,BPProgramer的isa一样指向自己的元类
- b、BPProgramer元类的isa指向根元类,不会指向 BPPerson 的元类
- c、根元类的isa指向自己,这一点不受影响
2.2 继承关系链
探索完isa的指向关系,接下来我们来探索类的继承关系链。
2.2.1 类的继承关系
首先,根据 BPProgramer 和 BPPerson写一个函数,代码如下:
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);
}
复制代码
执行结果如下图所示:
由图中结果可以看出,类的继承关系为 子类 -> 父类 -> … -> 根类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);
}
复制代码
代码执行结果如下:
上图结果中分别打印了NSObject类和元类的地址,可以用于分析元类继承的到底是 NSObject类 还是 NSObject元类 根据图中结果分析如下:
- BPPerson的元类 的父类为 NSObject的元类
- BPProgramer的元类 的父类为 BPPerson的元类
- 根元类 的父类为 NSObject 类,NSObject类 的父类为nil
2.3 isa关系及继承关系总结
其实苹果已经很清晰的解释这些关系,如图所示为苹果官方提供的一张解析图:
这里做下总结:
- 对象之间既没有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源码 中的定义如下:
class_data_bits_t bits是一个结构体,只包含了一个成员 uintptr_t bits;,占用8个字节。除了 bits 这一成员外,其余就是函数了,在众多函数中我们发现如下两个函数:
很显然这是一对 setter/getter 函数,根据 bits 来修改和取出数据。根据函数的返回值和参数类型,我们进入 class_rw_t,得到其定义如下:
在 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,跟进去看一下其定义如下:
property_array_t 是继承自 list_array_tt,我们继续跟进去看下,发现其采用的是C++的模板 template 来定义了 list_array_tt,使得我们可以使用泛型类型,从外部传入我们需要的类型,其代码如下:
我们可以在注释中看到模板中定义的 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;
};
/*
其他函数
*/
复制代码
通过代码可以发现,iterator 为 protectd 的,但是其内部函数是 public,所以在外部也可以访问。其实属性在源码中的存储方式可以简化为如下图所示:
property_array_t 是最外层的List,property_array_t 包含的是 property_list_t, 而 property_list_t 包含 property_t
Tip:这里对C++中的模板做一下补充,模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。使用模板的目的就是能够让程序员编写与类型无关的代码。具体可参照此教程 C++教程。
我们再回到 property_array_t 的定义,发现我们传入的 Element 为 property_t 类型,List 为 property_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,我们继续跟进去看下,发现其代码如下:
我们发现 entsize_list_tt 也是定义了一个模板,我们同样可以从外部传入我们需要的类型。在 entsize_list_tt 内部,我们发现两个函数如图中红圈所示,可以传入下标 i ,获取到对应的元素,返回值为 Element 的指针。
entsize_list_tt 提供了 get 函数用于访问元素,不过如果不想调用函数,我们还可以通过地址偏移的方式来获取,不过步长为 Element 类型的大小。
以上探索了源码上是如何存储属性的,不过在查看的 methods() 和 protocols() 后,可以发现
method_array_t 和 protocol_array_t 也是继承于 list_array_tt,所以其存储方式与属性大同小异,看来这也是使用模板的原因。
3.3 读取属性和实例方法
我们知道了源码中是如何存储属性、方法及协议的,本小节我们通过 LLDB 指令来读取相关的数据,验证下上一节的结论是否正确。
首先我们给 BPPerson 和 BPProgramer 添加几个属性和方法,其类定义代码如下图:
3.3.1 读取属性
我们先探索下属性如何获取,打开工程运行如下代码,进行断点调试:
以 BPProgramer 为例,通过如下步骤来进行获取,具体步骤及执行结果如下图:
因为结果较长,所以分为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(1)
- 分别获取到 bpTitle 和 bpLanguage
通过这个执行结果,可以发现在执行 *p $8 后得到的 properties 的数量为 2 ,也就是说并不会获取到父类的属性,由此得出结论类所存储的属性只是本类的属性,不会存储父类的属性。
BPProgramer 类还包含一个成员变量 programerAge,但是通过 properties() 也未获取到,说明成员变量并非存储在属性里,这一点在下一个篇章中会继续探索成员变量的存储位置及如何获取。
3.3.2 读取方法
其实通过属性的探究,我们已经了解了获取类的数据的大致步骤,对于方法的获取就比较容易上手了。但是与属性又一点不同,method_t 并非直接取到方法名称等信息,而是有一个结构体 big ,如下图所示:
在调用 get 方法后,需要再调用 big ,才能得到方法信息,具体读取方法的步骤和结果如下图:
获取方法的步骤分析如下
- 在 p 6 之前的操作与获取属性是一样的
- 在 p *$6 之后,结果显示一共有 5 个方法
- 之后通过 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() 函数,获取到方法、属性、协议的信息。具体的获取方式可以回看第三节。
以上即为关于类的初探,后续还会继续探索类的底层,欢迎大家继续关注,也希望大家批评指正。