阅读 73

聊聊iOS的启动优化怎么做?

首先我们需要知道iOS在启动会发生什么?

image.png

启动优化时间段

在苹果官方,将app的启动时间分为两个阶段

T1: pre-main 阶段,即main()函数之前,操作系统加载app可执行文件到内存中,然后执行一系列的加载&链接工作,最后执行到App的main() 函数

即我们常说的加载Mach-O文件的过程

T2: main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕前这段时间,主要是构建第一个界面,并完成渲染。

而,从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2。

image.png

接下来简单聊聊分别针对这两个时间段我们能做哪些优化

pre-main 阶段优化

通过上图,我们可以看到,pre-main阶段,app主要做的是dyld加载操作, 其次代码区主要在做init和+load函数的事情
首先来看下加载Mach-O.
我们可以通过xcode打印时间来查看,哪些时间是占用比较多的

检测方法
获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:

// example 
// DYLD_PRINT_STATISTICS
Total pre-main time: 383.50 milliseconds (100.0%)
         dylib loading time: 254.02 milliseconds (66.2%)
        rebase/binding time:  20.88 milliseconds (5.4%)
            ObjC setup time:  29.33 milliseconds (7.6%)
           initializer time:  79.15 milliseconds (20.6%)
           slowest intializers :
             libSystem.B.dylib :   8.06 milliseconds (2.1%)
    libMainThreadChecker.dylib :  22.19 milliseconds (5.7%)
                  AFNetworking :  11.66 milliseconds (3.0%)
                  TestDemo :  38.19 milliseconds (9.9%)

// DYLD_PRINT_STATISTICS_DETAILS
  total time: 614.71 milliseconds (100.0%)
  total images loaded:  401 (380 from dyld shared cache)
  total segments mapped: 77, into 1785 pages with 252 pages pre-fetched
  total images loading time: 337.21 milliseconds (54.8%)
  total load time in ObjC:  12.81 milliseconds (2.0%)
  total debugger pause time: 307.99 milliseconds (50.1%)
  total dtrace DOF registration time:   0.07 milliseconds (0.0%)
  total rebase fixups:  152,438
  total rebase fixups time:   2.23 milliseconds (0.3%)
  total binding fixups: 496,288
  total binding fixups time: 218.03 milliseconds (35.4%)
  total weak binding fixups time:   0.75 milliseconds (0.1%)
  total redo shared cached bindings time: 221.37 milliseconds (36.0%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load:  43.56 milliseconds (7.0%)
                         libSystem.B.dylib :   3.67 milliseconds (0.5%)
               libBacktraceRecording.dylib :   3.41 milliseconds (0.5%)
                libMainThreadChecker.dylib :  21.19 milliseconds (3.4%)
                              AFNetworking :  10.89 milliseconds (1.7%)
                              TestDemo :   2.37 milliseconds (0.3%)
total symbol trie searches:    1267474
total symbol table binary searches:    0
total images defining weak symbols:  34
total images using weak symbols:  97

看完时间后我们再来具体看看做了些什么? 以及我们在这个过程怎么优化点.

加载可执行文件
加载 Mach-O 格式文件,既 App 中所有类编译后生成的格式为 .o 的目标文件集合。

dyld 加载 dylib 会完成如下步骤:

分析 App 依赖的所有 dylib。
找到 dylib 对应的 Mach-O 文件。
打开、读取这些 Mach-O 文件,并验证其有效性。
在系统内核中注册代码签名。
对 dylib 的每一个 segment 调用 mmap()。

系统依赖的动态库由于被优化过,可以较快的加载完成,而开发者引入的动态库需要耗时较久。
优化点:
1.减少动态库数量可以加减少启动闭包创建和加载动态库阶段的耗时,官方建议动态库数量小于 6 个。

  1. 推荐的方式是动态库转静态库,因为还能额外减少包大小。另外一个方式是合并动态库,但实践下来可操作性不大。最后一点要提的是,不要链接那些用不到的库(包括系统),因为会拖慢创建闭包的速度。
Rebase和Bind操作

由于使用了ASLR 技术,在 dylib 加载过程中,需要计算指针偏移得到正确的资源地址。 Rebase 将镜像读入内存,修正镜像内部的指针,消耗 IO 性能;Bind 查询符号表,进行外部镜像的绑定,需要大量 CPU 计算。
优化点:
下线代码可以减少 Rebase & Bind & Runtime 初始化的耗时。那么如何找到用不到的代码,然后把这些代码下线呢?可以分为静态扫描和线上统计两种方式。我们可以定期扫描一下项目里面的无用方法,这里需要将方法找出来,然后推进大家负责的模块,去删除.

Objc setup, Initializers, 加载资源文件

进行 Objc 的初始化,包括注册 Objc 类、检测 selector 唯一性、插入分类方法等。

往应用的堆栈中写入内容,包括执行 +load 方法、调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数)、创建非基本类型的 C++ 静态全局变量等。

优化点:
+load 迁移
+load 除了方法本身的耗时,还会引起大量 Page In,另外 +load 的存在对 App 稳定性也是冲击,因为 Crash 了捕获不到。

合并功能类似的类和扩展(Category)
由于Category的实现原理,和ObjC的动态绑定有很强的关系,所以实际上类的扩展是比较占用启动时间的。尽量合并一些扩展,会对启动有一定的优化作用。不过个人认为也不能因为它占用启动时间而去逃避使用扩展,毕竟程序员的时间比CPU的时间值钱,这里只是强调要合并一些在工程、架构上没有太大意义的扩展。

通过减少IO操作量级优化 - 压缩资源图片
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了。

其次因为我的项目是OC-Swift的,由于OC有运行时,会比较耽误时间(各种runtime消息转发), 我们在把OC的类,尽量重写到Swift去,因为Swift静态语言的原因,编译完成就可以检测出没有调用的函数,优化删除后是可以减少二进制文件大小的,从而直接从体积上减少了加载时间. 建议大家在发版后的空闲时期,可以来做一些重构的工作

mian时间段的优化

main阶段的启动优化, 在main()函数之后的didFinishLaunchingWithOptions方法里执行了各种业务,有很多业务不是一定要在这里执行,我们可以延迟加载,防止影响启动时间。
在didFinishLaunchingWithOptions方法里我们一般做一下逻辑:

初始化第三方sdk
配置App运行需要的环境
自己的一些工具类的初始化 等等

Application Initializaiton

这个阶段主要是生命周期方法的回调,也正是开发者最熟悉的部分。

调用 UIApplicationDelegate 的 App 生命周期方法:

  application:willFinishLaunchingWithOptions: 
  application:didFinishLaunchingWithOptions:

在这个阶段,开发者可以做的优化:

推迟和启动时无关的工作
Senens 之间共享资源

Fisrt Frame Render

这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:

 loadView
 viewDidLoad 
 layoutSubviews

我们在闪屏期间需要多关注首页的这些方法加载时间,提前加载,或者异步处理耗时操作,从而来优化启动时间

最后, 聊一下怎么检测你的优化成功

使用 Instruments 分析和优化 App 启动过程

当知道如何优化之后,我们需要针对我们的启动过程进行分析。Xcode 11 的 Instruments 为此新增了一个 App launch 模板,让开发者可以更好的分析自己 App 的启动速度。

image

运行后可以看到各个阶段的具体时间,根据数据进行优化,还能看到耗时的函数调用。

image

以上是自己在工作中总结的一些方法,希望对大家有用.

参考资料:

深入探索 iOS 启动速度优化

https://juejin.cn/post/6844904127068110862

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

https://www.infoq.cn/article/xjb3cysclphv5sh5923q

抖音品质建设 - iOS启动优化《原理篇》

https://mp.weixin.qq.com/s/3-Sbqe9gxdV6eI1f435BDg

抖音品质建设 - iOS启动优化《实战篇》

https://mp.weixin.qq.com/s/ekXfFu4-rmZpHwzFuKiLXw

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

https://mp.weixin.qq.com/s/UlMAvuLuTcWgd3qkEAHYMA

今日头条 iOS 客户端启动速度优化

https://juejin.cn/post/6844903649416577037

马蜂窝 iOS App 启动治理:回归用户体验

https://juejin.cn/post/6844903841410842638

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐