一、前言
随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我将开启一个Android应用性能优化的专题,从理论到实战,从入门到深挖,手把手将性能优化实践到项目中,欢迎持续关注!
那么第一篇文章我就从应用的启动优化开始,根据实际案例,打造闪电般的App启动速度。
二、初识启动加速
来看一下Google官方文档《Launch-Time Performance》对应用启动优化的概述;
应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始! 应用在冷启动之前,要执行三个任务:
- 加载启动App;
- App启动之后立即展示出一个空白的Window;
- 创建App的进程;
而这三个任务执行完毕之后会马上执行以下任务:
- 创建App对象;
- 启动Main Thread;
- 创建启动的Activity对象;
- 加载View;
- 布置屏幕;
- 进行第一次绘制;
而一旦App进程完成了第一次绘制,系统进程就会用Main Activity替换已经展示的Background Window,此时用户就可以使用App了。
作为普通应用,App进程的创建等环节我们是无法主动控制的,可以优化的也就是Application、Activity创建以及回调等过程。
同样,Google也给出了启动加速的方向:
- 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
- 避免在启动时做密集沉重的初始化(Heavy app initialization);
- 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。
备注:方向1属于治标不治本,只是表面上快;方向2、3可以真实的加快启动速度。 接下来我们就在项目中实际应用。
三、启动加速之主题切换
按照官方文档的说明:使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable。 Layout XML file:
Manifest file:
这样在启动的时候,会先展示一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而产生一种快的感觉。不过如上文总结这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。 备注:截图同样来自官方文档《Launch-Time Performance》。
四、启动加速之Avoid Heavy App Initialization
通过代码分析我们可以得到App启动的业务工作流程图:
这一章节我们重点关注初始化的部分:在Application以及首屏Activity中我们主要做了:
- MultiDex以及Tinker的初始化,最先执行;
- Application中主要做了各种三方组件的初始化;
项目中除听云之外其余所有三方组件都抢占先机,在Application主线程初始化。这样的初始化方式肯定是过重的:
- 考虑异步初始化三方组件,不阻塞主线程;
- 延迟部分三方组件的初始化;实际上我们粗粒度的把所有三方组件都放到异步任务里,可能会出现WorkThread中尚未初始化完毕但MainThread中已经使用的错误,因此这种情况建议延迟到使用前再去初始化;
- 而如何开启WorkThread同样也有讲究,这个话题在下文详谈。
项目修改:
- 将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;
- 延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader 因为调用关系不能异步以及过久延迟,初始化从Application延迟到SplashActivity;而EventBus因为再Activity中使用所以必须在Application中初始化。
注意:闪屏页的2秒停留可以利用,把耗时操作延迟到这个时间间隔里。
五、启动加速之Diagnosing The Problem
本节我们实际定位耗时的操作,在开发阶段我们一般使用BlockCanary或者ANRWatchDog找耗时操作,简单明了,但是无法得到每一个方法的执行时间以及更详细的对比信息。我们可以通过Method Tracing或者DDMS来获得更全面详细的信息。 启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。
左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。注意两列:Real Time/Call(实际发生时间),Calls RecurCalls/Total(发生次数); 上图我们可以得到以下信息:
- 可以直观看到MainThread的时间轴很长,说明大多数任务都是在MainThread中执行;
- 通过Real Time/Call 降序排列可以看到程序中的部分代码确实非常耗时;
- 在下一页可以看出来部分三方SDK也比较耗时;
即便是耗时操作,但是只要正确发生在WorkThread就没问题。因此我们需要确认这些方法执行的线程以及发生的时机。这些操作如果发生在主线程,可能不构成ANR的发生条件,但是卡顿是再算难免的!结合上章节图App冷启动业务工作流程图中业务操作以及分析图,再次查看代码我们可以看到:部分耗时操作例如IO读取等确实发生在主线程。事实上在traceview里点击执行函数的名称不仅可以跟踪到父类及子类的方法耗时,也可以在方法执行时间轴中看到具体在哪个线程以及耗时的界面闪动。
分析到部分耗时操作发生在主线程,那我们把耗时操作都改到子线程是不是就万事大吉了?非也!!
- 卡顿不能都靠异步来解决,错误的使用工程线程不仅不能改善卡顿,反而可能加剧卡顿。是否需要开启工作线程需要根据具体的性能瓶颈根源具体分析,对症下药,不可一概而论;
- 而如何开启线程同样也有学问:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService等都各有利弊;例如通常情况下ThreadPoolExecutor比Thread更加高效、优势明显,但是特定场景下单个时间点的表现Thread会比ThreadPoolExecutor好:同样的创建对象,ThreadPoolExecutor的开销明显比Thread大;
- 正确的开启线程也不能包治百病,例如执行网络请求会创建线程池,而在Application中正确的创建线程池势必也会降低启动速度;因此延迟操作也必不可少。
通过对traceview的详细跟踪以及代码的详细比对,我发现卡顿发生在:
- 部分数据库及IO的操作发生在首屏Activity主线程;
- Application中创建了线程池;
- 首屏Activity网络请求密集;
- 工作线程使用未设置优先级;
- 信息未缓存,重复获取同样信息;
- 流程问题:例如闪屏图每次下载,当次使用;
以及其它细节问题:
- 执行无用老代码;
- 执行开发阶段使用的代码;
- 执行重复逻辑;
- 调用三方SDK里或者Demo里的多余代码;
项目修改: 1. 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程最多能获取到10%的时间片,优先保证主线程执行。
2. 流程梳理,延后执行; 实际上,这一步对项目启动加速最有效果。通过流程梳理发现部分流程调用时机偏早、失误等,例如:
- 更新等操作无需在首屏尚未展示就调用,造成资源竞争;
- 调用了IOS为了规避审核而做的开关,造成网络请求密集;
- 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争,在上图traceview功能说明图中最后一行可以看到编号12执行5次,耗时排名前列;此处线程池的创建是必要但可以延后的。
- 修改广告闪屏逻辑为下次生效。
3.其它优化;
- 去掉无用但被执行的老代码;
- 去掉开发阶段使用但线上被执行的代码;
- 去掉重复逻辑执行代码;
- 去掉调用三方SDK里或者Demo里的多余代码;
- 信息缓存,常用信息只在第一次获取,之后从缓存中取;
- 项目是多进程架构,只在主进程执行Application的onCreate();
通过以上三步及三方组件的优化:Application以及首屏Activity回调期间主线程就没有耗时、争抢资源等情况了。此外还涉及布局优化、内存优化等部分技术,因对于应用冷启动一般不是瓶颈点,这里不展开详谈,可根据实际项目实际处理。
六、对比效果:
通过ADB命令统计应用的启动时间:adb shell am start -W 首屏Activity。 同等条件下使用MX3及Nexus6P,启动5次,比较优化前与优化后的启动时间;
优化前: MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
1237 | 2205 | 2214 |
1280 | 2181 | 2189 |
1622 | 2508 | 2513 |
1485 | 2434 | 2443 |
1442 | 2418 | 2429 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
1229 | 1832 | 1868 |
1268 | 1849 | 1880 |
1184 | 1780 | 1812 |
1262 | 1845 | 1876 |
1164 | 1766 | 1807 |
优化后: MX3
ThisTime | TotalTime | WaitTime |
---|---|---|
865 | 1516 | 1523 |
911 | 1565 | 1573 |
812 | 1406 | 1418 |
962 | 1564 | 1574 |
925 | 1566 | 1577 |
Nexus6P
ThisTime | TotalTime | WaitTime |
---|---|---|
603 | 1192 | 1243 |
614 | 1076 | 1115 |
650 | 1120 | 1163 |
642 | 1107 | 1139 |
624 | 1084 | 1124 |
对比: MX3提升35%
对比 | ThisTime平均数 | TotalTime平均数 | WaitTime平均数 |
---|---|---|---|
优化前 | 1413 | 2349 | 2357 |
优化后 | 895 | 1523 | 1533 |
Nexus6P提升39%
对比 | ThisTime平均数 | TotalTime平均数 | WaitTime平均数 |
---|---|---|---|
优化前 | 1221 | 1814 | 1848 |
优化后 | 626 | 1115 | 1156 |
- 命令含义: ThisTime:最后一个启动的Activity的启动耗时; TotalTime:自己的所有Activity的启动耗时; WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)。
七、问题:
1、还可以继续优化的方向?
- 项目里使用Retrofit网络请求库,FastConverterFactory做Json解析器,TraceView中看到FastConverterFactory在创建过程中也比较耗时,考虑将其换为GsonConverterFactory。但是因为类的继承关系短时间内无法直接替换,作为优化点暂时遗留;
- 可以考虑根据实际情况将启动时部分接口合并为一,减少网络请求次数,降低频率;
- 相同功能的组件只保留一个,例如:友盟、GrowingIO、自有统计等功能重复;
- 使用ReDex进行优化;实验Redex发现Apk体积确实是小了一点,但是启动速度没有变化,或许需要继续研究。
2、异步、延迟初始化及操作的依据? 注意一点:并不是每一个组件的初始化以及操作都可以异步或延迟;是否可以取决组件的调用关系以及自己项目具体业务的需要。保证一个准则:可以异步的都异步,不可以异步的尽量延迟。让应用先启动,再操作。
3、通用应用启动加速套路?
- 利用主题快速显示界面;
- 异步初始化组件;
- 梳理业务逻辑,延迟初始化组件、操作;
- 正确使用线程;
- 去掉无用代码、重复逻辑等。
4、其它
- 将启动速度加快了35%不代表之前的代码都是问题,从业务角度上将,代码并没有错误,实现了业务需求。但是在启动时这个注重速度的阶段,忽略的细节就会导致性能的瓶颈。
- 开发过程中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。
结尾
好啦,文章写到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发 点赞 关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!