在开始本文之前,相信大家都知道面向对象编程中有一句名言:

万物皆对象

对象究竟从哪里来呢?带着这个问题,我们开始今天的主题。

本文所采用的源码为苹果开源的最新 objc4-781 版本。下载地址为: Source Browser

1、iOS 中的类到底是什么?

我们在日常开发中大多数情况都是从 NSObject 这个基类来派生出我们需要的类。那么在 OC 底层,我们的类 Class 到底被编译成什么样子了呢?

我们新建一个 macOS 控制台项目,然后新建两个类 TeacherStudent 出来,其中 Teacher 继承自 NSObject, Student 继承自 Teacher

  1. #import <Foundation/Foundation.h>
  2. #import "Student.h"
  3. int main(int argc, const char * argv[]) {
  4. @autoreleasepool {
  5. Student *stu = [Student alloc];
  6. NSLog(@"stu: %@", stu);
  7. }
  8. return 0;
  9. }

然后我们在终端使用 clang 命令:

  1. clang -rewrite-objc main.m -o main.cpp

扩展

  1. 如果是目标文件导入了 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版本来做更改。

  2. 其中 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:

image.png

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

image.png

所以由这行代码,我们可以得出一个结论,Class 类型在底层是一个结构体类型的指针,这个结构体类型为 objc_class。如果我们此时再使用全局搜索关键字 typedef struct objc_class,会发现搜不出什么东西出来了,这个时候就需要我们在源码中进行探索了。

我们在源码中直接搜索 struct objc_class,然后定位到 objc-runtime-new.h 文件

  1. struct objc_class : objc_object {
  2. // Class ISA;
  3. Class superclass;
  4. cache_t cache; // formerly cache pointer and vtable
  5. class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
  6. class_rw_t *data() const {
  7. return bits.data();
  8. }
  9. // 省略部分代码.......
  10. }
  11. /// Represents an instance of a class.
  12. struct objc_object {
  13. Class _Nonnull isa OBJC_ISA_AVAILABILITY;
  14. };

细心的读者看到这段代码,就会发现,我们在前面探索对象原理中发现的 objc_object 再次出现,并且作为 objc_class 的父类。这里又完美印证了我们开头的那句经典名言 万物皆对象,也就是说类其实一个一种 对象

此时,我们可以如下的关系图:

image.png

2、类的结构是什么?

通过上面的探索,我们已经了解到类的本质上也是对象,而且日常开发中常见的成员变量、属性、方法、协议等我们通过源码看到都是存储在类里面的,那么我们如何通过调试源码来验证呢?

从上面的 objc_class 的定义处,我们可以看到 Class 中的四个属性

  • isa 指针(隐藏属性)
  • superclass 指针
  • cache
  • bits

2.1 isa 指针

首先是 isa 指针,我们之前的文章已经探索过了,在对象初始化的时候,通过 isa 可以让对象和类关联,这一点很好理解,可是为什么在类结构里面还会有 isa 呢?其实就是我们的对象和类关联起来需要 isa,同样的,类和元类之间关联也需要 isa。这时候需要放出我们经典的 isa走位图

image.png

2.2 superclass 指针

顾名思义,superclass 指针表明当前类指向的是哪个父类。一般来说,类的根父类基本上都是 NSObject类。根元类的父类也是 NSObject 类。

2.3 cache 缓存

cache 的数据结构为 cache_t,其定义如下:

  1. struct cache_t {
  2. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
  3. explicit_atomic<struct bucket_t *> _buckets;
  4. explicit_atomic<mask_t> _mask;
  5. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  6. explicit_atomic<uintptr_t> _maskAndBuckets;
  7. mask_t _mask_unused;
  8. #if __LP64__
  9. uint16_t _flags;
  10. #endif
  11. uint16_t _occupied;
  12. }
  13. // 省略部分代码...

类的缓存里面存放的是什么呢?我们可以通过 objc-cache.mm 来解答这个问题。

  1. /***********************************************************************
  2. * objc-cache.m
  3. * Method cache management
  4. * Cache flushing
  5. * Cache garbage collection
  6. * Cache instrumentation
  7. * Dedicated allocator for large caches
  8. **********************************************************************/

上面 objc-cache.mm 源文件的注释信息,我们可以看到 Method cache management 的出现,翻译过来就是方法缓存管理。那么是不是就是说 cache 属性就是缓存的方法呢?这里我们留个悬念,因为我们 OC 中的方法到现在还没有进行探索,所以留在后面的文章进行详细讲解。

2.4 bits 属性

bits 的数据结构类型是 class_data_bits_t,同时也是一个结构体类型。而我们阅读 objc_class 源码的时候,会发现很多地方都有 bits 的身影,比如:

  1. class_rw_t *data() const {
  2. return bits.data();
  3. }
  4. void setData(class_rw_t *newData) {
  5. bits.setData(newData);
  6. }
  7. bool hasCustomRR() const {
  8. return !bits.getBit(FAST_HAS_DEFAULT_RR);
  9. }
  10. void setHasDefaultRR() {
  11. bits.setBits(FAST_HAS_DEFAULT_RR);
  12. }
  13. void setHasCustomRR() {
  14. bits.clearBits(FAST_HAS_DEFAULT_RR);
  15. }

这里值得我们注意的是,objc_classdata()方法其实是返回的 bitsdata() 方法,而通过这个 data() 方法,我们发现诸如类的字节对齐、ARC、元类等特性都有 data() 的出现,这间接说明 bits 属性其实是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。

这里我们会遇到一个十分重要的知识点: class_rw_tdata() 方法的返回值就是 class_rw_t 类型的指针对象。这就是我们本文的重点分析对象。

3、类的属性存在哪?(重点)

我们先从本文最开始创建的工程, Student 类里面添加一个成员变量和属性出来。

  1. #import "Teacher.h"
  2. @interface Student : Teacher
  3. {
  4. NSString *hobby;
  5. }
  6. @property (nonatomic, copy) NSString *name;
  7. @end
  8. #import <Foundation/Foundation.h>
  9. #import "Student.h"
  10. int main(int argc, const char * argv[]) {
  11. @autoreleasepool {
  12. Student *stu = [Student alloc];
  13. NSLog(@"stu: %@", stu);
  14. }
  15. return 0;
  16. }

然后我们在 NSLog() 处下一个断点,然后 run 起来,我们在控制台先打印输出一下 Student.class的内容:

image.png

此时输出的是 Studentisa 的地址信息。

3.1 类的内存结构

这个时候我们需要借助一下指针平移来进行探索,这个时候我们再来看一下类的内存结构分布图:

image.png

  1. struct cache_t {
  2. explicit_atomic<struct bucket_t *> _buckets; // 8
  3. explicit_atomic<mask_t> _mask; // 4
  4. uint16_t _flags; // 2
  5. uint16_t _occupied; // 2
  6. }
  7. // 省略部分代码...

从上面的代码我们可以看出,isasuperclass都是结构体指针,各占 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 递增,也就是

  1. 0x0000000100002328 + 0x20 = 0x0000000100002348

我们输出打印一下这个地址,注意要强转一下类型

image.png

成功输出了,现在我们再去查看一下源码,在 objc_class 源码中有:

  1. class_rw_t *data() const {
  2. return bits.data();
  3. }

我们继续打印一下里面的内容:

image.png

返回了一个 class_rw_t 指针对象。我们在源码中搜索 class_rw_t

  1. struct class_rw_t {
  2. // Be warned that Symbolication knows the layout of this structure.
  3. uint32_t flags;
  4. uint16_t witness;
  5. #if SUPPORT_INDEXED_ISA
  6. uint16_t index;
  7. #endif
  8. explicit_atomic<uintptr_t> ro_or_rw_ext;
  9. Class firstSubclass;
  10. Class nextSiblingClass;
  11. // 省略代码.....
  12. const method_array_t methods() const {
  13. auto v = get_ro_or_rwe();
  14. if (v.is<class_rw_ext_t *>()) {
  15. return v.get<class_rw_ext_t *>()->methods;
  16. } else {
  17. return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
  18. }
  19. }
  20. const property_array_t properties() const {
  21. auto v = get_ro_or_rwe();
  22. if (v.is<class_rw_ext_t *>()) {
  23. return v.get<class_rw_ext_t *>()->properties;
  24. } else {
  25. return property_array_t{v.get<const class_ro_t *>()->baseProperties};
  26. }
  27. }
  28. const protocol_array_t protocols() const {
  29. auto v = get_ro_or_rwe();
  30. if (v.is<class_rw_ext_t *>()) {
  31. return v.get<class_rw_ext_t *>()->protocols;
  32. } else {
  33. return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
  34. }
  35. }
  36. // 省略代码.....
  37. }

可以看到 class_rw_t 也是一个结构体类型,其内部有 methods、properties、protocols 等我们十分熟悉的内容。我们先猜想一下,我们的属性应该存放在 class_rw_tproperties 里面。为了验证我们的猜想,我们接着进行 LLDB 打印:

image.png

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

image.png

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

image.png

wow!我们的属性 name 终于被找到了。那么成员变量在哪里呢?这个问题我们暂时留给大家去思考一下。

4、类的方法存在哪?(重点)

研究完类的属性存在哪里之后,我们再来看看类的方法。接下来我们在 Student 类里面增加一个实例方法 - (void)say666 和类方法 + (void)say888

  1. @interface Student : Teacher
  2. {
  3. NSString *hobby;
  4. }
  5. @property (nonatomic, copy) NSString *name;
  6. - (void)say666;
  7. + (void)say888;
  8. @end
  9. @implementation Student
  10. - (void)say666 {
  11. NSLog(@"%s", __func__);
  12. }
  13. + (void)say888 {
  14. NSLog(@"%s", __func__);
  15. }
  16. @end

按照上面查找属性的方法,我们先一路输出,获取到 class_rw_t *

image.png

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

image.png

继续取出 $4 里面 list 的值

image.png

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

image.png

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

image.png

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

image.png

然后报错了,说明 methods 没有存储 say888 方法,那么它存储在哪里了呢,这个问题也留给大家思考。我们下一篇文章来对这些问题进行进一步的探索。

其他内容的查找,大家可以自行动手去查找。

5、总结

  • 万物皆对象:类的本质就是对象
  • 类在 class_rw_t 结构中存储了编译时确定的属性、方法和协议等内容。
  • 实例方法存放在类中

现在我们完成了对 iOS 中类的结构的基本分析,下一章我们将对类的结构进行深一步探索。

6、参考资料