关于ios:iOS-App启动优化

39次阅读

共计 3420 个字符,预计需要花费 9 分钟才能阅读完成。

前言

作为程序猿来说,“性能优化”是咱们都很相熟的词,也是咱们须要一直努⼒以及继续进⾏的事件;其实优化是⼀个很⼤的课题,因为细分来说的话有⼤⼤⼩⼩⼗⼏种优化⽅向,然而切忌在理论开发过程中不能盲⽬的 为了优化⽽优化,这样有时可能会造成事与愿违的负成果,须要咱们依据理论场景以及业务需要进⾏正当优化。接下来进⼊正题,本⽂将会以 iOS App 的启动优化为开展点进⾏探讨。

启动流程

iOS App 的启动咱们都晓得分为为 pre-main 和 main()两个阶段,并且在这两个阶段中,零碎会进⾏⼀系列的加载操作,过程如下:

1、pre-main 阶段

  1. 加载应⽤的可执⾏⽂件
  2. 加载 dyld 动静连接器
  3. dyld 递归加载应⽤所有依赖的动态链接库 dylib

2、main()阶段

  1. dyld 调⽤ main()
  2. 调⽤ UIApplicationMain()
  3. 调⽤ applicationWillFinishLaunching
  4. 调⽤ didFinishLaunchingWithOptions

阶段优化项

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 个⽅法执⾏完,⾸⻚控制器能力显示

场景补充

另外,在咱们理论开发过程中,很多项⽬的⾸⻚控制器都会有⼀些后盾可配、较为丰盛的构造或者举荐数据进⾏展现,⽽且咱们的⾸⻚展现速度通常也会被纳⼊启动优化的⼀局部,其实对于这种类型的优化,如果咱们还只是⽤传统的 api -> data -> UI ⽅式进⾏的话,就很难有显著的改善空间,因为⽤户的⽹络状态 并不是可控项,如果不做其余解决的话,那在很多场景下对⽤户来说,即便咱们放上⼀些占位图,展现的款式也是很不敌对的,毕竟⾸⻚控制器对⽤户的第⼀视觉冲击影响还是⽐较⼤的。

对于这种场景下的优化来说,⼀般咱们能够采取 Local + Network + Update 的⽅式在⼀定水平上优化
⾸⻚加载速度:
即:
1、app 更新过程中,⾸先进⾏本地内嵌解决逻辑,内嵌⾸⻚数据结构(localDataBase)、内嵌⾸⻚款式所需 资源(localStorage)
2、在装置启动之后,对本地与线上数据更新记录进⾏对⽐,检测是否须要更新本地内嵌数据结构
3、检测到有须要更新的数据时,才会对指定构造进⾏静默更新,并且同步更新本地数据结构

这样做的益处是:
1、⾸⻚数据间接从本地加载,缩小⽹络数据等待时间
2、仅检测数据 key 值变动,⼩数据量对⽐定向更新构造,缩小 api 数据交互频次及数据包体积
3、可能保障⾸⻚对于⽤户来说会⼀直处于⼀个敌对的展现状态

当然这种也并不是唯⼀的应答⽅式,⽽且也并⾮对所有场景都适⽤,只是提供⼀种思路⽽已,还是须要依据 项⽬的理论场景抉择适宜的优化⽅案。

统计时长

另外如果在开发过程中,咱们想直观的查看 app 启动期间,各阶段的耗时状况,也能够在 Xcode 的 edit scheme 设置增加 DYLD_PRINT_STATISTICS 为 1,打印启动时⻓,例如

优化前启动时⻓:

优化后启动时⻓:

当然,这些 log 咱们仅仅只能在开发调试阶段查看打印,那么在理论项⽬中,咱们须要对线上项⽬的启动数据进⾏监控,以便及时的定位和优化那些影响 app 启动时⻓的环节,这时咱们应该怎么更好的解决呢?
当然咱们能够通过服务器埋点上报的⽅式⾃⾏统计分析,不过这样⼀来会发现咱们的统计老本就会⼤⼤减少,⽽且后果剖析也会变得不那么灵便。所以这⾥举荐⼀种简略的监控⽅式,那就是友盟的 U -APM 应能性能监控 SDK,只须要咱们进⾏简略的 pod 集成之后,便可依据咱们的理论须要进⾏⼿动或者⾃动监控启动数据,详情能够参考 U -APM,并且为了⽅便咱们对数据进⾏剖析,友盟后盾曾经依据这些数据帮咱们绘制出了对应的分布图,咱们能够⼀⽬了然的得出启动耗时散布、启动类型占⽐等等,如图:

1. 启动耗时和次数


2. 启动耗时散布


除此之外,咱们还能够通过 SDK 进⾏解体剖析、ANR 剖析、监控告警、卡顿剖析、内存剖析等等诸多性能,有了 U-APM 这个监控平台,其实在理论开发过程中很⼤水平的晋升了咱们对线上 app 的优化剖析效率。当然本⽂的介绍也只是⽐较通俗的优化项,仅供参考以及思路疏导,优化之路任重⽽道远,还须要咱们一直 的去摸索、发现、提⾼。不过最初还是要揭示⼀句:在理论项⽬开发过程中,不要为了优化⽽优化,要依据 项⽬状况有针对性的进⾏优化。

参考:
探秘 Mach-O ⽂件
iOS 底层 – 从头梳理 dyld 加载流程
iOS app 启动 – dyld 加载 App 流程
wwdc2016optimizingappstartuptime.pdf

作者:武玉宝

正文完
 0