- 本文项目地址:github.com/Kyson/Rocke…
- 相关项目地址:github.com/Kyson/Andro…
随着应用逐渐成熟,启动所执行的任务也越来越多,每次版本迭代就会发现启动耗时又增加了,然后开始找原因,优化任务,下次版本发布重复这一过程,如此反复。
这里有两个问题
- 启动时间优化能到什么地步?
- 为什么每次版本迭代都会增加启动时间?
我们先从现状谈起。
现状
在Android中,启动一般分为三部分,application(应用入口) -> splash页(展示logo、广告等等) -> 首页。
第一阶段
这是一个app开发初期的启动概览图,我们会有若干sdk或者代码需要初始化,所以都塞进application oncreate中,当然了,如果有些sdk必须在子线程中初始化,那我们在后面起一个线程做这个事情,为了保证用户进入首页的时候已经执行完所有任务了,那我们可能会在闪屏页等待,直到异步线程做完...
这里问题比较多,比如异步线程完全可以和主线程并发执行,主线程的任务有些还是可以放子线程做的等等,所以稍有经验的开发者会忍不住重构一下,进入第二阶段。
第二阶段
这一阶段,我们把异步线程提前执行,这样保证了主线程子线程的并发,而且,还把一些代码放进了异步线程,主线程的代码会大大减少,这样,主线程执行很久的时间就会大大缩短。
做到这样,似乎已经很不错了,但是我们不能满足,继续思考一下,现在的启动时间短板在哪里?异步线程执行时间太久?主线程执行时间能不能再少?另外,首页绘制时间太久了,闪屏页除了等待似乎没什么作用了...
这么一想,好像还有很多优化空间,所以我们再重构一下,进入第三阶段。
第三阶段
在第三阶段,我们希望:
- 增加并发量,充分利用cpu,缩短异步线程的执行时间
- 首页无需等待所有任务完成
- 便于统计监控
- 可持续,版本迭代理论上不会显著增加启动时间
那么启动的模型大概长这样:
首先,我们先让首页减少布局层级、减少不必要的view,然后去除闪屏页,使用首页的WindowBackground来实现类似效果,最后,代码段都精细化并任务化,并把这些任务托管给框架来安排执行,我给这个框架取个名字,叫做"Rocket"。
Rocket
Rocket的设计以任务为最小单位,每个任务执行在哪个线程、执行的任务具体是什么、任务依赖关系等等都可以让任务自己定制,而Rocket则负责完成这些任务最快且正确地执行。
下面以7个关键问题来说明Rocket的设计和实现。
问题1:什么样的代码段需要创建线程?
启动的任务很多,有些任务耗时,有些任务不耗时,如果我们把所有的代码都细分并且都创建线程,那么可能启动的时候会创建几十甚至上百的线程,创建线程的时间、线程切换的开销或许比任务本身更耗时。
所以对于这个问题,我们需要权衡任务的耗时是不是值得让我们创建线程,这里我没有明确的耗时时间的分界点,毕竟对于不同的app、不同的设备、不同的任务都是不一样的情况。
问题2:使用哪种线程池?
需要事先说明的是,Rocket不会强制使用某种线程池,每个任务的执行线程由任务自己控制。
首先考虑如下两个方法的区别
System.currentTimeMillis()
SystemClock.currentThreadTimeMillis()
大家都知道,我们的代码都执行在线程上,cpu会在不同的线程之间切换执行,所以,SystemClock.currentThreadTimeMillis()就是cpu分配在当前线程的时间片。
然后考虑如下任务类型的区别
- I/O密集
- CPU密集
这里举几个例子
I/O密集任务:磁盘文件处理,网络传输(socket) CPU密集:计算任务,比如排序,动画
根据这些例子,实际上我们也可以知道这些任务的特性,I/O密集的任务,线程处于wait状态的时间会比较多,消耗的cpu较少,而CPU密集的任务,线程基本处于执行状态,消耗cpu较多,所以对于I/O密集任务,我们可以创建较多的线程,比如使用Executors.newCachedThreadPool()线程池,而对于CPU密集任务,如果创建了大量线程,那么只会徒增cpu切换的开销,反而增加了执行时间,所以我们一般会使用固定线程数量的线程池,比如Executors.newFixedThreadPool(5)。
题外话:既然网络传输属于I/O密集任务,为什么volley等等网络库会使用类似固定数量的线程池呢?
实际上,我的思考有两点,纯属猜测,如有异议,希望不吝赐教:
- 网络请求时间比较久,为了避免创建大量线程
- 最重要的是,网络请求需要有请求队列策略,比如FIFO,LIFO等等,而使用类似CachedThreadPool是没有等待队列的
所以现在回到问题上来,用哪种线程池?
事实上这也应该根据你的app的实际状况决定,我这里的情况是:任务大多数执行时间短,且处于等待时间较长,也就是说,大部分任务属于I/O密集任务,所以我这里使用CachedThreadPool线程池。
问题3:如何处理任务之间的依赖关系?
理论上,外部任务只需要知道自己依赖其他哪些任务即可,所以Rocket仅仅给任务提供了List<String> dependsOn()接口,用于表示这一点。 而为了保证执行的顺序(比如B依赖A,那么B应该等待A执行完成才开始),我们需要对任务进行排序,实际上,任务间的依赖就是有向图的数据结构,而排序算法也就是有向图的拓扑排序算法。
有向图的拓扑排序保证了所有任务,如果B依赖A,那么B必然在A后面,它使用邻接表来存储每个点及邻接点。算法描述:找到所有入度为0的点添加到0入度的队列中,遍历该队列,删除该点(输出),并让邻接点的入度-1,如果邻接点入度为0,那么添加到0入度的队列中,循环,直到没有点为止。如果遍历次数多于点的个数,说明有环。
保证了执行顺序之后,我们只需要逐个执行任务即可。
问题4:首页如何保证任务执行完成?
首先,首页并不需要等待所有的任务完成,而只需要等待它需要的几个任务完成即可,所以Rocket提供了ensureTask接口,表述页面或者代码依赖了哪些任务,这个接口会一直wait,直到任务完成。
问题5:有些任务非常重要,但是很耗时,怎样让它拿到更多的cpu时间片?
A任务非常耗时,同时首页又必须等A任务完成之后才可以进入,在启动任务并发的时候,A只是其中的一个任务,如果没有区分的话,cpu都是一视同仁的,所以,Rocket提供了优先级的接口,可以设置A任务的优先级大于其他任务,那么cpu就会更青睐A。
问题6:如何接入Rocket,需要注意什么?
接入Rocket:QuickStart
需要注意的是,添加的任务应该清楚自己的依赖关系并在dependsOn()接口声明。
问题7:优化效果如何在迭代过程中保持?
接入Rocket之后,这种情况会显著增加启动时间:
首先,首页依赖该任务,同时满足下面的其中一点
- 任务依赖当前最长任务链中的任务
- 本身非常耗时,超过了当前最长任务链
其他一般的情况下不会明显增加启动时间。
最后
应用性能优化是一项长期且艰巨的任务,除了启动时间优化,还有很多优化任务等待着我们去解决...
另外,这里有个辅助性能优化的工具github.com/Kyson/Andro…,用于实时查看Android性能数据,希望能有帮助。





