前言
最近高德地图APP完成了一次启动优化专项,超预期将双端启动的耗时都降低了65%以上,iOS在iPhone7上速度达到了400毫秒以内。就像产品们用后说的,快到不习惯。算一下每天为用户省下的时间,还是蛮有成就感的,本文做个小结。
(文中配图均为多才多艺的技术哥哥手绘)
启动阶段性能多维度分析
要优化,首先要做到的是对启动阶段的各个性能纬度做分析,包括主线程耗时、CPU、内存、I/O、网络。这样才能更加全面的掌握启动阶段的开销,找出不合理的方法调用。
启动越快,更多的方法调用就应该做成按需执行,将启动压力分摊,只留下那些启动后方法都会依赖的方法和库的初始化,比如网络库、Crash库等。而剩下那些需要预加载的功能可以放到启动阶段后再执行。
启动有哪几种类型,有哪些阶段呢?
启动类型分为:
Cold:APP重启后启动,不在内存里也没有进程存在。
Warm:APP最近结束后再启动,有部分在内存但没有进程存在。
Resume:APP没结束,只是暂停,全在内存中,进程也存在。
分析阶段一般都是针对Cold类型进行分析,目的就是要让测试环境稳定。为了稳定测试环境,有时还需要找些稳定的机型,对于iOS来说iPhone7性能中等,稳定性也不错就很适合,Android的Vivo系列也相对稳定,华为和小米系列数据波动就比较大。
除了机型外,控制测试机温度也很重要,一旦温度过高系统还会降频执行,影响测试数据。有时候还会设置飞行模式采用Mock网络请求的方式来减少不稳定的网络影响测试数据。最好是重启后退iCloud账号,放置一段时间再测,更加准确些。
了解启动阶段的目的就是聚焦范围,从用户体验上来确定哪个阶段要快,以便能够让用户可视和响应用户操作的时间更快。
简单来说iOS启动分为加载Mach-O和运行时初始化过程,加载Mach-O会先判断加载的文件是不是Mach-O,通过文件第一个字节,也叫魔数来判断,当是下面四种时可以判定是Mach-O文件:
0xfeedface对应的loader.h里的宏是MH_MAGIC
0xfeedfact宏是MH_MAGIC_64
NXSwapInt(MH_MAGIC)宏MH_GIGAM
NXSwapInt(MH_MAGIC_64)宏MH_GIGAM_64
Mach-O主要分为:
中间对象文件(MH_OBJECT)
可执行二进制(MH_EXECUTE)
VM 共享库文件(MH_FVMLIB)
Crash 产生的Core文件(MH_CORE)
preload(MH_PRELOAD)
动态共享库(MH_DYLIB)
动态链接器(MH_DYLINKER)
静态链接文件(MH_DYLIB_STUB)符号文件和调试信息(MH_DSYM)这几种。
确定是Mach-O后,内核会fork一个进程,execve开始加载。检查Mach-O Header。随后加载dyld和程序到Load Command地址空间。通过 dyld_stub_binder开始执行dyld,dyld会进行rebase、binding、lazy binding、导出符号,也可以通过DYLD_INSERT_LIBRARIES进行hook。
dyld_stub_binder给偏移量到dyld解释特殊字节码Segment中,也就是真实地址,把真实地址写入到la_symbol_ptr里,跳转时通过stub的jump指令跳转到真实地址。dyld加载所有依赖库,将动态库导出的trie结构符号执行符号绑定,也就是non lazybinding,绑定解析其他模块功能和数据引用过程,就是导入符号。
Trie也叫数字树或前缀树,是一种搜索树。查找复杂度O(m),m是字符串的长度。和散列表相比,散列最差复杂度是O(N),一般都是 O(1),用 O(m)时间评估 hash。散列缺点是会分配一大块内存,内容越多所占内存越大。Trie不仅查找快,插入和删除都很快,适合存储预测性文本或自动完成词典。
为了进一步优化所占空间,可以将Trie这种树形的确定性有限自动机压缩成确定性非循环有限状态自动体(DAFSA),其空间小,做法是会压缩相同分支。
对于更大内容,还可以做更进一步的优化,比如使用字母缩减的实现技术,把原来的字符串重新解释为较长的字符串;使用单链式列表,节点设计为由符号、子节点、下一个节点来表示;将字母表数组存储为代表ASCII字母表的256位的位图。
尽管Trie对于性能会做很多优化,但是符号过多依然会增加性能消耗,对于动态库导出的符号不宜太多,尽量保持公共符号少,私有符号集丰富。这样维护起来也方便,版本兼容性也好,还能优化动态加载程序到进程的时间。
然后执行attribute的constructor函数。举个例子:
#include <stdio.h>
__attribute__((constructor))
static void prepare() {
printf("%s\n", "prepare");
}
__attribute__((destructor))
static void end() {
printf("%s\n", "end");
}
void showHeader() {
printf("%s\n", "header");
}
运行结果:
ming@mingdeMacBook-Pro macho_demo % ./main "hi"
prepare
hi
end
运行时初始化过程分为:
加载类扩展。
加载C++静态对象。
调用+load函数。
执行main函数。
Application初始化,到applicationDidFinishLaunchingWithOptions执行完。
初始化帧渲染,到viewDidAppear执行完,用户可见可操作。
也就是说对启动阶段的分析以viewDidAppear为截止。这次优化之前已经对Application初始化之前做过优化,效果并不明显,没有本质的提高,所以这次主要针对Application初始化到viewDidAppear这个阶段各个性能多纬度进行分析。
工具的选择其实目前看来是很多的,Apple提供的System Trace会提供全面系统的行为,可以显示底层系统线程和内存调度情况,分析锁、线程、内存、系统调用等问题。总的来说,通过System Trace能清楚知道每时每刻APP对系统资源的使用情况。
System Trace能查看线程的状态,可以了解高优线程使用相对于CPU数量是否合理,可以看到线程在执行、挂起、上下文切换、被打断还是被抢占的情况。虚拟内存使用产生的耗时也能看到,比如分配物理内存,内存解压缩,无缓存时进行缓存的耗时等。甚至是发热情