Android性能优化(一)之启动加速35%

1、前言

随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我将开启一个Android应用性能优化的专题,从理论到实战,从入门到深挖,手把手将性能优化实践到项目中,欢迎持续关注!html

那么第一篇文章我就从应用的启动优化开始,根据实际案例,打造闪电般的App启动速度。android

2、初识启动加速

来看一下Google官方文档《Launch-Time Performance》对应用启动优化的概述;git

应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App自己都有更多的工做要从头开始! 应用在冷启动以前,要执行三个任务:github

  1. 加载启动App;
  2. App启动以后当即展现出一个空白的Window;
  3. 建立App的进程;

而这三个任务执行完毕以后会立刻执行如下任务:shell

  1. 建立App对象;
  2. 启动Main Thread;
  3. 建立启动的Activity对象;
  4. 加载View;
  5. 布置屏幕;
  6. 进行第一次绘制;

而一旦App进程完成了第一次绘制,系统进程就会用Main Activity替换已经展现的Background Window,此时用户就可使用App了。数据库

应用冷启动流程图
做为普通应用,App进程的建立等环节咱们是没法主动控制的, 能够优化的也就是Application、Activity建立以及回调等过程

一样,Google也给出了启动加速的方向缓存

  1. 利用提早展现出来的Window,快速展现出来一个界面,给用户快速反馈的体验;
  1. 避免在启动时作密集沉重的初始化(Heavy app initialization);
  2. 定位问题:避免I/O操做、反序列化、网络操做、布局嵌套等。

备注:方向1属于治标不治本,只是表面上快;方向二、3能够真实的加快启动速度。 接下来咱们就在项目中实际应用。性能优化

3、启动加速之主题切换

按照官方文档的说明:使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable。 Layout XML file: 微信

资源文件配置
Manifest file:

Manifest文件中

Activity中

这样在启动的时候,会先展现一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,咱们将主题从新设置为正常的主题,从而产生一种快的感受。不过如上文总结这种方式其实并无真正的加速启动过程,而是经过交互体验来优化了展现的效果。 备注:截图一样来自官方文档《Launch-Time Performance》markdown

4、启动加速之Avoid Heavy App Initialization

经过代码分析咱们能够获得App启动的业务工做流程图:

App冷启动业务工做流程图

这一章节咱们重点关注初始化的部分:在Application以及首屏Activity中咱们主要作了:

  • MultiDex以及Tinker的初始化,最早执行;关于MultiDex的优化本文再也不赘述,参考我以前Multidex的系列文章
  • Application中主要作了各类三方组件的初始化;

项目中**除听云以外其他全部三方组件都抢占先机,在Application主线程初始化。**这样的初始化方式确定是太重的:

  • 考虑异步初始化三方组件,不阻塞主线程;
  • 延迟部分三方组件的初始化;实际上咱们粗粒度的把全部三方组件都放到异步任务里,可能会出现WorkThread中还没有初始化完毕但MainThread中已经使用的错误,所以这种状况建议延迟到使用前再去初始化;
  • 而如何开启WorkThread一样也有讲究,这个话题在下文详谈。

项目修改:

  1. 将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;
  2. 延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader 由于调用关系不能异步以及太久延迟,初始化从Application延迟到SplashActivity;而EventBus由于再Activity中使用因此必须在Application中初始化。

三方组件调用优化示例代码

注意:闪屏页的2秒停留能够利用,把耗时操做延迟到这个时间间隔里。

5、启动加速之Diagnosing The Problem

本节咱们实际定位耗时的操做,在开发阶段咱们通常使用BlockCanary或者ANRWatchDog找耗时操做,简单明了,可是没法获得每个方法的执行时间以及更详细的对比信息。咱们能够经过Method Tracing或者DDMS来得到更全面详细的信息。 启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操做所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。

优化以前应用启动trace文件分析图

左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。注意两列: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回调期间主线程就没有耗时、争抢资源等状况了。此外还涉及布局优化、内存优化等部分技术,因对于应用冷启动通常不是瓶颈点,这里不展开详谈,可根据实际项目实际处理。

6、对比效果:

经过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
优化后 895 1523


Nexus6P提高39%

ThisTime平均数 TotalTime平均数 WaitTime平均数
优化前 1221 1814
优化后 626 1115
  • 命令含义: ThisTime:最后一个启动的Activity的启动耗时; TotalTime:本身的全部Activity的启动耗时;
    WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和本身Activity的启动)。

7、问题:

一、还能够继续优化的方向?

  • 项目里使用Retrofit网络请求库,FastConverterFactory作Json解析器,TraceView中看到FastConverterFactory在建立过程当中也比较耗时,考虑将其换为GsonConverterFactory。可是由于类的继承关系短期内没法直接替换,做为优化点暂时遗留;
  • 能够考虑根据实际状况将启动时部分接口合并为一,减小网络请求次数,下降频率;
  • 相同功能的组件只保留一个,例如:友盟、GrowingIO、自有统计等功能重复;
  • 使用ReDex进行优化;实验Redex发现Apk体积确实是小了一点,可是启动速度没有变化,或许须要继续研究。

二、异步、延迟初始化及操做的依据? 注意一点:并非每个组件的初始化以及操做均可以异步或延迟;是否能够取决组件的调用关系以及本身项目具体业务的须要。保证一个准则:能够异步的都异步,不能够异步的尽可能延迟。让应用先启动,再操做。

三、通用应用启动加速套路?

  • 利用主题快速显示界面;
  • ** 异步初始化组件;**
  • ** 梳理业务逻辑,延迟初始化组件、操做;**
  • ** 正确使用线程;**
  • ** 去掉无用代码、重复逻辑等。**

四、其它

  • 将启动速度加快了35%不表明以前的代码都是问题,从业务角度上将,代码并无错误,实现了业务需求。可是在启动时这个注重速度的阶段,忽略的细节就会致使性能的瓶颈。
  • 开发过程当中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。

参考文章:《官方文档——Launch-Time Performance》

欢迎关注微信公众号:按期分享Java、Android干货!

欢迎关注
相关文章
相关标签/搜索