iOS 底层探索系列编程
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列缓存
咱们在前面探索了 iOS
中的对象原理,面向对象编程中有一句名言:bash
万物皆对象markdown
那么对象又是从哪来的呢?有过面向对象编程基础的同窗确定都知道是类派生出对象的,那么今天咱们就一块儿来探索一下类的底层原理吧。数据结构
iOS
中的类究竟是什么?咱们在平常开发中大多数状况都是从 NSObject
这个基类来派生出咱们须要的类。那么在 OC
底层,咱们的类 Class
到底被编译成什么样子了呢?app
咱们新建一个 macOS
控制台项目,而后新建一个 Animal
类出来。oop
// Animal.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Animal : NSObject @end NS_ASSUME_NONNULL_END // Animal.m @implementation Animal @end // main.m #import <Foundation/Foundation.h> #import "Animal.h" int main(int argc, const char * argv[]) { @autoreleasepool { Animal *animal = [[Animal alloc] init]; NSLog(@"%p", animal); } return 0; } 复制代码
咱们在终端执行 clang
命令:post
clang -rewrite-objc main.m -o main.cpp
复制代码
这个命令是将咱们的 main.m
重写成 main.cpp
,咱们打开这个文件搜索 Animal
:测试
咱们发现有多个地方都出现了 Animal
:ui
// 1 typedef struct objc_object Animal; // 2 struct Animal_IMPL { struct NSObject_IMPL NSObject_IVARS; }; // 3 objc_getClass("Animal") 复制代码
咱们先全局搜索第一个 typedef struct objc_object
,发现有 843 个结果
咱们经过 Command + G
快捷键快速翻阅一下,最终在 7626 行找到了 Class
的定义:
typedef struct objc_class *Class; 复制代码
由这行代码咱们能够得出一个结论,Class
类型在底层是一个结构体类型的指针,这个结构体类型为 objc_class
。 再搜索 typedef struct objc_class
发现搜不出来了,这个时候咱们须要在 objc4-756
源码中进行探索了。
咱们在 objc4-756
源码中直接搜索 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() { return bits.data(); } } 复制代码
看到这里,细心的读者可能会发现,咱们在前面探索对象原理中遇到的 objc_object
再次出现了,而且此次是做为 objc_class
的父类。这里再次引用那句经典名言 万物皆对象,也就是说类其实也是一种对象。
由此,咱们能够简单总结一下类和对象在 C
和 OC
中分别的定义
C | OC |
---|---|
objc_object | NSObject |
objc_class | NSObject(Class) |
经过上面的探索,咱们已经知道了类本质上也是对象,而平常开发中常见的成员变量、属性、方法、协议等都是在类里面存在的,那么咱们是否是能够猜测在 iOS
底层,类其实就存储了这些内容呢?
咱们能够经过分析源码来验证咱们的猜测。
从上一节中 objc_class
的定义处,咱们能够梳理出 Class
中的 4 个属性
isa
指针superclass
指针cache
bits
须要值得注意的是,这里的
isa
指针在这里是隐藏属性.
isa
指针首先是 isa
指针,咱们以前已经探索过了,在对象初始化的时候,经过 isa
可让对象和类关联,这一点很好理解,但是为何在类结构里面还会有 isa
呢?看过上一篇文章的同窗确定知道这个问题的答案了。没错,就是元类。咱们的对象和类关联起来须要 isa
,一样的,类和元类之间关联也须要 isa
。
superclass
指针顾名思义,superclass
指针代表当前类指向的是哪一个父类。通常来讲,类的根父类基本上都是 NSObject
类。根元类的父类也是 NSObject
类。
cache
缓存cache
的数据结构为 cache_t
,其定义以下:
struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_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
中的方法咱们如今尚未进行探索,先假设咱们已经掌握了相关的底层原理,这里先简单提一下。
咱们在类里面编写的方法,在底层实际上是以
SEL
+IMP
的形式存在。SEL
就是方法的选择器,而IMP
则是具体的方法实现。这里能够以书籍的目录以及内容来类比,咱们查找一篇文章的时候,须要先知道其标题(SEL
),而后在目录中看有没有对应的标题,若是有那么就翻到对应的页,最后咱们就找到了咱们想要的内容。固然,iOS
中方法要比书籍的例子复杂一些,不过暂时能够这么简单的理解,后面咱们会深刻方法的底层进行探索。
bits
属性bits
的数据结构类型是 class_data_bits_t
,同时也是一个结构体类型。而咱们阅读 objc_class
源码的时候,会发现不少地方都有 bits
的身影,好比:
class_rw_t *data() { return bits.data(); } bool hasCustomRR() { return ! bits.hasDefaultRR(); } bool canAllocFast() { assert(!isFuture()); return bits.canAllocFast(); } 复制代码
这里值得咱们注意的是,objc_class
的 data()
方法实际上是返回的 bits
的 data()
方法,而经过这个 data()
方法,咱们发现诸如类的字节对齐、ARC
、元类等特性都有 data()
的出现,这间接说明 bits
属性实际上是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。
这里咱们会遇到一个十分重要的知识点: class_rw_t
,data()
方法的返回值就是 class_rw_t
类型的指针对象。咱们在本文后面会重点介绍。
上一节咱们对 OC
中类结构有了基本的了解,可是咱们平时最常打交道的内容-属性,咱们还不知道它到底是存在哪一个地方。接下来咱们要作一件事情,就是在 objc4-756
的源码中新建一个 Target
,为何不直接用上面的 macOS
命令行项目呢?由于咱们要开始结合 LLDB
打印一些类的内部信息,因此只能是新建一个依靠于 objc4-756
源码 project
的 target
出来。一样的,咱们仍是选择 macOS
的命令行做为咱们的 target
。
接着咱们新建一个类 Person
,而后添加一些实例变量和属性出来。
// Person.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject { NSString *hobby; } @property (nonatomic, copy) NSString *nickName; @end NS_ASSUME_NONNULL_END // main.m #import <Foundation/Foundation.h> #import <objc/runtime.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); NSLog(@"%s", p); } return 0; } 复制代码
咱们打一个断点到 main.m
文件中的 NSLog
语句处,而后运行刚才新建的 target
。
target
跑起来以后,咱们在控制台先打印输出一下 pClass
的内容:
咱们这个时候须要借助指针平移来探索,而对于类的内存结构咱们先看下面这张表格:
类的内存结构 | 大小(字节) |
---|---|
isa | 8 |
superclass | 8 |
cache | 16 |
前两个大小很好理解,由于 isa
和 superclass
都是结构体指针,而在 arm64
环境下,一个结构体指针的内存占用大小为 8 字节。而第三个属性 cache
则须要咱们进行抽丝剥茧了。
cache_t cache; struct cache_t { struct bucket_t *_buckets; // 8 mask_t _mask; // 4 mask_t _occupied; // 4 } typedef uint32_t mask_t; 复制代码
从上面的代码咱们能够看出,cache
属性实际上是 cache_t
类型的结构体,其内部有一个 8 字节的结构体指针,有 2 个各为 4 字节的 mask_t
。因此加起来就是 16 个字节。也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节,32 是 10 进制的表示,在 16 进制下就是 20。
bits
属性咱们刚才在控制台打印输出了 pClass
类对象的内容,咱们简单画个图以下所示:
那么,类的 bits
属性的内存地址瓜熟蒂落的就是在 isa
的初始偏移量地址处进行 16 进制下的 20 递增。也就是
0x1000021c8 + 0x20 = 0x1000021e8
复制代码
咱们尝试打印这个地址,注意这里须要强转一下:
这里报错了,问题实际上是出在咱们的 target
没有关联上 libobjc.A.dylib
这个动态库,咱们关联上从新运行项目
咱们重复一遍上面的流程:
这一次成功了。在 objc_class
源码中有:
class_rw_t *data() { return bits.data(); } 复制代码
咱们不妨打印一下里面的内容:
返回了一个 class_rw_t
指针对象。咱们在 objc4-756
源码中搜索 class_rw_t
:
struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; ...省略代码... } 复制代码
显然的,class_rw_t
也是一个结构体类型,其内部有 methods
、properties
、protocols
等咱们十分熟悉的内容。咱们先猜测一下,咱们的属性应该存放在 class_rw_t
的 properties
里面。为了验证咱们的猜测,咱们接着进行 LLDB
打印:
咱们再接着打印 properties
:
properties
竟然是空的,难道是 bug?其实否则,这里咱们还漏掉了一个很是重要的属性 ro
。咱们来到它的定义:
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; ...隐藏代码... } 复制代码
ro
的类型是 class_ro_t
结构体,它包含了 baseMethodList
、baseProtocols
、ivars
、baseProperties
等属性。咱们刚才在 class_rw_t
中没有找到咱们声明在 Person
类中的实例变量 hobby
和属性 nickName
,那么但愿就在 class_ro_t
身上了,咱们打印看看它的内容:
根据名称咱们猜想属性应该在 baseProperties
里面,咱们打印看看:
Bingo! 咱们的属性 nickName
被找到了,那么咱们的实例变量 hobby
呢?咱们从 $8 的 count 为 1 能够得知确定不在 baseProperites
里面。根据名称咱们猜想应该是在 ivars
里面。
哈哈,hobby
实例变量也被咱们找到了,不过这里的 count
为何是 2 呢?咱们打印第二个元素看看:
结果为 _nickName
。这一结果证明了编译器会帮助咱们给属性 nickName
生成一个带下划线前缀的实例变量 _nickName
。
至此,咱们能够得出如下结论:
class_ro_t
是在编译时就已经肯定了的,存储的是类的成员变量、属性、方法和协议等内容。class_rw_t
是能够在运行时来拓展类的一些属性、方法和协议等内容。
研究完了类的属性是怎么存储的,咱们再来看看类的方法。
咱们先给咱们的 Person
类增长一个 sayHello
的实例方法和一个 sayHappy
的类方法。
// Person.h - (void)sayHello; + (void)sayHappy; // Person.m - (void)sayHello { NSLog(@"%s", __func__); } + (void)sayHappy { NSLog(@"%s", __func__); } 复制代码
按照上面的思路,咱们直接读取 class_ro_t
中的 baseMethodList
的内容:
sayHello
被打印出来了,说明 baseMethodList
就是存储实例方法的地方。咱们接着打印剩下的内容:
能够看到 baseMethodList
中除了咱们的实例方法 sayHello
外,还有属性 nickName
的 getter
和 setter
方法以及一个 C++
析构方法。可是咱们的类方法 sayHappy
并无被打印出来。
咱们上面已经获得了属性,实例方法的是怎么样存储,还留下了一个疑问点,就是类方法是怎么存储的,接下来咱们用 Runtime
的 API 来实际测试一下。
// main.m void testInstanceMethod_classToMetaclass(Class pClass){ const char *className = class_getName(pClass); Class metaClass = objc_getMetaClass(className); Method method1 = class_getInstanceMethod(pClass, @selector(sayHello)); Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello)); Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy)); Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy)); NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4); NSLog(@"%s",__func__); } int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); testInstanceMethod_classToMetaclass(pClass); NSLog(@"%p", p); } return 0; } 复制代码
运行后打印结果以下:
首先 testInstanceMethod_classToMetaclass
方法测试的是分别从类和元类去获取实例方法、类方法的结果。由打印结果咱们能够知道:
sayHello
是实例方法,存储于类对象的内存中,不存在于元类对象中。而 sayHappy
是类方法,存储于元类对象的内存中,不存在于类对象中。sayHello
是类对象的实例方法,跟元类不要紧;sayHappy
是元类对象的实例方法,因此存在元类中。咱们再接着测试:
// main.m void testClassMethod_classToMetaclass(Class pClass){ const char *className = class_getName(pClass); Class metaClass = objc_getMetaClass(className); Method method1 = class_getClassMethod(pClass, @selector(sayHello)); Method method2 = class_getClassMethod(metaClass, @selector(sayHello)); Method method3 = class_getClassMethod(pClass, @selector(sayHappy)); Method method4 = class_getClassMethod(metaClass, @selector(sayHappy)); NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4); NSLog(@"%s",__func__); } int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); testClassMethod_classToMetaclass(pClass); NSLog(@"%p", p); } return 0; } 复制代码
运行后打印结果以下:
从结果咱们能够看出,对于类对象来讲,经过 class_getClassMethod
获取 sayHappy
是有值的,而获取 sayHello
是没有值的;对于元类对象来讲,经过 class_getClassMethod
获取 sayHappy
也是有值的,而获取 sayHello
是没有值的。这里第一点很好理解,可是第二点会有点让人糊涂,不是说类方法在元类中是体现为对象方法的吗?怎么经过 class_getClassMethod
从元类中也能拿到 sayHappy
,咱们进入到 class_getClassMethod
方法内部能够解开这个疑惑:
Method class_getClassMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; return class_getInstanceMethod(cls->getMeta(), sel); } Class getMeta() { if (isMetaClass()) return (Class)this; else return this->ISA(); } 复制代码
能够很清楚的看到,class_getClassMethod
方法底层其实调用的是 class_getInstanceMethod
,而 cls->getMeta()
方法底层的判断逻辑是若是已是元类就返回,若是不是就返回类的 isa
。这也就解释了上面的 sayHappy
为何会出如今最后的打印中了。
除了上面的 LLDB
打印,咱们还能够经过 isa
的方式来验证类方法存放在元类中。
具体的过程笔者再也不赘述。
咱们在探索类和元类的时候,对于其建立时机还不是很清楚,这里咱们先抛出结论:
那么如何来证实呢,咱们有两种方式能够来证实:
LLDB
打印类和元类的指针MachoView
打开程序二进制可执行文件查看:LLDB
来打印类和元类的指针,或者 MachOView
查看二进制可执行文件class_ro_t
结构中存储了编译时肯定的属性、成员变量、方法和协议等内容。咱们完成了对 iOS
中类的底层探索,下一章咱们将对类的缓存进行深一步探索,敬请期待~