今天我们围绕2个问题研究laod
方法的原理
-
load
方法什么时候调用的? -
load
方法和initialize
方法的区别是什么?他们在category
中的调用顺序.
在上一篇我们研究Category实现的原理二:分类信息如何添加到本类中已经知道,分类中的信息最后都会通过memmove
,memcpy
附加到本类中,并且会存放在本类信息的前面.所以分类和本类如果存在相同的方法,会优先调用分类中的方法.那么load
方法也是如此吗?我们研究一下看看.
首先创建一个Person
类,和Person+Test1
,Person+Test2
两个分类,然后再创建一个Student
类继承自Person
类,以及Student
的分类Student+Test1
,重写他们的+ load
方法,并添加一个+ test
方法:
//Person
+ (void)load{
NSLog(@"Person load");
}
+ (void)test{
NSLog(@"Person test");
}
//Person+Test1
+ (void)load{
NSLog(@"Person+Test1 load");
}
+ (void)test{
NSLog(@"Person+Test1 test");
}
//Person+Test2
+ (void)load{
NSLog(@"Person+Test2 load");
}
+ (void)test{
NSLog(@"Person+Test2 test");
}
//Student
+ (void)load{
NSLog(@"Student load");
}
//Studeng+Test1
+ (void)load{
NSLog(@"Student+Test1 load");
}
直接运行打印结果如下:
会发现我们连
Person
和Student
类的实例都没创建,每个类的load
方法都打印了?这是为什么?难道分类中的load
方法没有附加到本类中去吗?我们写个方法,打印出Person
类中所有的方法查看一下:
- (void)getClassMethodWithClass:(Class)cls{
unsigned int count;
NSMutableString *names = [NSMutableString string];
Method *methods = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i ++) {
NSString *methodName = NSStringFromSelector(method_getName(methods[i]));
[names appendFormat:@"%@ ,",methodName];
}
free(methods);
NSLog(@"cls: %@, methods:%@",cls,names);
}
从打印结果可以看出,
load
方法和test
方法都已经附加到了本类中.
我们再调用一下Student
的+ Test
方法看看调用结果:
从结果上可以看到test
方法的确如我们之前所说,被附加到了本类中并且优先调用,那为什么每个类中load
方法都会调用呢?
我们从runtime
源码中寻找答案,查看源码步骤如下:
打开objc-os.mm
文件->找到_objc_init()
方法->进入load_images
->进入call_load_methods()
方法:
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
//先调用类的load方法
call_class_loads();
}
// 2. Call category +loads ONCE
//再调用分类的load方法
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
可以从源码中看出,runtime会先调用类的load
方法,然后再调用分类的load
方法,我们进入call_class_loads()
方法内部:
static void call_class_loads(void)
{
int I;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
//1: 遍历类列表
for (i = 0; i < used; i++) {
//2: 取出每一个类
Class cls = classes[i].cls;
//3: 取出每个类中的 load 方法
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
//调用 load 方法
(*load_method)(cls, SEL_load); //相当于 load(cls,sel)
}
// Destroy the detached list.
if (classes) free(classes);
}
其中第三步 取出每个类中的laod
方法中的method
是一个结构体,他是专门用来存储类中的load
方法的:
struct loadable_class {
Class cls; // may be nil
IMP method;
};
分类的处理方法call_category_loads()
和类的处理方法同理.所以我们现在明白了,为什么每个load
方法都会调用,因为load
方法是直接拿到每个类load
方法的地址,直接调用,并不是像test()
方法那样通过消息发送机制去查找.
总结一:load
方法的加载顺序,是优先调用类的load
方法,类的load
方法调用完后,再调用分类中的load
方法.
我们再思考一下,如果涉及到继承关系,load
方法的调用顺序又是怎样的呢?
继续从runtime
源码中找寻答案,再次进入load_images
:
可以看到,
load_images
方法中,在执行call_load_methods()
之前有个准备的过程prepare_load_methods
,我们进入此方法:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, I;
runtimeLock.assertWriting();
//根据编译顺序把类存放到 classlist 中
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//定制任务,规划任务,处理类的 load 方法.
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[I];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
//把分类的load方法添加到 loadable_classes 列表中
add_category_to_loadable_list(cat);
}
}
刚才我们已经知道了,在call_class_loads
内部遍历loadable_classes
列表,遍历列表中类的load
方法.实际上prepare_load_methods
方法就是把类存放到loadable_classes
列表中的.所以我们搞清楚prepare_load_methods
方法内部存放类的顺序,就明白了load
方法调用顺序.我们进入schedule_class_load
:
从图中可以看到,我们传入一个类
cls
,在方法内部通过递归的形式找到cls
的父类,然后再存储到loadalbe_classes
中.所以,load方法的加载顺序是,优先调用父类的laod
方法,再调用子类的
那如果没有子类关系,有很多同级别的类,
laod
方法是怎样调用的呢?我们可以试一下,新增Cat
和Dog
类,然后运行:
可以看到,
Cat
和Dog
方法线运行,我们查看一下这些类的编译顺序:
会发现打印顺序和编译顺序是大致一致的,只是
Student+Test1
比Student
先编译,打印顺序却在Student
后面,这不正是我们刚才所说的:优先加载类的laod
方法,再加载分类的load
方法嘛
ok,我们可以改变编译顺序,然后再运行:
结果:
会发现laod
的调用顺序和编译顺序,完全一致.
我们用伪代码梳理一下load
的加载步骤:
// 第一步: 把需要加载的 类 添加到 loadable_list 中
/*
classlist : 需要加载的类
count: 需要加载的类的数量
categorylist: 需要加载的分类
categoryCount: 需要加载的分类的数量
**/
// 1: 首先把类添加到 loadable_list 中
for (int I = 0 ; i < count ; i ++){
class cls = classlist[I];
// 先找到 cls->superclass 添加到 loadable_list
// 再把 cls 添加到 loadable_list
}
//2: 再把分类添加到 loadable_list 中
for (int I = 0 ; i < categoryCount ; i ++){
class cls_cat = categorylist[I];
// 直接把 cls_cat 添加到 loadable_list
}
// 第二部: 调用 laod 方法
do {
// 1: 先调用类的 load 方法
// 2: 在调用分类的 load 方法
}while(...)
现在我们来一个大的总结:
load
方法会在runtime
加载类,分类的时候调用,即使你不使用这个类,同样也会调用.每个类的
load
方法在程序的运行过程中只会调用一次.-
laod
方法的调用顺序:
一. 先调用类的+ load
方法
1:先按照编译顺序调用 (先编译,先调用)
2:调用子类的+ load
方法之前,会先调用父类的+ laod
方法二. 在调用分类的
+ laod
方法
1:按照编译先后顺序调用 (先编译,先调用)
initialize 方法
initialize
方法和load
方法很多人一直傻傻分不清楚,这两个方法的确也很容易搞混淆,下面我们将研究一下initialize
方法,搞清楚他们之间的区别.
我们把上面讲解代码中的+ laod
方法全部替换为+ initialize
方法,然后运行代码,会发现控制台没有任何输出,initialize
都没有调用.事实上,initialize
方法是在向类第一次发送消息的时候调用的.
我们调用[Person alloc];
然后再运行一下:
会发现调用了分类
Person + Test1
的initialize
方法,这说明initialize
方法是通过msgSend(target,sel)
消息发送来调用方法的.如果分类有相同的方法,会优先调用分类的方法.
那么含有继承关系的类调用
initialize
方法的顺序是怎样的呢?我们再创建一个Teacher
类继承Person
类,所以Person
现在就有两个子类:Student
,Teacher
.
我们调用
[Student alloc] , [Teacher alloc]
方法然后运行:
会发现
Person , Student , Teacher
三个类的initialize
方法都调用了,这是为什么呢?刚才我们说过,initialize
方法是通过消息发送机制调用的,按理说它只会调用子类的中的方法,为什么父类的方法也会调用?
我们再把
Student , Teacher
中的initialize
方法注释掉,再运行一下:
更加奇怪了,
Person
类的initialize
调用了3次???
如果我们多调用几次
[Student alloc] , [Teacher alloc]
方法会怎样?
现在我们可以大胆的猜测一下:
initialize
方法既然是通过msgSend(cls,sel)
来实现的,而每次调用子类的initialize
方法之前都会调用父类的initialize
方法,从上图多次调用的运行结果我们也可以猜测,一个类的initialize
方法只会调用一次,那么msgSend(cls,sel)
方法在执行initialize
方法之前很可能会判断父类的的是否已经初始化,如果父类没有初始化,则初始化父类.
那么到底是不是如我们猜测的那样呢?还是从
runtime
源码中找寻答案.
我们在
runtime
源码中搜索objc_msgSend(
,会发现objc_msgSend()
方法的源码都是汇编代码:
汇编语言我也不懂,咱们可以换一个角度切入:消息发送机制的本质就是查找方法,调用实例方法就从类的方法列表中查找,调用类方法就从元类的方法列表中查找,我们在
runtime
源码中查找class_getClassMethod(
:
原来
class_getClassMethod
的内部也是调用class_getInstanceMethod
.点击 进入class_getInstanceMethod
-> 进入lookUpImpOrNil
-> 进入lookUpImpOrForward
:
最后再进入
lookUpImpOrForward
会发现我们要找的重点:
进入_class_initialize
:
会发现正如我们猜测的一样:在
_class_initialize
内部会先判断父类是否已经初始化,如果父类未初始化,则先初始化父类再初始化子类.
回到刚才的问题,为何Student , Teacher
中的initialize
方法都注释掉后,仍然打印3次initialize
?
结合刚才的源码,我们可以大致分析一下[Student alloc] , [Teacher alloc]
底层伪代码大致如下:
bool personIsInItializer = NO;
bool studentIsInItializer = NO;
bool teacherIsInItializer = NO;
//调用 [Student alloc]
if (Student 未被初始化){
if (Student 的父类 Person 未被初始化) {
1: 初始化 Person 类
2: personIsInItializer = YES
}
1: 初始化 Student 类
2: studentIsInItializer = YES;
}
// 调用 [Teacher alloc]
if (Teacher 未被初始化){
if (Teacher 的父类 Person 未被初始化) {
1: 初始化 Person 类
2: personIsInItializer = YES
}
1: 初始化 Teacher 类
2: teacherIsInItializer = YES;
}
在如图的 第
1,2,4
步中都调用了person 的 initialize
方法,所以打印了三遍Person initialize
,但是要搞清楚,调用了三遍initialize
方法并不表示Person
类被初始化了3次,它还是初始化了一次,后面调用的两次的时候条件判断已经不成立了
initialize
总结
initialize
方法会在类第一次接收到消息的时候调用,先调用父类initialize
,在调用子类的initialize
.每个类只会初始化 1 次.- 如果子类没有实现
initialize
会调用父类的initialize
,所以父类的initialize
可能会被调用多次. - 如果分类实现了
initialize
,就覆盖了类本身的initialize
+ load
和 + initialize
方法的区别:
- 调用方式:
load
是直接拿到函数地址,直接调用;initialize
是通过消息机制调用. - 调用时机:
load
是runtime
加载类或者分类的时候调用,不管有没有使用这个类,都会调用,也就是说load
方法是肯定会执行的;initialize
是类第一次接收到消息的时候调用,如果没有向这个类发送消息,则不会调用.