高德APP启动耗时剖析与优化实践(iOS篇)

前言

最近高德地图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数量是否合理,可以看到线程在执行、挂起、上下文切换、被打断还是被抢占的情况。虚拟内存使用产生的耗时也能看到,比如分配物理内存,内存解压缩,无缓存时进行缓存的耗时等。甚至是发热情

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值