iOS启动流程
main 函数是 iOS 程序的入口,我们写的代码都是在 main 函数之后执行的,main 函数之前到底发生了什么?用户点击程序图标之后,App 是怎样被启动的?这期间系统做了哪些事情、经历了哪些步骤才一步步地调用到程序 main 函数的?
系统为程序启动做好准备
系统将控制权交给 Dyld,Dyld 会负责后续的工作
Dyld 加载程序所需的动态库
Dyld 对程序进行 rebase 以及 bind 操作
Objc SetUp
运行初始化函数
执行程序的 main 函数
Dyld
Dyld 是 iOS 系统的动态链接器,Dyld 的启动代码源于 dyldStartup.s 文件,在一大串的汇编代码中有个名为 __dyld_start 的方法,它会去调用 dyldbootstrap::start() 方法,然后进一步调用 dyld::_main() 方法,里面包含 App 的整个启动流程,该函数最终返回应用程序 main 函数的地址,最后 Dyld 会去调用它。dyld::_main() 函数的源码很长,所以这里只保留关键信息,并用伪代码进行简化从而得到整体流程:
uintptr_t _main(···/省略参数/···) {
// 1. 设置运行环境
......
// 2. instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
......
//3. link main executable
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
......
//4. run all initializers
initializeMainExecutable();
......
//5. find entry point for main executable
result = (uintptr_t)sMainExecutable->getThreadPC();
......
return result;
}
加载可执行文件
二进制文件常被称为 image,包括可执行文件、动态库等,ImageLoader 的作用就是将二进制文件加载进内存。dyld::_main() 方法在设置好运行环境后,会调用 instantiateFromLoadedImage 函数将可执行文件加载进内存中,加载过程分为三步:
1.合法性检查。主要是检查可执行文件是否合法,是否能在当前的 CPU 架构下运行。
2.选择 ImageLoader 加载可执行文件。系统会去判断可执行文件的类型,选择相应的 ImageLoader 将其加载进内存空间中。
3.注册 image 信息。可执行文件加载完成后,系统会调用 addImage 函数将其管理起来,并更新内存分布信息。
以上三步完成后,Dyld 会调用 link 函数开始之后的处理流程。
Load Dylibs
void ImageLoader::link(···/省略参数/···) {
//dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
uint64_t t0 = mach_absolute_time();
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
// we only do the loading step for preflights
if ( preflightOnly )
return;
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());
uint64_t t2 = mach_absolute_time();
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased, false);
uint64_t t3 = mach_absolute_time();
this->recursiveBind(context, forceLazysBound, neverUnload);
uint64_t t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
this->weakBind(context);
uint64_t t5 = mach_absolute_time();
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();
std::vector<DOFInfo> dofs;
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
uint64_t t7 = mach_absolute_time();
// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
this->recursiveApplyInterposing(context);
}
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;
// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
首先调用 recursiveLoadLibraries,递归加载程序所需的动态链接库。使用 otool -L命令查看 二进制文件路径 可以列出程序的动态链接库
$ otool -L xxxxx
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1349.55.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.50.2)
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1349.56.0)
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 3600.7.47)
libobjc.A.dylib 包含 runtime,而 libSystem.B.dylib 则包含像 libdispatch、libsystem_c 等系统级别的库,二者都是被默认添加到程序中的。动态链接库的加载也是借助 ImageLoader 完成的,但是由于动态链接库本身还可能依赖其他动态链接库,所以整个加载过程是递归进行的。当程序的动态链接库加载完毕后,link 函数进入下一流程。
Rebase && Bind
因为地址空间加载随机化(ASLR,Address Space Layout Randomization)的缘故,二进制文件最终的加载地址与预期地址之间会存在偏移,所以需要进行 rebase 操作,对那些指向文件内部符号的指针进行修正,在 link 函数中该项操作由 recursiveRebase 函数执行。rebase 完成之后,就会进行 bind 操作,修正那些指向其他二进制文件所包含的符号的指针,由 recursiveBind 函数执行。
当 rebase 以及 bind 结束时,link 函数就完成了它的使命,iOS 应用的启动流程也进入到下一阶段,即 Objc SetUp
Objc SetUp
void _objc_init(void) {
......
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
Dyld 在 bind 操作结束之后,会发出 dyld_image_state_bound 通知,然后与之绑定的回调函数 map_2_images 就会被调用,它主要做以下几件事来完成 Objc Setup:
读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
注册 Objc 类
确保 selector 的唯一性
读取 protocol 以及 category 的信息
除了 map_2_images, _objc_init 还注册了 load_images 函数,它的作用就是调用 Objc 的 + load 方法,它监听 dyld_image_state_dependents_initialized 通知
Initializers
Objc SetUp 结束后,Dyld 便开始运行程序的初始化函数,该任务由 initializeMainExecutable 函数执行。整个初始化过程是一个递归的过程,顺序是先将依赖的动态库初始化,然后在对自己初始化。初始化需要做的事情包括:
调用 Objc 类的 + load 函数
调用 C++ 中带有 constructor 标记的函数
非基本类型的 C++ 静态全局变量的创建
main
当初始化结束之后,可执行文件才处于可用状态,之后 Dyld 就会去调用可执行文件的 main 函数,开始程序的运行
Xcode 测量 pre-main 时间
对于如何测试启动时间,Xcode 提供了一个方法,只需要在 Edit scheme -> Run -> Arguments 中将环境变量 DYLD_PRINT_STATISTICS 设为 1,就可以看到 main 之前各个阶段的时间消耗
还有一个方法获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1 就可以
Xcode For Static Initializers
Apple 在 https://developer.apple.com/videos/play/wwdc2017/413/ 中公布了一个新的追踪 Static Initializers 时间消耗的方案, Instruments 增加了一个叫做 Static Initializer Tracing 的工具,可以方便排查每个 Static Initializer 的时间消耗。
启动时间优化
目前很多项目使用 use_frameworks 的 pod 动态库,系统的动态库有共享缓存等优化方案,但是我们的动态库变多了的话会非常耗时,所以合并动态库是一个有效且可行的方案
把启动任务细分,不需要及时初始化,不需要在主线程初始化的,都选择异步延时加载
监控好 load 和 Static Initializers 的时间消耗,一不小心就容易出现几百毫秒的时间消耗