深入了解+load方法的执行顺序
+load方法的执行时机
官方文档描述:
Invoked whenever a class or category is added to the Objective-C runtime。The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
当一个类或者分类被加载到Objectie-C的Runtime运行环境中时,会调用它对应的+load方法。对于所有静态库中和动态库中实现了+load方法的类和分类都有效。
当应用启动时,首先要fork进程,然后进行动态链接。+load方法的调用就是在动态链接这个阶段进行的。动态链接结束之后,会执行程序的main函数。
dyld简介
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。整个加载过程可细分为九步:
1,设置运行环境
2,记载共享缓存
3,实例化主程序
4,加载插入的动态库
5,链接主程序
6,链接插入的动态库
7,执行弱符号绑定
8,执行初始化方法
9,查找入口点并返回
首先新建一个Pserson类,实现+load方法并打断点,得到下图
从图中可以看出执行顺序是_dyld_start->call_load_methods->Person(+load)
由此可知+load方法会在dyld阶段的执行初始化方法中执行。
官方文档中提到了+load方法的执行顺序
一个类的+load方法调用在它的父类的+load方法之后
一个分类的+load方法调用在它本身类的+load方法之后
接下来继续验证类与类之间的+load方法的执行顺序
@interface Person : NSObject
@end
@implementation Person
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
然后新建一个Student类继承Person类。
@interface Student : Person
@end
@implementation Student
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
再新建一个HighSchoolStudent类继承Student。
@interface HighSchoolStudent : Student
@end
@implementation HighSchoolStudent
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
开始运行,查看结果:
---- 0x10b04c0c0 +[Person load]
---- 0x10b04c160 +[Student load]
---- 0x10b04c1b0 +[HighSchoolStudent load]
结果和我们猜想的一样,接下来,我们增加一个Animal类
@interface Animal : NSObject
@end
@implementation Animal
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
现在看一下结果
---- 0x1094930e8 +[Person load]
---- 0x109493138 +[Animal load]
---- 0x109493188 +[Student load]
---- 0x1094931d8 +[HighSchoolStudent load]
我们发现,Animal类的+load方法也调用了,但是它的调用顺序,我们还不知道是如何的。这个时候,我们去Build Phases中的Compile Sources中看一下。
我们发现这里面的四个.m顺序与+load方法打印的顺序一致。那么我们把这里的顺序全部调转,然后再看下打印结果。
---- 0x1010331d8 +[Person load]
---- 0x101033138 +[Student load]
---- 0x1010330e8 +[HighSchoolStudent load]
---- 0x101033188 +[Animal load]
我们发现Animal的输出变到最后了,那我们再次修改顺序。
查看打印结果
---- 0x10803e0e8 +[Animal load]
---- 0x10803e1d8 +[Person load]
---- 0x10803e188 +[Student load]
---- 0x10803e138 +[HighSchoolStudent load]
这样我们能得出结论:有继承关系的类的+load方法的执行顺序,是从基类到子类的;没有继承关系的两个类的+load方法的执行顺序是与编译顺序有关的(Build Phases -> Compile Sources中的顺序)。
了解Mach-o文件布局的人应该明白,先编译的类就会在可执行文件的前面,编译顺序也体现到了没有继承关系的两个类的+load方法的执行顺序中了。
类与分类之间+load方法的执行顺序
看完了类与类之间+load方法的执行顺序,我们来看看类与分类,以及分类与分类之间的+load方法的执行顺序。
在刚才例子的基础上,我们在新建Person的两个分类Test1、Test2,以及Student的两分类Test1、Test2,和Animal的分类Test。
@interface Person (Test1)
@end
@implementation Person (Test1)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface Person (Test2)
@end
@implementation Person (Test2)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface Student (Test1)
@end
@implementation Student (Test1)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface Student (Test2)
@end
@implementation Student (Test2)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface Animal (Test)
@end
@implementation Animal (Test)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
运行,看下执行结果
---- 0x10b1fd3d8 +[Animal load]
---- 0x10b1fd4c8 +[Person load]
---- 0x10b1fd478 +[Student load]
---- 0x10b1fd428 +[HighSchoolStudent load]
---- 0x10b1fd3d8 +[Animal(Test) load]
---- 0x10b1fd478 +[Student(Test2) load]
---- 0x10b1fd478 +[Student(Test1) load]
---- 0x10b1fd4c8 +[Person(Test1) load]
---- 0x10b1fd4c8 +[Person(Test2) load]
有了上面的经验,我们来看下现在的Complie Sources里面的顺序。
到现在为止我们能确定的是,所有分类的+load方法都要在所有类的+load方法之后执行。然后我们修改一些顺序。
再来看看执行结果。
---- 0x108ff1478 +[Person load]
---- 0x108ff14c8 +[Student load]
---- 0x108ff13d8 +[HighSchoolStudent load]
---- 0x108ff1428 +[Animal load]
---- 0x108ff1478 +[Person(Test2) load]
---- 0x108ff14c8 +[Student(Test2) load]
---- 0x108ff14c8 +[Student(Test1) load]
---- 0x108ff1478 +[Person(Test1) load]
---- 0x108ff1428 +[Animal(Test) load]
经过两次的对比我们发现,之前我们猜测正确:所有分类的+load方法都在所有类+load方法之后执行,同时又发现所有分类的+load方法的执行顺序与编译顺序有关,与是谁的分类无关,也与一个类有几个分类无关。
接着上面咱们刚刚说的dyld的执行初始化方法继续说,在Runtime的源码中,可以看到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) {
call_class_loads();
}
// 2. Call category +loads ONCE
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;
}
从这里我们从代码及注释中也能看到:
1,循环调用call_class_loads方法,直到没有可执行的+load方法
2,调用call_category_loads方法
3,重复1->2,直到所有的类和分类的+load方法都执行完毕
所以在这里也能看出来,所有的类的+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.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
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_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
代码循环的次数是loadable_classes_used,这个变量在add_class_to_loadable_list方法中每添加一个Class对象,计数加一。所以在执行到这里的时候,就是当前所有已经加载好的Class对象的数量。loadable_classes数组也是在这个方法中一个一个把Class加进去的。所以无关的两个Class的执行顺序,与编译顺序有关。循环中得到load_method后,调用(*load_method)(cls, SEL_load)方法来调用+load方法。
接下来,再看一下call_category_loads方法
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}
// Destroy the new list.
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
基本上与load_class_loads方法类似,同时还做了一些其他操作。在这里看,我们也就能了解,该函数会获取到所有类及分类的+load方法并执行,所以我们不必手动调用[super load]方法,也能执行到父类的+load方法。
多个镜像中存在+load方法的执行顺序
我们都知道iOS应用的可执行文件,最后会作为一个镜像,加载到内存中,那如果我们还包含动态库和静态库呢?其实静态库会与我们的主程序编译在同一个可执行文件中,也就是一个镜像。但是即便他们在同一个镜像中,主程序与静态库都存在+load方法,其执行顺序是如何的呢?那与主程序不在同一镜像中的动态库中的+load方法,其执行顺序又是如何的呢?
我们在上面的Demo工程中,新建三个Target分别是Cocoa Touch Static Library,以及两个Cocoa Touch Framework,其中两个Framework,设定Mach-o Type一个是Static Library,一个是Dynamic Library,Target名称分别为TestStaticLib、TestStaticFramework和TestDynamcFramework,三个Target中分别有一个类和对应的分类,的代码如下
@interface TestStaticLib : NSObject
@end
@implementation TestStaticLib
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface TestStaticLib (Test)
@end
@implementation TestStaticLib (Test)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface TestStaticFramework : NSObject
@end
@implementation TestStaticFramework
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface TestStaticFramework (Test)
@end
@implementation TestStaticFramework (Test)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface TestDynamicFramework : NSObject
@end
@implementation TestDynamicFramework
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
@interface TestDynamicFramework (Test)
@end
@implementation TestDynamicFramework (Test)
+ (void)load
{
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
执行观察结果
---- 0x1072ab1e8 +[TestDynamicFramework load]
---- 0x1072ab1e8 +[TestDynamicFramework(Test) load]
---- 0x106fb8478 +[Person load]
---- 0x106fb84c8 +[Student load]
---- 0x106fb83d8 +[HighSchoolStudent load]
---- 0x106fb8428 +[Animal load]
---- 0x106fb8478 +[Person(Test2) load]
---- 0x106fb84c8 +[Student(Test2) load]
---- 0x106fb84c8 +[Student(Test1) load]
---- 0x106fb8478 +[Person(Test1) load]
---- 0x106fb8428 +[Animal(Test) load]
首先输出的动态库中的类的+load方法及子类的+load方法,后面的是主工程的输出,我们之前已经看过的。
到这里我们不难发现,动态库由于与主工程不是同一个镜像,所以他们之间的输出是分开的,而且动态库的链接要优先与主工程的链接,来保证主工程链接时能链接到期望的动态库。所以动态库的+load方法都要在主工程的+load方法之前执行。其中动态库中类与子类、类与类之间的+load方法的执行顺序,与之前说的一致,这里就不再赘述。
但是我们还发现一个问题,静态库.a和.framework都没有打印结果。原因,我们也能想到,因为我们没有调用到这两个库的代码,所以也就没有把这两个库加载,链接进来。所以我们只需要在主工程代码中调用一下这两个库中的类即可。
#import "ViewController.h"
#import "TestStaticLib.h"
#import "TestStaticFramework.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[TestStaticLib alloc] init];
[[TestStaticFramework alloc] init];
}
@end
看下打印结果
---- 0x1060a1198 +[TestDynamicFramework load]
---- 0x1060a1198 +[TestDynamicFramework(Test) load]
---- 0x105dae518 +[Person load]
---- 0x105dae568 +[Student load]
---- 0x105dae478 +[HighSchoolStudent load]
---- 0x105dae4c8 +[Animal load]
---- 0x105dae5b8 +[TestStaticLib load]
---- 0x105dae608 +[TestStaticFramework load]
---- 0x105dae518 +[Person(Test2) load]
---- 0x105dae568 +[Student(Test2) load]
---- 0x105dae568 +[Student(Test1) load]
---- 0x105dae518 +[Person(Test1) load]
---- 0x105dae4c8 +[Animal(Test) load]
我们看到了连个静态库类的+load方法打印,在主程序的类的+load方法之后,在主程序的分类的+load方法之前。我们再在Buld Phases -> Link Binary With Libraries中修改一下两个静态库的先后顺序。
---- 0x1060a1198 +[TestDynamicFramework load]
---- 0x1060a1198 +[TestDynamicFramework(Test) load]
---- 0x105dae518 +[Person load]
---- 0x105dae568 +[Student load]
---- 0x105dae478 +[HighSchoolStudent load]
---- 0x105dae4c8 +[Animal load]
---- 0x105dae608 +[TestStaticFramework load]
---- 0x105dae5b8 +[TestStaticLib load]
---- 0x105dae518 +[Person(Test2) load]
---- 0x105dae568 +[Student(Test2) load]
---- 0x105dae568 +[Student(Test1) load]
---- 0x105dae518 +[Person(Test1) load]
---- 0x105dae4c8 +[Animal(Test) load]
我们发现,静态库中的类的+load方法,是必须要有代码调用才能加载链接,并且其类的+load方法的执行顺序与编译顺序有关(Link Binary With Libraries的顺序)。
但是这里还有一个问题,静态库中的分类的+load方法没有调用,其实经常使用静态库开发的同学就知道了,要在主工程的other linker flag中设置-all_load,设置完毕查看运行结果。
---- 0x108a68198 +[TestDynamicFramework load]
---- 0x108a68198 +[TestDynamicFramework(Test) load]
---- 0x108775608 +[Person load]
---- 0x108775658 +[Student load]
---- 0x108775568 +[HighSchoolStudent load]
---- 0x1087755b8 +[Animal load]
---- 0x1087756a8 +[TestStaticFramework load]
---- 0x1087756f8 +[TestStaticLib load]
---- 0x108775608 +[Person(Test2) load]
---- 0x108775658 +[Student(Test2) load]
---- 0x108775658 +[Student(Test1) load]
---- 0x108775608 +[Person(Test1) load]
---- 0x1087755b8 +[Animal(Test) load]
---- 0x1087756a8 +[TestStaticFramework(Test) load]
---- 0x1087756f8 +[TestStaticLib(Test) load]
看静态库中的分类的+load方法调用了,而且打印顺序与静态库中的类的+load方法的打印顺序一致。
如果在+load方法中调用[super load]会有什么影响
我们就继续看例子吧,还是在demo中Student的主类中的+load方法中,调用[super load]。
@implementation Student
+ (void)load
{
[super load];
NSLog(@"---- %p %s", self, __FUNCTION__);
}
@end
查看打印结果
---- 0x10a7b4198 +[TestDynamicFramework load]
---- 0x10a7b4198 +[TestDynamicFramework(Test) load]
---- 0x10a4c1618 +[Person load]
---- 0x10a4c1668 +[Person(Test1) load]
---- 0x10a4c1668 +[Student load]
---- 0x10a4c1578 +[HighSchoolStudent load]
---- 0x10a4c15c8 +[Animal load]
---- 0x10a4c16b8 +[TestStaticFramework load]
---- 0x10a4c1708 +[TestStaticLib load]
---- 0x10a4c1618 +[Person(Test2) load]
---- 0x10a4c1668 +[Student(Test2) load]
---- 0x10a4c1668 +[Student(Test1) load]
---- 0x10a4c1618 +[Person(Test1) load]
---- 0x10a4c15c8 +[Animal(Test) load]
---- 0x10a4c16b8 +[TestStaticFramework(Test) load]
---- 0x10a4c1708 +[TestStaticLib(Test) load]
我们发现第四行调用了[Person(Test1) load]方法,而且在后面这个方法还继续调用了一次。这个原因是什么呢?
首先我们在之前得到的结论,在执行到Student的+load方法之前,其父类Person的+load方法已经完毕了。此时我们执行Student的+load方法,调用了[super load],将父类的+load方法再次执行一次。那么这里为什么是[Person(Test1) load]呢,我们看一下编译顺序。
我们知道分类如果与类方法重名了,那么在之后调用时,会调用分类的同名方法,如果多个分类都实现了这个方法,那么就会按照编译顺序,最后执行最后编译的分类中的同名方法,于是就有了这样的结果。在后面,执行到分类的+load方法时,会把该方法再次执行一次。
所以为了避免一些不必要的麻烦,我们就不必手动去写[super load]方法,同时也不要自己手动调用[object load]方法。
总结
结合了例子以及dyld、Runtime的源码,弄清楚了+load方法的执行时机,以及顺序。下面就是一些总结
1,+load方法是在dyld阶段的执行初始化方法步骤中执行的,其调用为load_images -> call_load_methods
2,一个类在代码中不主动调用+load方法的情况下,其类、子类实现的+load方法都会分别执行一次
3,父类的+load方法执行在前,子类的+load方法在后
4,在同一镜像中,所有类的+load方法执行在前,所有分类的+load方法执行在后
5,同一镜像中,没有关系的两个类的执行顺序与编译顺序有关(Compile Sources中的顺序)
6,同一镜像中所有的分类的+load方法的执行顺序与编译顺序有关(Compile Sources中的顺序),与是谁的分类,同一个类有几个分类无关
7,同一镜像中主工程的+load方法执行在前,静态库的+load方法执行在后。有多个静态库时,静态库之间的执行顺序与编译顺序有关(Link Binary With Libraries中的顺序)
8,不同镜像中,动态库的+load方法执行在前,主工程的+load执行在后,多个动态库的+load方法的执行顺序编译顺序有关(Link Binary With Libraries中的顺序)。