在iOS底层中,关于类的加载,在应用程序开始加载时,首先通过dyld
链接到动态库objc
,从objc
中的init
方法将类中的属性、方法、协议通过编译成Mach-o
格式,而在Mach-o
中是通过里面的data()
读取其中的ro
(干净的内存,由于iOS是运行时,需要在内存中不断插入,修改,因此将干净内存copy到rw中,rwe是脏内存,在wwdc2020有详细介绍)
和rw
,那么他是如何加载的,这是本文探索的重点。
而在之前探索过dyld与objc的关联,在objc
的源码中我们研究过一个很重要的函数_read_images
,在这个函数中,有很多事件的处理:
1: 条件控制进行一次的加载
2: 修复预编译阶段的 `@selector` 的混乱问题
3: 错误混乱的类处理
4:修复重映射一些没有被镜像文件加载进来的类
5: 修复一些消息!
6: 当我们类里面有协议的时候 : readProtocol
7: 修复没有被加载的协议
8: 分类处理
9: 类的加载处理
10 : 没有被处理的类 优化那些被侵犯的类
关于类的加载,我们只需要查看第4点和第10点,首先我们以能够运行的objc4-781源码当作工程来研究。
在KCObjc
中有一个Person类,里面的代码:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : 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
#import "Person.h"
@implementation Person
//+ (void)load{
//
//}
- (void)kc_instanceMethod2{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod3{
NSLog(@"%s",__func__);
}
+ (void)kc_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
可以看到在Person.m中,将load
方法进行了注释掉,接下来来探究一下
看下图:
看上图可以得出,当系统对类进行加载时,并不会执行3724-3749
之间的代码,而当我们将load
方法写出来时,程序执行了3724-3749
的代码,如下图:
那么进来之后,我们继续执行下一步:
iShot2020-10-15 19.16.06.png
当我们在3747行打上断点后,发现if (cls->isSwiftStable()) {
这个判断中的代码并不会执行,而是直接执行3747行代码,当我们往后继续看代码时(如下图),你会发现,它有多个方法中都有realizeClasses
的函数名,那根据之前对方法的慢速查找时,也有着类似的地方。
那么接下来我们来看一下realizeClassWithoutSwift
方法的实现,继续执行代码:
在上图中,将对cls
的data()
值赋值给ro
,在接下来中(看下图),会执行方框中的代码,将ro
赋值给rw
,设置cls
的数据为rw
当继续执行程序,突然发现了一个很奇怪的东西,看下图:
iShot2020-10-15 19.34.11.png
当上面的cls
的rw
已经赋值了,在我们打印cls
的值时,却始终为空;
那么继续执行程序:
iShot2020-10-15 19.42.52.png
就会走到下面的递归程序中,对父类,元类进行递归执行;
iShot2020-10-15 19.45.14.png在上图中,当程序执行到此处时,我们发现在进这个方法时,cls是Person,而在此处编程了地址,这是因为这是元类信息,而这个地址跟我们之前打印的cls的isa地址一摸一样。
而执行到这个方法最后,有一个很重要的方法:! iShot2020-10-15 20.01.00.png下面我们继续进入这个方法去看一下他的源码实现:
iShot2020-10-15 20.17.26.png
方框中是自己写判断探索的类为自己创建的类,因为系统的类并不好判断,只能使用我们自己的类去探索,我们现在去看一下class_ro_t
是什么东西,先看一下他的结构:
上图中,方框中的属性是方法列表。
以下的代码是我利用控制台对kc_ro
的一些输出,可以看到
(lldb) p kc_ro
(const class_ro_t *) $0 = 0x00000001000080a8
(lldb) p $0.baseMethodList
(method_list_t *const) $1 = 0x00000001000080f0
Fix-it applied, fixed expression was:
$0->baseMethodList
(lldb) p *$1
(method_list_t) $2 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 3
first = {
name = "kc_instanceMethod2"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
}
}
}
(lldb) p $2.get(0)
(method_t) $3 = {
name = "kc_instanceMethod2"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
}
(lldb) p $2.get(1)
(method_t) $4 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
}
(lldb) p $2.get(2)
(method_t) $5 = {
name = "kc_instanceMethod3"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003e40 (KCObjc`-[Person kc_instanceMethod3])
}
iShot2020-10-15 20.20.46.png
而继续执行程序,下面的代码会很有意思,在prepareMethodLists
方法中有一个fixupMethodList
,在这个方法中,会对方法进行排序,如下图:
下面去验证一下它是否排序:
(lldb) p mlist
(method_list_t *) $6 = 0x00000001000080f0
(lldb) p *$6
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 3
first = {
name = "kc_instanceMethod1"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
}
}
}
(lldb) p $7.get(0)
(method_t) $8 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
}
(lldb) p $7.get(1)
(method_t) $10 = {
name = "kc_instanceMethod2"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
}
(lldb) p $7.get(2)
(method_t) $9 = {
name = "kc_instanceMethod3"
types = 0x0000000100003f9e "v16@0:8"
imp = 0x0000000100003e40 (KCObjc`-[Person kc_instanceMethod3])
}
可以看到方法列表名是排序的;
继续执行程序,看下图所示:
iShot2020-10-15 20.26.47.png
当排序完之后,这个rwe值为空,也就是说,我们探究到了data()->ro->rw->rwe(这一步没执行,值为空)
。
而在上面,探究过在类中加入了load
方法后,就执行了非那一段非懒加载的代码,那么非懒加载类和懒加载类是什么意思呢?
就是说方法在需要的时候就加载,不需要的时候就不加载;
那么结果就是,当有load
方法时,他就是非懒加载类,没有load
时,就是懒加载类,当有需要让程序提前执行时,就添加load
方法,
而非懒加载类为什么会来,是由于消息转发,请看下图,当没有load
方法,执行到realizeClassWithoutSwift
方法之后,我们去打印堆栈信息,从dyld_start
->_objc_msgSend_uncached
->lookUpImpOrForward
->realizeClassWithoutSwift
,可以得到,这一过程是消息转发的过程:
而为什么要这样呢,因为在加载方法的过程中,会有很多代码,对方法进行排序,如果方在main
函数之前,那么main
的启动会非常慢,那么当你的方法从开始到结束都没调用,那这样运行会耗费内存,而设计懒加载,节省了内存的消耗。
所以在我们一般开发中,不建议用load
。
下面对懒加载类和非懒加载类进行一些总结:
1: 懒加载类情况 数据加载推迟到第一次消息的时候
lookUpImpOrForward
realizeClassMaybeSwiftMaybeRelock
realizeClassWithoutSwift
methodizeClass
2: 非懒加载类情况 map_images的时候 加载所有类数据
_getObjc2NonlazyClassList
readClass
realizeClassWithoutSwift
methodizeClass
在上面的探究过程中,漏了对分类的探究,下面来简单的来探究一下分类的加载,在下一篇文章中将会详细介绍分类的加载。
在main.m中添加分类代码:
@interface Person (LG)
@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod3;
- (void)cate_instanceMethod2;
+ (void)cate_sayClassMethod;
@end
@implementation Person (LG)
- (void)cate_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)cate_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
在终端通过指令clang -rewrite-objc main.m -o main.cpp
编译cpp文件。
看下图,当编译完cpp文件后,找分类的结构,可以看到一个_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;
};
iShot2020-10-15 21.32.23.png
方法代码:函数名就是imp,是一个method_t
结构,而分类中的属性没有set,get方法,只能通过关联属性实现set,get方法。
那么继续回到objc
源码中来,从上面的探索中,分类是在methodizeClass
方法中,而在上面我们通过对类方法进行排序的验证之后,继续执行代码,如下图所示:
在上图中,程序中有分类,但是这个分类的代码并没有执行,直接跳到1480行代码,unattachedCategories
的初始化在runtime
,那么继续去看看attachToClass
方法的实现:
在上图中,我们将程序继续执行,却发现程序直接跳过了1150-1161行,而在这中间有一个重要的方法attachCategories
,这里有两个一样的函数调用,一个是实例方法,一个是类方法,那么我们就进入这个方法看一下:
上图是一部分代码,其中图片中是比较重要的一部分:
下图是对一些数据的处理,其中cats_count
值为1是因为在前面是一个方法一个方法的进来,而ATTACH_BUFSIZ
的值为64,则1374行的作用就是倒叙插入。
来看一下mlist的结构,
iShot2020-10-15 22.01.29.png
继续执行,当执行完循环后,看下图:
执行完循环,在1398处打断点,当程序执行到1400处后,rwe有值了;
iShot2020-10-15 22.08.41.png
那rwe
是什么时候开始有值了呢?
我们往上看代码,如下图:
可以看到rew在此处调用了一个方法;
这个方法对rew进行判断,如果存在,就直接获取,如果不存在,就开辟创建,如下两图:
iShot2020-10-15 22.11.59.png iShot2020-10-15 22.13.40.png
rwe是在分类的添加的时候才进行处理,所以在这个时候才会进行初始化。
在经过一翻查询后,可以得到分类的添加处理在addMethod
、addPro
、addprotocol
的时候都会进行处理。
网友评论