上一篇文章应用程序加载了解了应用程序的加载流程,dyld和objc_init的启动关系,也了解了map_images和load_images的调用时机。本片文章我们需要解决遗留的一些问题,同时在上一篇文章的基础上做一些扩展。
1.+load()方法
在学习应用程序加载dyld的时候,我们定位到了load_images方法的调用时机。load方法是我们平时开发中常用的,很多面试题也经常会出现,比如类、父类、分类中load方法的调用顺序等。这里我们首先通过案例明确一下load方法的调用顺序,然后结合源码去分析。
1.案例分析
引入一个案例,父类Fater、子类Son及其分类均实现了load方法,见下面代码:
// 父类
@implementation Father
+ (void)load{
NSLog(@"%s", __func__);
}
@end
// 父类分类
@implementation Father (GF)
+ (void)load{
NSLog(@"%s", __func__);
}
@end
// 子类
@implementation Son
+ (void)load{
NSLog(@"%s", __func__);
}
@end
// 子类分类
@implementation Son (GF)
+ (void)load{
NSLog(@"%s", __func__);
}
@end
运行程序,会发现,系统会自动调用各个类的load方法。调用顺序为:父类->子类->分类。见下图:
2.load方法的过程
那么上面案例中的load方法是什么时候调用的呢?
上一篇中我们知道,调用sNotifyObjCInit也就是调用了load_images,在libobjc.A.dylib源码中可以找到load_images实现。见下图:
在load_images函数实现中,找到load方法的调用入口:call_load_methods。同时根据注释,我们也可以验证这一点。在call_load_methods 中会反复调用class的+load方法,直到没有更多。
- 类load方法
在call_class_loads中实现了函数级别的load方法调用,见下图:
源码中是从loadable_classes获取获取的方法,并调用。
- 分类load方法
在call_category_loads中实现了函数级别的load方法调用,见下图:
源码中是从loadable_categories获取获取的方法,并调用。
这里需要思考个问题,我们找到了load方法的调用过程,但是这个顺序在哪里确定下来的?load_images中有个关键地方还没有研究!
3.load方法调用顺序的确定
Discover load methods发现加载方法,这里会对load方法进行处理,并放入对应的loadable_classes和loadable_categories表中。
prepare_load_methods方法到底在做什么准备工作呢?进去看看。见下面源码:
- 非懒加载的类处理
_getObjc2NonlazyClassList方法做什么呢?获取所有实现了+load方法的类(也就是非懒加载的类),加入到一个静态数组loadable_classes。看schedule_class_load的源码实现:
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
首先判断判断类是否为空,如果为空,直接返回;如果不为空,判断是否已经设置为RW_LOADED,如果已经被设置为RW_LOADED ,直接返回。然后进行了递归操作,获取当前类的父类,以确保父类优先进行了处理,然后调用add_class_to_loadable_list,将类和load方法添加到loadable_classes数组中。
- 非懒加载的分类处理
_getObjc2NonlazyCategoryList方法做什么呢?获取所有实现了+load的分类(非懒加载的分类),然后判断分类所对应的类是否为nil,如果分类所对应的类为nil则跳过,反之初始化分类所对应的类,然后将分类加入一个静态数组loadable_categories。
- 总结:
走完
prepare_load_methods,也就是准备工作做好后,程序走到call_load_methods,调用load方法。call_load_methods方法的实现,从代码中我们可以看出来,在这个方法中系统会把类本身的+load方法和分类的+load方法都调用了,并且类的+load要比分类中先调用。 那么如果有多个分类都实现了+load,先调用哪个分类呢?这个和编译有关,编译时谁在前面谁先调用。
至此我们明白了load方法的调用顺序,但是依然有个问题,分类的方法是如何加载到类中的呢?后面的文章再解密!
2.+initialize()方法
initialize方法也是面试过程中经常问到的问题,那么它与load方法又有什么区别呢?它又是在哪里调用的呢?
1.initialize源码分析
其实在进行消息慢速查找流程中,我们已经找到了入口。见下图:
在此过程中,首先要确保类已经实现,也就是针对懒加载的类(未实现load方法)进行实现。类实现后,再进行initialize方法的调用。见下图:
这里只分析initialize方法的调用的调用过程,类的实现会在之后的章节中分析。
继续跟踪源码,最终找到了方法调用的位置:
initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass->callInitialize->objc_msgSend(cls, @selector(initialize))
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
- 通过该源码可以发现:
initialize是通过obj_msgSend,消息发送的方式进行调用的,与load方法调用有本质区别;load方法是在应用程序加载阶段,通过函数级别调用,而非消息发送。
那么initialize调用逻辑是怎样的呢?关键在initializeNonMetaClass实现中,见下图:
- 所以可以得出以下结论:当一个类在发送消息时,采用递归的方式,优先判断父类是否
isInitialized(),如果没有会调用对应的initialize方法,因为是消息发送逻辑,所以如果子类没有实现initialize方法,就会调用父类的initialize方法。
2.initialize案例验证
依然引用分析load的案例,父类Fater、子类Son及其分类同时实现了load方法和initialize方法,见下面代码:
// 父类
@implementation Father
+ (void)load{
NSLog(@"%s", __func__);
}
+ (void)initialize
{
NSLog(@"%s", __func__);
}
@end
// 父类分类
@implementation Father (GF)
+ (void)load{
NSLog(@"%s", __func__);
}
+ (void)initialize
{
NSLog(@"%s", __func__);
}
@end
// 子类
@implementation Son
+ (void)load{
NSLog(@"%s", __func__);
}
+ (void)initialize
{
NSLog(@"%s", __func__);
}
@end
// 子类分类
@implementation Son (GF)
+ (void)load{
NSLog(@"%s", __func__);
}
+ (void)initialize
{
NSLog(@"%s", __func__);
}
@end
直接启动程序,会发现只打印了load方法,initialize并没有触发,这个结果也验证了前面的分析,load方法是由应用程序加载阶段,dyld流程中自动调动。而initialize是在向类发送消息的时候才会被触发。
-
创建
Son对象,运行程序,结果如下:- 首先调用了父类的
initialize,再调用子类的initialize - 因为分类中也实现了
initialize方法,所以会调用分类的initialize,本类不会被调用 load方法,先于initialize方法调用
- 首先调用了父类的
-
创建
Father对象,运行程序,结果如下:- 不会触发子类的
initialize方法
- 不会触发子类的
-
注意: 如果子类和子类的分类都没有实现initialize方法,在对子类进行发送消息时,父类的initialize方法会被调用两次!见下图:- 在子类进行第一次消息发送时(父类没有调用过的情况下),进行递归处理,确保父类先调用了
initialize方法,而子类本身没有实现,所以消息发送流程会找到父类,进行调用一次!所以会调用两次!
- 在子类进行第一次消息发送时(父类没有调用过的情况下),进行递归处理,确保父类先调用了