再谈App的启动优化

本文首发于我的微信公众号(程序员华仔),有兴趣,去看看。 

--------------分割线------------------------

之前在实际项目中处理过App的启动优化的问题,而今再对这个问题进行系统的整理,也算一种温故而知新。

如果说,之前的项目是偏实践的,是缺乏理论的指导,那么今天就是理论和实践的一次结合。

记得那是几年的一个项目,当时的需求是:“我们的App启动太慢了,需要做到1秒内打开”,也即“秒开”。这个任务最终由我去分析和解决。

处理这样一个棘手的问题,当时的我,没有经验,没有可参考的范例。完全是摸着石头过河。

然而,当时我根据个人经验,制定了一个大概的实施步骤:

1.既然说App启动慢,那么需要多久的时间才能启动呢?(搞清楚现状)

2.在哪些方向上优化,能减少启动时间呢?(确定优化方向)

3.针对每个方向,精雕细琢地优化每一个能优化的点。

第一步:搞清楚现状

由于我们的app模块比较多、业务比较复杂。启动流程也不一样。所以不能简单地统计出app的启动时间。

还好启动和业务流程,我都比较熟悉,知道哪些启动场景。所以我就自测完成了每一个启动流程的场景并得到对应的启动时间。然后统计出平均的启动时间,最终平均时间为:2.4秒。

这个数据,对比了微信、飞书等应用,他们的启动时间没有这么长,我想这就是我要改进的地方。

第二步:确定优化方向

根据工作经验和知识积累,当时我确定以下的优化方向:

1.工程目录下的没有使用的类、文件、资源,一律删除。减少没有必要的编译。

2.对业务模块的+load()函数的优化。尽量不执行没有必要的操作,例如读写IO操作、多线程操作等。

3.对didFinishLaunchingWithOptions函数的优化,这个部分主要是业务模块的代码,我的主要优化点也是在这一块。

第三步:具体各项的优化

既然确定了优化的方向,那就按这些方向,进行各项的优化。对于第一、二点(无效类文件,+load函数的处理)没有什么问题。直接操纵即可。

对于第三点(didLaunch),有一些细节可说。

首先我写了一个函数执行时间统计工具。当然Instruments也有对应的工具,只是我感觉它统计的时间不精确(我的精确到了微妙级)。

然后使用这个统计工具,把didLaunch函数中认为耗时的的函数都统计了一遍。从统计到的数据来看,有100ms,200ms,300ms等不同的执行时间,也有一个是600ms。除了600ms的时间,认为耗时比较大,其他的执行时间都是很短的。

就单个执行时间来说,优化的空间有限了。发现这个问题之后,我就只对600ms的执行函数进行认真分析和优化,其他的就放过了。其实600ms的执行函数,也就是在网络请求和读取IO方面多了一些时间,其他的没有发现耗时的地方。

做完了上面的优化后,我当然知道对整体App的性能,没有提升。所以我就对didLaunch函数整体部分进行了优化。大概是以下的一些方向:

1.对于没有必要的网络请求,延迟到启动之后。

2.对于没有必要的业务和数据处理,也延迟到启动之后。(通过这两步操作,didLaunch函数移除了一些代码)

3.对于必要的IO读取和网络请求,都开启线程。

4.优化启动步骤,省去了一些没有必要的启动步骤。

通过以上这些步骤的处理,我们当时的App启动时间缩短到0.9秒。满足了1秒内打开的要求。

写到这里,如果认为就结束了,那就大错特错了。前面只是实践,缺乏通用的方案。那有没有通用的指导方法呢?答案是有的。

这个指导方案到了几年后的今天,我才拿出来好好的整理一下。经过一段时间的学习、思考和总结,得到了这个问题的方法论,其实所谓的方法论就是应用程序的启动流程。

App的启动流程

应用程序的启动流程一般划分以下的几个阶段:

1.pre-main阶段

2.main到didFinishLaunch阶段

3.didFinishLaunch阶段

1.pre-main阶段

1.pre-main 指的是操作系统开始执行一个可执行文件,并完成进程创建、执行文件加载、动态链接、环境配置等过程。

这里主要讲dyld的调用。dyld又细分以下步骤:

1.加载dyld到App进程

2.加载App所需要的动态库

3.rebase/bind

4.Objc类的初始化(调用+load)

5.Initializers 

具体步骤见下图

写到这里:

1.想想在实战部分的对load函数的优化,就是在pre-main阶段需要做的事情。

2.想到了一个经常遇到的面试题:load和Initializers的区别是什么?答案就在这里。

3.还有若是想进一步了解以上步骤的具体细节,可以看下【连载】程序员自我修养读书心得(总结篇)

还要一点补充,其实在Xcode里面可以打印以上步骤的执行时间,这样就可以展示以上步骤所花费的时间,具体操作就是设置环境变量(DYLD_PRINT_STATISTICS)

libMainThreadChecker.dylib 是XCode 9新增的动态库,主要做主线程检查

2.main到didFinishLaunch阶段

系统主要做三个事情:

   1.创建App的对象。

   2.开启App的runloop。

   3.设置App的Delegate等参数。

在这个阶段,主要由系统完成,这里没有可优化的空间。

3.didFinishLaunch阶段

这个阶段就是我们大家熟悉的业务模块的调用,这块的优化,主要取决于优化者的道行了,不过还是要遵循以下几个原则:

1.开线程处理耗时模块。

2.延迟执行没有必要的代码。

3.优化流程,尽量减少可执行的代码。

二进制的重排

到这里,似乎该结束本文了。然而还要说下“二进制的重排 ”,其实“二进制的重排”,

简单来说:对执行的符号(函数)进行重新调整装载次序。举例说下,启动的时候,要调用A模块的Test1、B模块的Test2、C模块的Test3 三个模块三个函数。按正常装载流程来说,就要装载A模块的所有函数(包括Test1),B模块的所有函数(包括Test2),C模块的所有函数(包括Test3)。

我们知道IOS系统是使用页的方式进行装载的,且每一页是16Kb。那正常装载的话,加载A、B、C三个模块就需要3个16Kb(假设每个模块需要一个16Kb)的页来加载。而我们实际需求就是加载A模块的Test1,B模块的Test2和C模块的Test3. 这三个函数,也许就只要1个16Kb的页面就可以了。同样的符号的装载次序,在未采用二进制重排机制是:main、didFinishLanch、Test1、Test2、Test3。采用重排,可以变成:Test1、Test2、Test3,main、didFinishLanch。这样Test1,Test2,Test3 排在前面,就有优先命中的概率(这里说的是装载次序调整,不是调用次序,调用次序还是main开头,didFinishLanch次之,Test1、2、3根据调用次序决定),从而提高了执行速度。 

从整体来说,“二进制的重排”,节省了物理内存,提高了加载速度,加快了App启动速度,是一个值得采用的方案。那具体怎么做“二进制的重排”,因篇幅问题,下次专题讲讲。

-------------- end --------------

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员华仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值