@qidiandasheng
2020-10-22T12:25:13.000000Z
字数 6120
阅读 1564
极客时间
①解析Info.plist
加载相关信息,例如闪屏
沙箱建立、权限检查
②Mach-O加载
如果是胖二进制文件,寻找合适当前CPU架构的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的 +load 函数
执行声明为__attribute__((constructor))的C函数
③程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
动态链接库的加载过程主要由dyld来完成,dyld是苹果的动态链接器。
App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),从里面获得dyld的路径,然后加载动态链接器dyld,dyld是一个专门用来加载动态链接库的库。
dyld去初始化运行环境,开启缓存策略,dyld从可执行文件的依赖开始,递归加载程序相关的所有依赖的动态链接库(其中也包含我们的可执行文件)。
并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime
被初始化。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。
当启动一个应用程序时,系统最后会根据你的行为调用两个函数,fork和execve。fork功能创建一个进程;execve功能加载和运行程序。这里有多个不同的功能,比如execl,execv和exect,每个功能提供了不同传参和环境变量的方法到程序中。在OSX中,每个这些其他的exec路径最终调用了内核路径execve。
在每个动态库的加载过程中,dyld需要做下面工作:
优化:
1.减少非系统库的依赖
2.合并非系统库
由于ASLR(address space layout randomization)
的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。 rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
优化:
1.减少Objc类数量, 减少selector数量
2.减少C++虚函数数量
Objc setup
主要是在objc_init
完成的。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
//读取Runtime相关的环境变量
environ_init();
tls_init();
static_init();
lock_init();
//初始化libobjc异常处理系统
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
这就是dyld在加载runtime的时候,runtime的动态库(libobjc)初始化时向dyld绑定了3个回调函数,分别是map_images
,load_images
和unmap_image
。
dyld在binding操作结束之后,会发出dyld_image_state_bound通知,然后与之绑定的回调函数map_images就会被调用
它主要做以下几件事来完成Objc Setup:
1.读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
2.注册 Objc 类(Class),类别(category)
3.确保 selector 的唯一性
4.读取 protocol 以及 category 的信息
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
//开启Runtime锁
mutex_locker_t lock(runtimeLock);
//将具体任务交由map_images_nolock执行
return map_images_nolock(count, paths, mhdrs);
}
load_images
load_images函数作用就是调用Objc的load方法,它监听dyld_image_state_dependents_initialize通知
unmap_image
unmap_image可以理解为map_2_images的逆向操作
优化:
参照rebase/binding time,尽量减少类的数量,可以达到减少这一部分的时间
之前是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和栈中写入内容。 工作主要有:
1、 Objc的+load()函数
2、 C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
3、 非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
由于上一步runtime的动态库(libobjc)向dyld绑定了load_images
函数。
所以之后dyld开始将程序二进制文件初始化(dyld_image_state_dependents_initialize
)后交由ImageLoader
读取image
,其中包含了我们的类、方法等各种符号。
这时候会dyld会发出dyld_image_state_dependents_initialize
通知 (依赖的所有 image 已走完 initializers
流程时)。
runtime接受到通知后调用mapimages
做解析和处理,接下来loadimages
中调用 callloadmethods
方法,遍历所有加载进来的Class,按继承层级依次调用Class的+load
方法和其 Category的+load
方法。
优化:
1.使用+initialize来代替+load
2.不要使用atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_once()、pthread_once() 或 std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。也尽量不要用到C++的静态对象。
启动速度优化来说,可以做的事情包括:
删除无用代码(未被调用的静态变量、类和方法),减少加载启动后不会去使用的类或者方法。
rebase和binding的时间占据了最多的时间消耗。也就是说dyld修复指针指向花费了300多毫秒。从上文我们知道,dyld修复的指针都位于__DATA段,所以我们需要做的很简单,减少项目中指针的使用。如何减少? 如果你的项目使用纯Objc开发,那就要适当减少类的个数,根据WWDC2016(Session 406),Apple工程师的说法,项目中包含100,1000个类没什么太大的overhead,但要是5000到20000以上,则加载时间会多700到800毫秒。如果工程使用Swift居多,甚至是纯Swift,那么Apple建议能使用struct则使用struct,因为struct是值类型,不会引入指针(使用偏移量)。
+load() 方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。不要小看这 4 毫秒,积少成多,执行 +load() 方法对启动速度的影响会越来越大。
main() 函数执行后:
具体可以参考iOS基于二进制重排的启动优化
简单解释看下面两张图就可以了:
假设我们只有两个page:page1/page2,其中绿色的method1和method3启动时候需要调用,为了执行对应的代码,系统必须进行两个Page Fault。
但如果我们把method1和method3排布到一起,那么只需要一个Page Fault即可,这就是二进制文件重排的核心原理。
如何找到启动时调用的函数呢?
使用Clang插件SanitizerCoverage
。
简单来说 SanitizerCoverage
是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_
为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++
等语言,Method/Function/Block
全支持。
开启 SanitizerCoverage
的方法是:在 build settings
里的 “Other C Flags”
中添加 -fsanitize-coverage=func,trace-pc-guard
。如果含有 Swift 代码的话,还需要在 “Other Swift Flags”
中加入 -sanitize-coverage=func
和 -sanitize=undefined
。所有链接到 App 中的二进制都需要开启 SanitizerCoverage
,这样才能完全覆盖到所有调用。
以下两个函数就是SanitizerCoverage
帮我们自动插入到函数了,通过__sanitizer_cov_trace_pc_guard
我们能得到下一个函数调用的首地址,也就是其中最重要的PC
。
typedef struct {
void *pc;
void *next;
} PCNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (collectFinished || !*guard) {
return;
}
*guard = 0;
void *PC = __builtin_return_address(0);
PCNode *node = malloc(sizeof(PCNode));
*node = (PCNode){PC, NULL};
OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}
可直接使用AppOrderFiles。
注意:
有时候我们会得到如下的错误,找不到__sanitizer_cov_trace_pc_guard_init
和__sanitizer_cov_trace_pc_guard
符号。
因为这两个符号是加在当前的库里面的,当我们开启use_frameworks!
的时候,我们图中的AFNetWorking
就自己打包成了一个动态库,所以找不到那两个符号。
所以我这里是去掉了use_frameworks!
之后这些依赖库就都打进了可执行文件中,就不会报错了。或者让AFNetWorking
也依赖AppOrderFiles
库这样的话也可以(但显然是不可能的)。
使用Time Profiler
,这里主要监控的是启动后的方法耗时。
在 Xcode 中 Edit scheme -> Run -> Auguments -> Environment Variables
点击+添加环境变量 DYLD_PRINT_STATISTICS
设为 1,然后启动app,我们能在控制台看到如下的输出:
Total pre-main time: 3.5 seconds (100.0%)
dylib loading time: 2.7 seconds (77.0%)
rebase/binding time: 575.27 milliseconds (16.3%)
ObjC setup time: 90.65 milliseconds (2.5%)
initializer time: 141.40 milliseconds (4.0%)
slowest intializers :
libSystem.B.dylib : 4.31 milliseconds (0.1%)
AFNetworking : 80.39 milliseconds (2.2%)
1、main()函数之前总共用时3.5s
2、加载动态库使用了2.7s,指针重定位用了575.27ms,ObjC类初始化使用了90.65ms,其他依赖库各种初始化使用了141.40ms
3、在初始化用时的141.40ms中,用时较多的几个初始化是libSystem.B.dylib
、AFNetworking
。
iOS深思篇 | 启动时间的度量和优化
计算 +load 方法的耗时
iOS启动时间优化
iOS基于二进制重排的启动优化
二进制重排
App 二进制文件重排已经被玩坏了
iOS App冷启动优化