在上一篇 iOS 底层原理-类的加载(上) 分析了 map_images
的流程,那么问题来了,一个应用程序有很多的类,如果都按照这个流程走,太耗费性能了,这是苹果不允许的,那么它真实的流程是什么样子呢?下面我们一起来探索下
如何定位当前研究的类
我们知道一个应用程序运行需要加载很多的类文件,那么我们如何去定位到我们要研究的指定的类呢?这里我们需要对源码进行一些处理,让它只针对我们需要研究的类。上一篇中讲到 objc
源码通过 cls->mangledName()
来获取类名,那么我们判断自定义研究的类名与 mangledName
是否一致,如果一致,则就是我们需要研究的,反之,则不需要研究。如下
const char *mangledName = cls->mangledName();
const char *LCPersonName = "LCPerson";
if (strcmp(mangledName, LCPersonName) == 0) {
bool lc_isMeta = cls->isMetaClass(); // 用来判断是否是元类,排除干扰(如果需要)
if (!lc_isMeta) {
printf("%s: 这个是我要研究的 %s \n", __func__, LCPersonName);
}
}
这样我们就可以避免了其他类的干扰,只关注我们自定义的类。核心的方法在上一篇中提到了,只需要将自定义的代码添加上去就可以了
懒加载与非懒加载类
在 ObjC
中,判断一个类是否是懒加载类,就是看它是否实现了 +load
方法
实现了
+load
方法,它就是非懒加载类反之,就是懒加载类
+load
方法会提前加载(+load
会在load_images
调用,前提是类存在,这个会在后面进行验证)。如果没实现+load
方法,会在第一次调用方法时加载
实现 load 方法的类加载
创建一个 LCPerson
类,声明并实现一个实例方法以及重写 +load
方法
@interface LCPerson : NSObject
- (void)lc_instanceMethod1;
@end
@implementation LCPerson
+ (void)load {
NSLog(@"%s", __func__);
}
- (void)lc_instanceMethod1 {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello!");
}
return 0;
}
运行 objc-781
源码,查看打印结果,如下
非懒加载的流程图如下
未实现 load 方法的类加载
删除 +load
方法,此时我们运行 objc-781
源码,发现只会打印 readClass
方法,那么它是什么时候加载的呢?现在我们在 main
函数中添加如下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [LCPerson alloc];
[person lc_instanceMethod1];
NSLog(@"Hello!");
}
return 0;
}
1. 断点调试
此时在 main
函数处打个断点,我们运行 objc-781
源码,调用了 readClass
方法后直接来到断点处,再次执行下一步,可以看到调用流程如下
2.堆栈信息
我们在 realizeClassWithoutSwift
处打个断点,运行 objc-781
源码,查看堆栈信息
方法调用流程为什么能来 realizeClassWithoutSwift
?,本质上调用 alloc
,alloc
的本质是消息的发送。因为是第一次调用,会走消息的慢速查找 lookUpImpOrForward
,类没有初始化,会调用 realizeClassMaybeSwiftAndLeaveLocked
,后续调用 realizeClassMaybeSwiftMaybeRelock
,最后调用 realizeClassWithoutSwift
,后面是实现类。
懒加载类的流程图如下
分类
分类是什么?要研究分类,首先我们需要知道分类是什么,怎么研究呢?可以从下面三种方法探索,首先在 main
文件中定义个分类,如下
@interface LCPerson (LC)
@property (nonatomic, copy) NSString *lc_name;
@property (nonatomic, assign) int lc_age;
- (void)lc_instanceMethod3;
- (void)lc_instanceMethod1;
- (void)lc_instanceMethod2;
@end
@implementation LCPerson (LC)
- (void)lc_instanceMethod3 {
NSLog(@"%s",__func__);
}
- (void)lc_instanceMethod1 {
NSLog(@"%s",__func__);
}
- (void)lc_instanceMethod2 {
NSLog(@"%s",__func__);
}
+ (void)lc_sayClassMethod {
NSLog(@"%s",__func__);
}
@end
1. 通过 clang 探索
cd
到 main.m
所在的文件夹,执行 clang -rewrite-objc main.m -o main.cpp
,文件夹中会多出一个 main.cpp
类文件,打开 main.cpp
,就可以看到底层编译,分类的类型是 _category_t
结构体
搜索 struct _category_t
,看它的结构如下
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
从上面对应关系可以看出,由于 LCPerson (LC)
没有协议,所以对应的为 0,结构体中有两个 _method_list_t
,分别表示的是 实例方法列表
和 类方法列表
。搜索 _CATEGORY_INSTANCE_METHODS_LCPerson_
查看它的实例方法列表底层实现
对应分类中我们添加的三个实例方法,其格式为 sel + 签名 + 地址
,看着很熟悉是不是?就是 method_t
结构体的属性
struct method_t {
SEL name; // 方法名
const char *types; // 方法签名
MethodListIMP imp; // 函数地址
struct SortBySELAddress :
public std::binary_function
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
我们再来看下分类的属性列表
可以看到,我们在分类里定义了属性,但是在底层编译中并没有看到它的成员变量,而且在实例方法列表中也没有看到属性的 setter
以及 getter
方法。这是因为分类中定义的属性不会生成成员变量,只是有 setter
和 getter
的声明,并没有实现它的 setter
和 getter
方法。我们可以通过关联对象来设置(objc_setAssociatedObject
和 objc_getAssociatedObject
)。
2. 通过 Xcode 官方文档探索
如果不会 clang
,可以通过 Xcode
文档搜索 Category
查看
3. 通过 objc 源码探索
打开 objc
源码,搜索 category_t
分类的本质是一个
_category_t
的结构体类型,它有两个属性:name
(类名)和cls
(本类对象);两个方法列表:实例方法列表和类方法列表;一个协议列表;一个属性列表。另外分类中的属性是没有成员变量的,只有setter
和getter
的声明,并没有实现setter
和getter
方法。
分类数据的加载时机
在上一篇 iOS 底层原理-类的加载(上) 中分析了分类数据的加载是在 attachCategories
方法中实现的,且分类的加载顺序是根据编译器编译的先后顺序加载到类中,越晚加进来,越在前面。
但是它在什么时机调用的,我们还不得而知,下面就让我们就一起探索下吧
什么时机调用
下面我们通过反推法和堆栈信息两种方法去探索
1. 反推法
- 在
objc
源码中全局搜索attachCategories(
,发现只有两处调用,分别是attachToClass
和load_categories_nolock
通过调试发现不会走 attachToClass
中的 attachCategories
(这里我们设置的主类和分类都实现了 +load
方法,如果主类未实现 +load
方法,分类有实现 +load
方法,则会调用 attachToClass
中的 attachCategories
,后面会分析到)
- 全局搜索
load_categories_nolock
的调用,发现有两处调用_read_images
和loadAllCategories
_read_images
中的调用如下
通过调试,不会走 _read_images
的 if
流程,走的是 loadAllCategories
的流程
- 再次全局搜索
loadAllCategories
的调用,发现只有一次调用,是在load_images
时调用的
2. 堆栈信息
现在我们在 attachCategories
中加上自定义的断点,bt
查看它的堆栈
这里也验证了我们刚刚反推的流程,反推流程和正常流程图如下
分类与类的搭配使用(+load 方法实现与否)
上面我们分析了主类的懒加载与非懒加载,下面我们看下它们搭配使用(+load 实现与否)的加载情况。大致可以分为四种情况
分类实现 +load | 分类未实现 +load | |
---|---|---|
主类实现 +load | 非懒加载类 + 非懒加载分类 | 非懒加载类 + 懒加载分类 |
主类未实现 +load | 懒加载类 + 非懒加载分类 | 懒加载类 + 懒加载分类 |
主类源码
/*------ .h ------**/
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, assign) int kc_age;
- (void)kc_instanceMethod1;
- (void)kc_instanceMethod2;
- (void)kc_instanceMethod3;
+ (void)kc_sayClassMethod;
@end
/*------ .m ------**/
#import "LGPerson.h"
@implementation LGPerson
+ (void)load {
}
- (void)kc_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)kc_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
分类源码
/*------分类LGA.h ------**/
@interface LGPerson (LGA)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end
/*------分类LGA.m ------**/
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_2{
NSLog(@"%s",__func__);
}
- (void)cateA_1{
NSLog(@"%s",__func__);
}
- (void)cateA_3{
NSLog(@"%s",__func__);
}
@end
/*------分类LGB.h ------**/
@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
/*------分类LGB.m ------**/
#import "LGPerson+LGB.h"
@implementation LGPerson (LGB)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateB_2{
NSLog(@"%s",__func__);
}
- (void)cateB_1{
NSLog(@"%s",__func__);
}
- (void)cateB_3{
NSLog(@"%s",__func__);
}
@end
非懒加载类与非懒加载分类
这种情况是 主类实现了 +load 方法,分类也实现了 +load 方法
,前面我们分析的就是这种情况,我们可以得出如下结论
- 类的数据加载是在
_read_images
中调用_getObjc2NonlazyClassList
加载,插入表操作,ro、rw 的操作。调用路径:
map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
,后面会走 load_images
方法
- 分类的数据加载是通过
load_images
加载到类中的,调用路径为
load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists
通过我们自定义的打印数据,运行程序,打印日志如下
非懒加载类与懒加载分类
这种情况是 主类实现了 +load 方法,分类没有实现 +load 方法
- 首先,我们在
realizeClassWithoutSwift
的自定义代码中下个断点,查看ro
情况
从上面可以看到,方法列表中有 16 个方法,但是在主类中没有这么多,那剩余的方法是从哪里来的呢?我们通过 lldb
命令一一打印出方法列表中的方法
从上面的打印信息可以看出,除了主类的方法外,分类的方法也被加载进来了,依次是 LGA->LGB->LGPerson
。方法还没有排序,说明分类的数据没有进行非懒加载时,通过 cls->data()
读取到 mach-o
可执行文件时,数据就已经进来了,不需要在运行时添加进去了
- 下面我们进入
methodizeClass
方法中查看排序后的方法列表数据
通过打印发现,方法排序只对 同名方法进行了排序
,而类中的其他方法则是按照 imp地址有序排列
,排序的源码如下(核心代码)
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[I];
ASSERT(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
}
static void
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
// sel - imp
// Sort by selector address.
if (sort) {
method_t::SortBySELAddress sorter;
std::stable_sort(mlist->begin(), mlist->end(), sorter);
}
// Mark method list as uniqued and sorted
mlist->setFixedUp();
}
通过我们自定义的打印数据,运行程序,打印日志如下
懒加载类与懒加载分类
这种情况是 主类和分类都没有实现 +load 方法
,这里我们需要在 main
函数中调用类的实例方法来辅助,添加代码如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person kc_instanceMethod1];
}
return 0;
}
- 我们先通过添加我们自定义的打印,不加任何断点,直接运行程序,看下打印日志
其中的 realizeClassMaybeSwiftMaybeRelock
方法调用是消息发送流程的慢速查找的函数,在第一次发送消息时才走的函数
- 我们在
readClass
处下个断点,读取此时的ro
情况
此时的 baseMethodList
的个数是 16 个,说明也是从 data
中读取出来的
懒加载类 与 非懒加载分类
这种情况是 主类没有实现 +load 方法,分类实现了 +load 方法
- 我们先运行程序,获取打印日志
- 我们在
readClass
处打个断点,查看ro
情况
可以看到,baseMethodList
的 count 是 8 个,我们打印出每个方法如下
可以看到方法列表里是 LGPerson
的三个实例方法和属性的 setter
、getter
方法以及 1 个 cxx
方法,说明 ro
中只有主类的数据。那么怎么查看分类的数据呢?为了调试分类的数据加载,继续往下执行:load_images -> loadAllCategories -> load_categories_nolock
。打印此时的堆栈信息
继续执行,在 attachToClass
方法打个断点,继续点击下一步,走到 attachCategories
主类未实现
+load
,分类实现了+load
,会迫使主类提前加载,即主类强行转换为非懒加载类样式
总结
类和分类搭配使用,其数据的加载时机总结如下:
非懒加载类 + 非懒加载分类
:类的数据加载是在 _read_images 中调用 _getObjc2NonlazyClassList 加载;分类的数据加载是通过 load_images 加载到类中的懒加载类 + 非懒加载分类
:分类实现了+load
,会迫使主类提前加载,即在_read_images
中不会对类做实现操作,在load_images
方法中触发类的数据加载,同时加载分类数据。
非懒加载类 + 懒加载分类
:数据加载在read_image就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。懒加载类 + 懒加载分类
:其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。