在开始本文之前,相信大家都知道面向对象编程中有一句名言:
万物皆对象
对象究竟从哪里来呢?带着这个问题,我们开始今天的主题。
本文所采用的源码为苹果开源的最新 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 vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_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
指针cache
bits
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_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#if __LP64__
uint16_t _flags;
#endif
uint16_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; // 8
explicit_atomic<mask_t> _mask; // 4
uint16_t _flags; // 2
uint16_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_ISA
uint16_t index;
#endif
explicit_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
中类的结构的基本分析,下一章我们将对类的结构进行深一步探索。