在开始本文之前,相信大家都知道面向对象编程中有一句名言:
万物皆对象
对象究竟从哪里来呢?带着这个问题,我们开始今天的主题。
本文所采用的源码为苹果开源的最新 objc4-781 版本。下载地址为: Source Browser。
1、iOS 中的类到底是什么?
我们在日常开发中大多数情况都是从 NSObject 这个基类来派生出我们需要的类。那么在 OC 底层,我们的类 Class 到底被编译成什么样子了呢?
我们新建一个 macOS 控制台项目,然后新建两个类 Teacher 和 Student 出来,其中 Teacher 继承自 NSObject, Student 继承自 Teacher。
#import <Foundation/Foundation.h>#import "Student.h"int main(int argc, const char * argv[]) {@autoreleasepool {Student *stu = [Student alloc];NSLog(@"stu: %@", stu);}return 0;}
然后我们在终端使用 clang 命令:
clang -rewrite-objc main.m -o main.cpp
扩展
如果是目标文件导入了
UIKit框架,则我们需要使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk main.m, 其中 13.7 要根据当前的模拟器SDK版本来做更改。其中
xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了一些封装,要更简单好用一些。
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (手机)
这个命令是将我们的 main.m 文件编译成C++文件 main.cpp,我们打开这个文件搜索 Student:

我们发现有多个地方都出现了 Student,然后我们使用全局搜索关键字 typedef struct objc_object,最终我们在7660行左右找到了 class 的定义。

所以由这行代码,我们可以得出一个结论,Class 类型在底层是一个结构体类型的指针,这个结构体类型为 objc_class。如果我们此时再使用全局搜索关键字 typedef struct objc_class,会发现搜不出什么东西出来了,这个时候就需要我们在源码中进行探索了。
我们在源码中直接搜索 struct objc_class,然后定位到 objc-runtime-new.h 文件
struct objc_class : objc_object {// Class ISA;Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits; // class_rw_t * plus custom rr/alloc flagsclass_rw_t *data() const {return bits.data();}// 省略部分代码.......}/// Represents an instance of a class.struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY;};
细心的读者看到这段代码,就会发现,我们在前面探索对象原理中发现的 objc_object 再次出现,并且作为 objc_class 的父类。这里又完美印证了我们开头的那句经典名言 万物皆对象,也就是说类其实一个一种 对象。
此时,我们可以如下的关系图:

2、类的结构是什么?
通过上面的探索,我们已经了解到类的本质上也是对象,而且日常开发中常见的成员变量、属性、方法、协议等我们通过源码看到都是存储在类里面的,那么我们如何通过调试源码来验证呢?
从上面的 objc_class 的定义处,我们可以看到 Class 中的四个属性
isa指针(隐藏属性)superclass指针cachebits
2.1 isa 指针
首先是 isa 指针,我们之前的文章已经探索过了,在对象初始化的时候,通过 isa 可以让对象和类关联,这一点很好理解,可是为什么在类结构里面还会有 isa 呢?其实就是我们的对象和类关联起来需要 isa,同样的,类和元类之间关联也需要 isa。这时候需要放出我们经典的 isa走位图。

2.2 superclass 指针
顾名思义,superclass 指针表明当前类指向的是哪个父类。一般来说,类的根父类基本上都是 NSObject类。根元类的父类也是 NSObject 类。
2.3 cache 缓存
cache 的数据结构为 cache_t,其定义如下:
struct cache_t {#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINEDexplicit_atomic<struct bucket_t *> _buckets;explicit_atomic<mask_t> _mask;#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16explicit_atomic<uintptr_t> _maskAndBuckets;mask_t _mask_unused;#if __LP64__uint16_t _flags;#endifuint16_t _occupied;}// 省略部分代码...
类的缓存里面存放的是什么呢?我们可以通过 objc-cache.mm 来解答这个问题。
/************************************************************************ objc-cache.m* Method cache management* Cache flushing* Cache garbage collection* Cache instrumentation* Dedicated allocator for large caches**********************************************************************/
上面 objc-cache.mm 源文件的注释信息,我们可以看到 Method cache management 的出现,翻译过来就是方法缓存管理。那么是不是就是说 cache 属性就是缓存的方法呢?这里我们留个悬念,因为我们 OC 中的方法到现在还没有进行探索,所以留在后面的文章进行详细讲解。
2.4 bits 属性
bits 的数据结构类型是 class_data_bits_t,同时也是一个结构体类型。而我们阅读 objc_class 源码的时候,会发现很多地方都有 bits 的身影,比如:
class_rw_t *data() const {return bits.data();}void setData(class_rw_t *newData) {bits.setData(newData);}bool hasCustomRR() const {return !bits.getBit(FAST_HAS_DEFAULT_RR);}void setHasDefaultRR() {bits.setBits(FAST_HAS_DEFAULT_RR);}void setHasCustomRR() {bits.clearBits(FAST_HAS_DEFAULT_RR);}
这里值得我们注意的是,objc_class 的 data()方法其实是返回的 bits 的 data() 方法,而通过这个 data() 方法,我们发现诸如类的字节对齐、ARC、元类等特性都有 data() 的出现,这间接说明 bits 属性其实是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。
这里我们会遇到一个十分重要的知识点: class_rw_t,data() 方法的返回值就是 class_rw_t 类型的指针对象。这就是我们本文的重点分析对象。
3、类的属性存在哪?(重点)
我们先从本文最开始创建的工程, Student 类里面添加一个成员变量和属性出来。
#import "Teacher.h"@interface Student : Teacher{NSString *hobby;}@property (nonatomic, copy) NSString *name;@end#import <Foundation/Foundation.h>#import "Student.h"int main(int argc, const char * argv[]) {@autoreleasepool {Student *stu = [Student alloc];NSLog(@"stu: %@", stu);}return 0;}
然后我们在 NSLog() 处下一个断点,然后 run 起来,我们在控制台先打印输出一下 Student.class的内容:

此时输出的是 Student 类 isa 的地址信息。
3.1 类的内存结构
这个时候我们需要借助一下指针平移来进行探索,这个时候我们再来看一下类的内存结构分布图:

struct cache_t {explicit_atomic<struct bucket_t *> _buckets; // 8explicit_atomic<mask_t> _mask; // 4uint16_t _flags; // 2uint16_t _occupied; // 2}// 省略部分代码...
从上面的代码我们可以看出,isa 和 superclass都是结构体指针,各占 8 字节,而 cache 属性其实是 cache_t 类型的结构体,其内部有一个 8 字节的结构体指针,有 1 个为 4 字节的mask_t 和有 2 个各为 2 字节的 uint16_t。所以加起来就是 16 个字节。也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节。
3.2 探索 bits 属性
我们刚才在控制台打印输出了 Student 类的 isa 指针地址,所以我们在 isa 的初始偏移量的地址处进行 16 进制下的 20 递增,也就是
0x0000000100002328 + 0x20 = 0x0000000100002348
我们输出打印一下这个地址,注意要强转一下类型

成功输出了,现在我们再去查看一下源码,在 objc_class 源码中有:
class_rw_t *data() const {return bits.data();}
我们继续打印一下里面的内容:

返回了一个 class_rw_t 指针对象。我们在源码中搜索 class_rw_t
struct class_rw_t {// Be warned that Symbolication knows the layout of this structure.uint32_t flags;uint16_t witness;#if SUPPORT_INDEXED_ISAuint16_t index;#endifexplicit_atomic<uintptr_t> ro_or_rw_ext;Class firstSubclass;Class nextSiblingClass;// 省略代码.....const method_array_t methods() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) {return v.get<class_rw_ext_t *>()->methods;} else {return method_array_t{v.get<const class_ro_t *>()->baseMethods()};}}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 *>()->properties;} else {return property_array_t{v.get<const class_ro_t *>()->baseProperties};}}const protocol_array_t protocols() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) {return v.get<class_rw_ext_t *>()->protocols;} else {return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};}}// 省略代码.....}
可以看到 class_rw_t 也是一个结构体类型,其内部有 methods、properties、protocols 等我们十分熟悉的内容。我们先猜想一下,我们的属性应该存放在 class_rw_t 的 properties 里面。为了验证我们的猜想,我们接着进行 LLDB 打印:

我们再接着打印 $9 里面 list 的值:

输出的是 property_list_t * 类型,我们继续打印输出里面的值

wow!我们的属性 name 终于被找到了。那么成员变量在哪里呢?这个问题我们暂时留给大家去思考一下。
4、类的方法存在哪?(重点)
研究完类的属性存在哪里之后,我们再来看看类的方法。接下来我们在 Student 类里面增加一个实例方法 - (void)say666 和类方法 + (void)say888。
@interface Student : Teacher{NSString *hobby;}@property (nonatomic, copy) NSString *name;- (void)say666;+ (void)say888;@end@implementation Student- (void)say666 {NSLog(@"%s", __func__);}+ (void)say888 {NSLog(@"%s", __func__);}@end
按照上面查找属性的方法,我们先一路输出,获取到 class_rw_t *

既然获取属性是用 properties,那么按照源码,我们使用 methods 取方法列表

继续取出 $4 里面 list 的值

看到了我们熟悉的 method_list_t *,继续

say666 被打印出来了,说明 methods() 就是存储实例方法的地方。我们接着打印剩下的内容:

到现在为止还没有看到 say888 类方法,我们继续打印

然后报错了,说明 methods 没有存储 say888 方法,那么它存储在哪里了呢,这个问题也留给大家思考。我们下一篇文章来对这些问题进行进一步的探索。
其他内容的查找,大家可以自行动手去查找。
5、总结
- 万物皆对象:类的本质就是对象
- 类在
class_rw_t结构中存储了编译时确定的属性、方法和协议等内容。 - 实例方法存放在类中
现在我们完成了对 iOS 中类的结构的基本分析,下一章我们将对类的结构进行深一步探索。
