一, 前言
随着项目版本的迭代, App 的性能问题会逐渐暴露出来, 而好的用户体验与性能表现紧密相关, 从本篇文章开始, 我将开启一个 Android 应用性能优化的专题, 从理论到实战, 从入门到深挖, 手把手将性能优化实践到项目中, 欢迎持续关注!
那么第一篇文章我就从应用的启动优化开始, 根据实际案例, 打造闪电般的 App 启动速度.
二, 初识启动加速
来看一下 Google 官方文档《Launch-Time Performance》(https://ldeveloper.android.com/topic/performance/launch-time.html)对应用启动优化的概述;
应用的启动分为冷启动, 热启动, 温启动, 而启动最慢, 挑战最大的就是冷启动: 系统和 App 本身都有更多的工作要从头开始!
应用在冷启动之前, 要执行三个任务:
加载启动 App;
App 启动之后立即展示出一个空白的 Windows;
创建 App 的进程;
而这三个任务执行完毕之后会马上执行以下任务:
创建 App 对象;
启动 Main Thread;
创建启动的 Activity 对象;
加载 View;
布置屏幕;
进行第一次绘制;
而一旦 App 进程完成了第一次绘制, 系统进程就会用 Main Activity 替换已经展示的 Background Windows, 此时用户就可以使用 App 了.
作为普通应用, App 进程的创建等环节我们是无法主动控制的, 可以优化的也就是 Application,Activity 创建以及回调等过程.
同样, Google 也给出了启动加速的方向:
利用提前展示出来的 Windows, 快速展示出来一个界面, 给用户快速反馈的体验;
避免在启动时做密集沉重的初始化(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
Nexus6P
优化后:
MX3
Nexus6P
对比:
MX3 提升 35%
Nexus6P 提升 39%
命令含义:
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 进行分析, 尽早发现瓶颈.
来源: http://mobile.51cto.com/aengine-603482.htm