Objective-C中的load方法执行的来龙去脉
引子
我们都知道: Objective-C中类Class
的+load
方法会在类第一次加载到内存时, 并且APP的整个生命周期只会执行一次. 但是知其然最好知其所以然, 今天来分析一下+load
方法执行的来龙去脉.
准备工作, 本文涉及到的Apple 开源源码如下:
- dyld-635.2
- objc4-750
上一篇文章<<iOS APP启动前后发生了什么?>>
开篇, 有如下的调用栈:
0 +[AppDelegate load]
1 call_load_methods
2 load_images
// 这里是一个断层
3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start
我们能看到实际最后会调用+[Class load]
类方法, 我们发现从调用栈那里有一个断层, 3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
-> +[AppDelegate load]
的过程, 明显不是在dyld, ImageLoader
库中, 而是在runtime
中的方法, 重要的原因就是dyld::notifySingle
是对外发送两个一个通知, 而loadImage
是针对通知注册的handler.
而前文在讲到, 当运行到后面会在runtime
初始化时调用_objc_init
, 这个方法最后会调用dyld::_dyld_objc_notify_register
方法注册三个hanlder, 其中有一个方法就是runtime
的load_images
, 因此dyld::notifySingle
实际是发出了某个通知, 触发load_images
.
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 这里是在dyld中加入一个监听器, 一旦dyld监听到有新的镜像加载到runtime时, 就调用 load_images 方法, 并传入最新镜像的信息类别 infoList
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
dyld_image
的state的监听与通知
为了证明我们前面的内容, 我们需要在源码中去找到线索.
我们打开dyld
的源码dyld_priv.h
, 中的关于dyld_image_states
的定义:
// DEPRECATED
// dyld_image 整个生命周期中会经历的状态
enum dyld_image_states {
dyld_image_state_mapped = 10, // No batch notification for this - 是否已经映射
dyld_image_state_dependents_mapped = 20, // Only batch notification for this - 依赖是否映射
dyld_image_state_rebased = 30, // rebase
dyld_image_state_bound = 40, // 已经bound
dyld_image_state_dependents_initialized = 45, // Only single notification for this
dyld_image_state_initialized = 50, // -- 已经初始化!!!!! 重要的状态
dyld_image_state_terminated = 60 // Only single notification for this
};
而且dyld_image.state
的状态切换都是在dyld::_main(...)
方法中进行的, 我们将该方法简写如下:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
...
// dyld::instantiateFromLoadedImage -> ImageLoaderMachOClassic::instantiateMainExecutable(create image for main executable) -> setMapped -> dyld_state = dyld_image_state_mapped-> 发出notification
// 初始化完成以后, sMainExecutable被push到 sAllImages, 并且将它的关键信息插入到MappedRanges链表(这个链表中的内容已经mapped完毕)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); // dyld_image_state_mapped 并通知
...
/*
注意, 这里执行 link(...) 时, linkingMainExecutable = true!!!
1. recursiveLoadLibraries -> dyld_image_state_dependents_mapped 并通知
2. recursiveRebase -> dyld_image_state_rebased 并通知
(不会执行: 3. recursiveBindWithAccounting -> recursiveBind -> dyld_image_state_bound)
(不会执行: 4. weakBind -> 通知 dyld_image_state_bound)
*/
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
...
gLinkContext.linkingMainExecutable = false;
// 从这里开始 linkingMainExecutable = false, 也就是 MainImageLoader完成link操作!!! 切换 MainImageLoader.fstate = dyld_image_state_bound, 并发送 dyld_image_state_bound_notify 通知
// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);
...
/*
这里开始执行各个dyld_image的initializers方法, 当初始化完成以后, 就MainImageLoader.fstate = dyld_image_state_initialized, 并发送 dyld_image_state_initialized_notify 通知
1. initializeMainExecutable
2. sMainExecutable->runInitializers
3. sMainExecutable->processInitializers
4. context.notifyBatch(dyld_image_state_initialized, false);
*/
initializeMainExecutable();
...
return Main函数的入口
}
在梳理之前, 我们需要有一个简单的概念, 关于
link(..)
过程中的rebase
和bind
.当
mach-o
或dyld_image
二进制文件被加载到内存中以后, 由于地址空间加载随机化(ASLR
, Address Space Layout Randomization)的缘故, 二进制文件最终的加载地址与预期地址之间会存在偏移, 所以需要进行rebase
操作, 对那些指向文件内部符号的指针进行修正, 在link
函数中该项操作由recursiveRebase
函数执行.rebase
完成之后, 就会进行bind
操作, 修正那些指向其他二进制文件所包含的符号的指针, 由recursiveBind
函数执行。 当rebase
以及bind
结束时,link
函数就完成了它的使命.
我们能看到在dyld::_main(...)
函数中dyld_image
会随着过程切换自己的state状态, 并且对外发出相关状态的通知.
同时我们在源码中有如下代码:
// DEPRECATED -- 当 dyld_image 的state状态变化以后, 调用的回调函数callback格式如下
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);
// 注册的方法的 函数指针当 mapped/ init/ unmapped 状态时, 分别调用的callback格式如下
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
//
// Note: only for use by objc runtime
// 这个方法只有在 runtime 的 _objc_init 方法中调用
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.
// 1. During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.
// 2. During any later dlopen() call, dyld will also call the "mapped" function. (每一次调用dlopen(), 都会调用'mapped' function)
// 3. Dyld will call the "init" function when dyld would be called initializers in that image. This is when objc calls any +load methods in that image. - 当 image状态变化成 initializer 时候, 会调用`init` callback, 这个callback在实际代码中是调用的 objc4.750 的 `loadImages` 方法, 这个方法内部会调用这个`image`中的每个`Class`的`+load`方法
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped) {
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
注意上面这个方法_dyld_objc_notify_register
是在dyld::_main
方法中的mainImageLoader
的link(...)
方法结束以后, 由于依赖的库中有libSystem
和libCloure
从而加载runtime
的_objc_init(...)
方法结束时候才调用, 因此在执行_dyld_objc_notify_register
以后, 相当于runtime
就会监听所有在runtime
之后被加载的dyld_image
, 根据他们的的状态, 去调用注册的3个回调函数, 这里我们重点关注load_images
方法.
load_images
在runtime
的_objc_init(...)
被注册以后,一旦dyld
中有新的image
状态成为init
(也就是dyld_image_state_initialized
, 此时表示该image
已经完成link
), 就会调用load_images
方法, 对这个完全初始化成功的image
中的内容做一些处理.
objc中的load_images
load_images
方法的源码如下:
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
有新的镜像image被加载到 runtime 时,调用 load_images 方法,并传入最新镜像image的信息列表 infoList:
images 是镜像的意思: 这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜, 从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。这里面有很多的动态链接库,还有一些苹果为我们提供的框架,比如 Foundation、 CoreServices 等等,都是在这个 load_images 中加载进来的,而这些 imageFilePath 都是对应的二进制文件的地址。
+load 的应用:
+load 可以说我们在日常开发中可以接触到的调用时间最靠前的方法,在主函数运行之前,load 方法就会调用。
由于它的调用不是惰性的,且其只会在程序调用期间调用一次,最最重要的是,如果在类与分类中都实现了 load 方法,它们都会被调用,不像其它的在分类中实现的方法会被覆盖,这就使 load 方法成为了方法调剂的绝佳时机。
但是由于 load 方法的运行时间过早,所以这里可能不是一个理想的环境,因为某些类可能需要在在其它类之前加载,但是这是我们无法保证的。不过在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的。
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);
void load_images(const char *path __unused, const struct mach_header *mh) {
// Return without taking locks if there are no +load methods here.
// 如果 没有 +load 方法, 直接返回
if (!hasLoadMethods((const headerType *)mh)) return;
// 此时表示 mh中有 +load 方法
// 上锁, 不能同时多个线程执行 loadMethod, 锁1
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
// runtimeLock 两个锁, 锁2
// 这里 write-locks 需要两个锁
mutex_locker_t lock2(runtimeLock);
//调用 prepare_load_methods 对 load 方法的调用进行准备, 主要工作就是将Class的所有方法都加载到一个叫loadable_classes的数组中
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
// 在将镜像加载到运行时, 对 load 方法的准备就绪之后,执行 call_load_methods,开始调用 load 方法
call_load_methods();
}
从load_images
中的源码游走以后, 我们主要看到两个重要的步骤 -- 准备load
和调用load
:
- 当有新的镜像被dyld加载, runtime就会去该镜像中对所有的
class/category
进行准备操作. - 准备操作是
prepare_load_methods
- 调用操作是
call_load_methods
objc中如何准备 -- prepare_load_methods
解析
/**
准备load methods
*/
void prepare_load_methods(const headerType *mhdr) {
size_t count, i;
// 调用 load_method 时, 必须是 runtimeLock已经上锁
runtimeLock.assertLocked();
//处理mach-o中的class:
// 通过 _getObjc2NonlazyClassList 获取二进制文件中所有的类的列表之后,会通过 remapClass 获取类对应的指针,然后调用 schedule_class_load 递归地安排当前类的父类和当前类加入到一个 loadable_list中
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 内部处理以后调用 add_class_to_loadable_list方法
schedule_class_load(remapClass(classlist[i]));
}
//处理mach-o中的categorys:
// 通过 _getObjc2NonlazyCategoryList 方法获取二进制文件中所有的category的列表, 然后递归处理每个单独的category.
// 单独处理Category的过程如下: 首先获取每个category的Class, 然后先调用一个关键的方法`realizeClass`, 这个方法能够保证每个类已经被runtime进行了`realize`过, 这个过程很重要(后面有专门的文章来解释整个realize的过程), 然后调用`add_category_to_loadable_list`将category方法加入loadable_list
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 做的工作就是Class第一次 initiail
realizeClass(cls);
assert(cls->ISA()->isRealized());
// 获取 category中的+load方法, 然后按照一定顺序将+load方法加入到一个loadabel_list中
add_category_to_loadable_list(cat);
}
}
通过源码注释, 我们可以看出准备过程会处理两块内容, 分别是镜像中的class
以及category
.
如果处理镜像中的class
, 过程是:
-
_getObjc2NonlazyClassList
获取二进制文件中所有的类, 放到一个链表中, 然后递归处理每个class
- 遍历这个链表, 取出每个节点, 先调用
remapClass
, 然后调用schedule_class_load
-
schedule_class_load
主要是将入参的class
的继承链的每个+load
方法都加入到loadable_classes
链表中. 注意这里的添加+load
方法到链表的顺序是, 先父类, 然后自己.
如果处理镜像中的category
, 过程有点不一样:
-
_getObjc2NonlazyCategoryList
方法获取二进制中所有的category, 放到一个链表中, 然后递归处理每个category
- 单独
category
的过程是: 首先获取每个category
对应的class
, 先对class
进行remapClass
,调用一个关键的方法realizeClass
(我们可以认为这个方法是Class类对象在内存中的初始化创建方法),最后调用add_category_to_loadable_list
方法 -
add_category_to_loadable_list
是将category
按照一定顺序将+load
方法加入到一个叫做loadable_categories
链表中.
realizeClass
方法我们后面专门分析, 这里我们只简单了解一下. 我们直到Class
在编译期间, 有很多方法是我们自己在代码里面定义的, 这些方法在编译器编译期间就搞定了, 当它加载到内存时候, 它的方法列表里面都是编译期间确定的方法, 我们称为只读方法, 但是还有一些方法例如在category
中的方法, 也是与Class
有关的, 但是并没有与这个Class
关联, 通过realizeClass
来调整类在内存中的结构, 例如将category
中的方法都关联到class
上去, 添加到class
的方法列表中, 当然, 还有一些其他的作用, 后面再讲.
objc中正式调用每个类的+load
方法 -- 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(); // 这里会调用 load 方法
}
// 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;
}
static void call_class_loads(void) {
int i;
// Detach current loadable list.
// 用一个便利结构体, 内部持有Class对应方法的+load IMP
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.
// 遍历所有的 loadable_classes 中的每个 loadable Class, 从中按照顺序取出+load方法, 按照 loadable_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 方法会执行!!!
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
简单来说, 就是将loadable_classes
中之前存储的class
的+load
调用, 然后清理, 最后调用loadable_categories
缓存的category
相关的+load
方法.
小总结
-
+load
方法是如何被调用的?
runtime
在初始化时, 会注册一个回调, 去监听镜像加载, 每次有新的镜像加载时, 就会调用注册的load_images
回调, 这个方法会将镜像中所有类和分类的+load
方法按照一定顺序, 放到loadable_classes
和loadable_categories
两个链表中, 然后遍历执行两个链表中的每个节点的+load
方法.
-
+load
方法的调用顺序如何?- 父类的
+load
会先调用, 然后才调用子类+load
- 类的
+load
会先调用, 然后调用分类的+load
- 总得来说, 会先调用super类的
+load
方法, 然后调用自身的+load
方法, 最后调用分类重写的+load
方法.
- 父类的
并且结合前面文章我们知道: +load
方法会先于app的启动方法main
执行, 并且它在全局只会调用一次等特性, +load
方法是让我们实现的method swizzling
最佳位置!!!!
就算分类重写了
+load
方法, 通过上面分析, 仍然会按照父类, 本类, 分类的顺序执行+load
方法. 需要注意这点比较特殊, 与其他方法的执行不一样!!!
参考
iOS程序启动->dyld加载->runtime初始化(初识)
你真的了解load方法么?
http://www.cocoachina.com/ios/20170716/19876.html