前言:本文简单描述APP启动过程和监控,一些深入原理性的东西可能需要绕路了,站在大神的肩膀上,简单总结跟APP启动性能有关,如果差错请不吝赐教
冷启动
相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。
App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
用户感知到的启动慢,其实都发生在主线程上。而主线程慢的原因有很多,比如在主线程上执行了大文件读写操作、在渲染周期中执行了大量计算等。
热启动
当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程。这种持续存活的情况下启动App,称为热启动。
查看APP启动耗时
根据APP启动时间,继续了解APP启动时候都做了哪些
Xcode:(快捷键:command + shift + <
)
Project
→ Scheme
→ Edit Scheme
→ Run
→ Environment Variables
添加 DYLD_PRINT_STATISTICS
环境变量,value为1
APP 启动时间:
Total pre-main time: 802.16 milliseconds (100.0%)
dylib loading time: 294.37 milliseconds (36.6%)
rebase/binding time: 377.42 milliseconds (47.0%)
ObjC setup time: 86.68 milliseconds (10.8%)
initializer time: 43.51 milliseconds (5.4%)
slowest intializers :
libSystem.B.dylib : 4.20 milliseconds (0.5%)
libMainThreadChecker.dylib : 21.22 milliseconds (2.6%)
时间消耗解读
- main()函数之前总共使用了802.16ms
- 加载动态库占用36.6%
- 指针重定位占用47.6%
- ObjC类初始化占用10.8%
- 各种初始化使用了5.4%。
- initializer time中最耗时的是libSystem.B.dylib、libBacktraceRecording.dylib。
换言之:
App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接器dyld,dyld是一个专门用来加载动态链接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。
APP启动阶段
启动时间:用户点击APP → APP首页面加载完成
- 阶段1:main() 函数执行前
- 阶段2:main() 函数执行后
- 阶段3:首屏渲染完成后
main()函数执行前
在 main() 函数执行前,系统主要会做下面几件事情:
- 【解析Info.plist】:加载信息,例如闪屏;沙盒建立、权限检查
- 【Mach-O加载】:加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法);加载可执行文件(App 的.o 文件的集合)
- 【加载动态链接库】:进行 rebase 指针调整和 bind 符号绑定;定位内部、外部指针引用,例如字符串、函数等
- 【Objc 运行时的初始处理】:包括 Objc 相关类的注册、category 注册、selector 唯一性检查等
- 【初始化】:执行 +load() 方法,执行声明为attribute((constructor))的C函数,C++静态对象加载
程序执行
- 调用
main()
- 调用
UIApplicationMain()
- 调用
applicationWillFinishLaunching
可优化的功能点
- 【减少动态库加载】:每个库本身都有依赖关系,使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。最多可以支持 6 个非系统动态库合并为一个。
- 【减少+load方法】:方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。
- 【减少使用】:减少写attribute((constructor))的C函数,控制 C++ 全局变量的数量;
main()函数之后
从main()
函数开始至 appDelegate
的
didFinishLaunchingWithOptions
结束,称为main()函数之后的部分。
主要执行内容
- 首屏初始化所需配置文件的读写操作;
- 首屏列表大数据的读取;
- 首屏渲染的大量计算等。
main()函数之后耗时的影响因素
- 执行
main()
函数的耗时 - 执行
applicationWillFinishLaunching
的耗时 -
rootViewController
及其childViewController
的加载、view
及其subviews
的加载
首屏渲染完成之后
[首屏渲染完成之后]指的是非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。
该阶段指的就是截止到 didFinishLaunchingWithOptions
方法作用域内执行首屏渲染之后的所有方法执行完成。从渲染完成时开始,到 didFinishLaunchingWithOptions
方法作用域结束时结束。
优化思路一:功能启动优化
main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。
根据刚需分置阶段进行
根据启动流程把刚需功能放置在启动阶段,其他业务功能放在合适的阶段
- 首屏渲染必要的初始化功能
- App 启动必要的初始化功能
- 只需要在对应功能开始使用时才需要初始化的功能
- 例如:主视图第一时间加载,里面的数据和界面延后加载
优化思路二:方法启动优化
检查首屏渲染完成前主线程上的耗时方法,将非刚需的耗时方法滞后或异步执行。耗时较长的方法主要发生在计算大量数据的情况下,例如加载、编辑、存储图片和文件等资源。
+load() 方法,一个耗时 4 毫秒,100 个就是 400 毫秒,不可小视
优化三:移除不必要的动态库
移除项目中非必要的动态库
优化四:移除不必要用到的类
代码工程的维护非常重要
优化五:合并功能相似的类和扩展(Category)
由于Category的实现原理,和ObjC的动态绑定有很强的关系,实际上类的扩展是比较占用启动时间的。尽量可能合并一些扩展,并不是让你不使用扩展
优化六:压缩资源图片
图片小了,IO操作量小了,启动就快了。推荐 TinyPNG
优化七:优化applicationWillFinishLaunching
需要在applicationWillFinishLaunching
里处理的业务较多时,可以管理起这些任务
将不需要马上在applicationWillFinishLaunching执行的代码延后执行
优化八:优化rootViewController
rootViewController的加载,适当将某一级的childViewController或subviews延后加载
如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController
优化九:小优化
- 不使用xib,直接视用代码加载首页视图
- NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
- 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
- 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求
Debug 打印代码块
//MARK: - DEBUG print
func printLog<T>(msg: T,
file: String = #file,
method: String = #function,
line: Int = #line){
if !DEBUG_ALPHA{//线上环境不print
return
}
print("\((file as NSString).lastPathComponent) \(method),[\(line)]: \(msg)")
}
APP监控方法一:计算主线程方法耗时
定时抓取主线程上的方法调用对战,计算在一段时间内各个方法的耗时。Xcode 工具套件里自带的 Time Profiler,开发类似工具成本不高,能够快速开发后集成到你的 App 中,以便在真实环境中进行检查。
对于定时时间间隔的控制
- 间隔长:会漏掉某些方法,从而导致检查出来的耗时不精确
- 间隔短:抓取堆栈这个方法本身调用过多会影响整体耗时,导致结果不准确
- 定时抓取主线程调用栈的方式精准度不够高,做参考足以
- 大神得出最合适时间为 0.01 秒,虽然导致许多运行速度快的方法监控误差,对整体耗时影响小
APP监控方法二:objc_msgSend 方法 hook 所有方法耗时
Hook:在原方法开始执行时换成执行其他指定的方法,或者在原有方法执行前后执行你指定的方法,来达到掌握和改变指定方法的目的。
- 优点:非常精确
- 缺点:只能对Objective-C方法,对c方法和block需要借助第三方框架处理,编写维护成本高
Objective-C 里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由 selector、函数指针和 metadata 组成的。
objc_msgSend
就是在运行时根据对象和方法的 selector
去找到对应的函数指针,然后执行。objc_msgSend
是 Objective-C
里方法执行的必经之路,能够控制所有的 Objective-C
的方法,可自行阅读objc_msgSend
源码获得更深层次的理解。
objc_msgSend
用汇编语言写的:
-
objc_msgSend
的调用频次最高,在它上面进行的性能优化能够提升整个 App 生命周期的性能。汇编语言在性能优化上属于原子级优化,能够把优化做到极致。 - 其他语言难以实现未知参数跳转到任意函数指针的功能。
objc_msgSend
方法执行逻辑
先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现
Tips
Swift AppDelegate 没有main函数入口了
错,swift有main函数,被精简成了一个@NSApplicationMain
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
检查方法耗时的工具
推荐大神神作 SMCallTrace
友情提示:需要在SMCallTrace.m中打开第54行的注释。
+load为什么会增加4毫秒,Swift呢
aop 的耗时,swift没有
iOS中用llvm的IR中插桩来统计函数耗时这个方法也可行
有待学习,嘤嘤嘤
objc的hook是用method swizzle来实现,对于swift
使用Time Profiler 或者 使用Clang打桩统计耗时
oc的代码在编译时会转成c++,再转成c,那swift如何转换?
swift 和 c 编译方式类似。Swift 会先编成 SIL( Swift Intermediate Language)然后再编成机器码。
未完待续…
【干货推荐】
iOS启动时间优化
iOS App 启动性能优化
今日头条iOS客户端启动速度优化
优化 App 的启动时间
《How we cut our iOS app’s launch time in half (with this one cool trick)》
汇编相关
https://blog.nelhage.com/2010/10/amd64-and-va_arg/
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
博客、课程、开源 推荐 戴神
小结:一枚突然顿悟的小白兔,学无止境