“性能优化”一直是开发者、程序员等所努力追求的,比如说手机系统,只有拥有强悍的性能才能够在众多系统当中脱颖而出;手机APP也不例外,流畅的操作,强悍的性能才能够留住用户。
其实,对于程序员来说,“性能优化”是我们都很熟悉的词,也是我们需要不断努⼒以及持续进⾏的事情;性能优化是⼀个很⼤的课题,因为细分来说的话,有很多种优化⽅向,但是切忌在实际开发过程中盲目的为了优化⽽优化,这样反而会适得其反;那么接下来,我以iOS App的启动性能优化方面来给大家介绍下相关事项!
一、启动流程
iOS App 的启动我们都知道分为 为pre-main 和 main() 两个阶段,并且在这两个阶段中,系统会进 ⾏⼀系列的加载操作,过程如下:
1、 pre-main阶段
①加载应⽤的可执⾏⽂件
②加载dyld动态连接器
③dyld递归加载应⽤所有依赖的动态链接库dylib
2、 main()阶段
①dyld调⽤ main()
②调⽤ UIApplicationMain()
③调⽤ applicationWillFinishLaunching
④调⽤
3、阶段优化项
1、 pre-main阶段
针对 pre-main 阶段做优化时,我们需要先详细了解其加载过程,这个可以在2016年WWDC 的 Optimizing App Startup Time 中详细了解到, 相关材料
1.1 Load dylibs
这⼀阶段dyld会分析应⽤依赖的 dylib (xcode7以后.dylib已改为名.tbd),找到其 mach-o ⽂件,打开和读取这些⽂件并验证其有效性,接着会找到代码签名注册到内核,最后对 dylib 的每⼀个 segment 调 ⽤ mmap() 。不过这⾥的 dylib ⼤部分都是系统库,不需要我们去做额外的优化。
优化结论:
1、尽量不使⽤内嵌的dylib,从⽽避免增加 `Load dylibs`开销
2、合并已有的dylib和使⽤静态库(static archives),减少dylib的使⽤个数
3、懒加载dylib,但是要注意dlopen()可能造成⼀些问题,且实际上懒加载做的⼯作会更多。
1.2 Rebase/Bind
在dylib的加载过程中,系统为了安全考虑,引⼊了ASLR (Address Space Layout Randomization)技术和 代码签名。由于ASLR的存在,镜像(Image,包括可执⾏⽂件、 dylib和bundle)会在随机的地址上加载,和 之前指针指向的地址(preferred_address)会有⼀个偏差(slide), dyld需要修正这个偏差,来指向正确的 地址。 Rebase在前, Bind在后, Rebase做的是将镜像读⼊内存,修正镜像内部的指针,性能消耗主要在 IO。 Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。
优化结论:
在此过程中,我们需要注意的是尽量减少指针数量,⽐如:
1. 减少ObjC类(class)、⽅法(selector)、分类(category)的数量
2. 减少C++虚函数的的数量(创建虚函数表有开销)
3. 使⽤ Swift struct (内部做了优化,符号数量更少)
1.3 Objc setup
⼤部分ObjC初始化⼯作已经在Rebase/Bind阶段做完了,这⼀步dyld会注册所有声明过的ObjC类,将分类插 ⼊到类的⽅法列表⾥,再检查每个selector的唯⼀性。
在这⼀步倒没什么优化可做的, Rebase/Bind阶段优化好了,这⼀步的耗时也会减少。
1.4 Initializers
在这⼀阶段, dyld开始运⾏程序的初始化函数,调⽤每个Objc类和分类的+load⽅法,调⽤C/C++ 中的构造器 函数(⽤attribute((constructor))修饰的函数),和创建⾮基本类型的C++静态全局变量。 Initializers阶段执⾏ 完后, dyld开始调⽤main()函数。
优化结论:
1. 少在类的+load⽅法⾥做事情,尽量把这些事情推迟到+initiailize
2. 减少构造器函数个数,在构造器函数⾥少做些事情
3. 减少构造器函数个数,在构造器函数⾥少做些事情
2、 main()阶段
在这⼀阶段⾥,主要优化重点放在 SDK初始化、业务⼯具注册、整体
didFinishLaunchingWithOptions ⽅法中,因为我们的⼀些第三⽅ app ⻛格配置、启动引导⻚显示状态逻辑、版本更新逻辑等等基本⽅都
会在这⾥进⾏,如果这部分逻辑没有做好优化梳理,随着业务不断拓展,臃肿的业务逻辑会直接导致启动时 间加⻓。
优化结论:
在满⾜业务需求的前提下,尽量减少 didFinishLaunchingWithOptions ⽅法在主线程中的事件处理逻辑, ⽐如:
1. 根据实际业务状况,梳理各个⼆⽅/三⽅库,找到可以延迟加载的库,做延迟加载处理,⽐如放到⾸⻚控制器 的viewDidAppear⽅法⾥。
2. 梳理业务逻辑,把可以延迟执⾏的逻辑,做延迟执⾏处理。⽐如检查新版本、注册推送通知等逻辑
3. 避免进⾏⼀些复杂/多余的计算逻辑,这类逻辑尽量进⾏异步延迟处理
4. 避免在⾸⻚控制器的viewDidLoad和viewWillAppear做太多容易阻塞主线程的事情,这2个⽅法执⾏完, ⾸⻚控制器才能显示。
那么,在实际项⽬中我们需要对线上项⽬的启动数据进⾏监控,以便及时的定位和优化那些影响 app 启动时⻓的环节,这时我们应该怎样更好的处理呢?
其实,我们可以通过服务器埋点上报的⽅式⾃⾏统计分析,不过这样⼀来会发现我们的统计成本就会⼤⼤增加,⽽且结果分析也会变得不那么灵活。所以这⾥推荐⼀种简单的监控⽅式,那就是友盟的 U-APM 应能性 能监控SDK ,只需要我们进⾏简单的pod集成之后,便可根据我们的实际需要进⾏⼿动或者⾃动监控启动数 据,详情可以参考 U-APM, 并且为了⽅便我们对数据进⾏分析,友盟后台已经根据这些数据帮我们绘制出 了对应的分布图,我们可以⼀⽬了然的得出启动耗时分布、启动类型占⽐等等,如图:
除此之外,我们还可以通过SDK进⾏崩溃分析、 ANR分析、监控告警、卡顿分析、内存分析等等诸多功能;另外,友盟的“云真机”功能也很适合开发者朋友们,提供了海量Android、iOS真机,通过资源集中管理,合理调度分配,为开发者提供发版前测试、发现线上问题后复现等场景使用,助力开发者平衡成本与需求,提升研发效率!令漫长枯燥的开发及测试工作变得简单且快速!