随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关。应用的启动速度缓慢这是很多开发者都遇到的一个问题,比如启动缓慢导致的黑屏,白屏问题,大部分的答案都是做一个透明的主题,或者是做一个Splash界面,但是这并没有从根本上解决这个问题, 只是从视觉上让用户以为黑屏白屏问题得到了解决。那么如何从根本上解决这个问题或者做到一定程度的缓解?
应用的启动方式
应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!
-
冷启动
冷启动指的是应用程序从头开始:系统的进程没有,直到此开始,创建了应用程序的进程。 在应用程序自设备启动以来第一次启动或系统杀死应用程序等情况下会发生冷启动。 这种类型的启动在最小化启动时间方面是最大的挑战,因为系统和应用程序比其他启动状态具有更多的工作。 -
热启动
与冷启动相比,热启动应用程序要简单得多,开销更低。在热启动,系统会把你活动放到前台,如果所有应用程序的活动仍驻留在内存中,那么应用程序可以避免重复对象初始化,UI的布局和渲染。
热启动显示与冷启动场景相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。 -
温启动
用户退出您的应用,但随后重新启动。该过程可能已继续运行,但应用程序必须通过调用onCreate()从头开始重新创建活动。系统从内存中驱逐您的应用程序,然后用户重新启动它。进程和Activity需要重新启动,但任务可以从保存的实例状态包传递到onCreate()中。
为什么出现白屏
冷启动白屏持续时间可能会很长,这可是个槽糕的体验,它的启动速度是由于以下引起的:
1、Application的onCreate流程,对于大型的APP来说,通常会在这里做大量的通用组件的初始化操作;
建议:很多第三方SDK都放在Application初始化,我们可以放到用到的地方才进行初始化操作。
2、Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题;
建议:Activity仅初始化那些立即需要的对象,xml布局减少冗余或嵌套布局。
优化APP启动速度意义重大,启动时间过长,可能会使用户直接卸载APP。
启动速度优化方案
作为普通应用,App进程的创建等环节我们是无法主动控制的,可以优化的也就是Application、Activity创建以及回调等过程。
关于启动加速方案,Google给出的建议是:
1.利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
2.避免在启动时做密集沉重的初始化(Heavy app initialization);
3.定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。
官方文档给出的建议中,方向1属于治标不治本,只是表面上快;方向2、3可以真实的加快启动速度。接下来我们就在项目中实际应用一下。
- 1.启动加速之主题切换
定义一个style:
<style name="AppTheme.Launcher">
<!--关闭启动窗口-->
<item name="android:windowDisablePreview">true</item>
</style>
只需要再启动页面引用:
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
最后在MainActivity恢复正常主题:
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(R.style.AppTheme);
setContentView(R.layout.activity_main);
}
}
这样启动APP,就没有白屏,但会出现点击桌面图标而半天没有反应的现象,显然不好,很多APP把这个闪屏当做一个广告、品牌宣传的页面。
来看看如何使用drawable方式实现的,修改之前的style:
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/branded_launch_screens</item>
</style>
drawable/branded_launch_screens:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<!--黑色背景颜色-->
<item android:drawable="@android:color/black" />
<!-- 产品logo-->
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/empty_image01" />
</item>
<!-- 右上角的图标元素 -->
<item>
<bitmap
android:gravity="top|right"
android:src="@mipmap/github" />
</item>
<!--最下面的文字-->
<item android:bottom="50dp">
<bitmap
android:gravity="bottom"
android:src="@mipmap/ic_launcher" />
</item>
</layer-list>
其中android:opacity=”opaque”参数是为了防止在启动的时候出现背景的闪烁。
如果不用drawable的方式实现,直接将背景设置为一张图片也是可以的,更加简单:
<style name="AppTheme.Launcher">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@mipmap/app_welcome</item>
</style>
最终:在启动的时候,会先展示一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而产生一种快的感觉。不过如上文总结这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。
- 2.启动加速之避免过多的初始化操作
在Application以及首屏Activity中我们主要做了下面这些事:
- MultiDex初始化,最先执行;关于MultiDex的优化本文不再赘述,参考我之前的文章。
1. Android使用Multidex突破64K方法数限制原理解析
2. 其实你不知道MultiDex到底有多坑
3. Android MultiDex初次启动APP优化方案优雅的实现 - Application中主要做了各种三方组件的初始化;
对于过多的初始化任务,我们考虑以下优化方案:
- 考虑异步初始化三方组件,不阻塞主线程;
- 延迟部分三方组件的初始化;实际上我们粗粒度的把所有三方组件都放到异步任务里,可能会出现WorkThread中尚未初始化完毕但MainThread中已经使用的错误,因此这种情况建议延迟到使用前再去初始化;
三方组件调用优化示例代码
三方组件调用优化示例代码- 3.启动加速之定位问题
启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。
如果对TraceView的使用不是很清楚的,可以查看下文TraceView的使用 - 数据采集与数据分析
优化之前应用启动trace文件分析图
优化之前应用启动trace文件分析图
左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。
注意两列:Real Time/Call(实际发生时间),Calls+RecurCalls/Total(发生次数);
- 可以直观看到MainThread的时间轴很长,说明大多数任务都是在MainThread中执行;
- 通过Real Time/Call 降序排列可以看到程序中的部分代码确实非常耗时;
- 在下一页可以看出来部分三方SDK也比较耗时;
结合第二种实现方式,可以对项目中耗时严重的代码进行优化。通过以上三步及三方组件的优化:Application以及首屏Activity回调期间主线程就没有耗时、争抢资源等情况了。
TraceView的使用 - 数据采集与数据分析
TraceView
是什么,TraceView 是 Android
平台特有的数据采集和分析工具,主要用做热点分析,找出最需要优化的点。TraceView 从代码层面分析性能问题,针对每个方法来分析,比如当我们发现我们的应用出现卡顿的时候,我们可以来分析出现卡顿时在方法的调用上有没有很耗时的操作,通过TraceView,可以得到两种数据。
- 单次执行最耗时的方法
- 执行次数最多的方法
要打开上面的面板,代码中一般有两种方式:
- 第一种方式:
- 首先选择跟踪范围,在想要根据的
代码片段
之间使用以下两句代码
Debug.startMethodTracing(“hello”);
Debug.stopMethodTracing();
生成的traceview文件会自动放在SDCARD
上,没有SDCARD卡会出现异常,所以使用这种方式需要确保应用的AndroidMainfest.xml中的SD卡的读写权限是打开的,其中hello是traceview文件
的名字,
- 然后用adb导出traceview文件。
adb pull sdcard/hello.trace C:\Users\lwf\Desktop
- 然后启动Android Device Monitor-->File-->openFile,打开traceview文件即可。
- 第二种方式:
- 同样是要先打开Android Device Monitor
- 先选择应用进程,然后点击
Start Method Profiling
(开启方法分析),按钮会变为Stop Method Profiling
(停止方法分析),开启方法分析后,对应用的目标页面进行测试操作,测试完毕后停止方法分析,界面会自动跳转到 DDMS 的 trace 分析界面。
两种方式的对比:第一种方式更精确到方法,起点和终点都是自己定,不方便的地方是自己需要添加方法并且要导出文件,第二种方式的优缺点刚好相反。
下面举例来解释一下TraceView具体是怎么使用的:
该DEMO是用来分别模拟调用次数不多,但每次调用却需要花费很长时间的函数,和自身占用时间不长,但调用却非常频繁的函数。
public class MainActivity extends Activity {
int count = 0;
long longCount=-1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Debug.startMethodTracing("hello");
//线程1
new Thread(new Runnable() {
@Override
public void run() {
printNum();
}
},"printNum_thread").start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
calculate();
}
},"calculate_thread").start();
}
@Override
protected void onDestroy() {
super.onDestroy();
Debug.stopMethodTracing();
}
private void printNum() {
for (int i = 0; i < 20000; i++) {
print();
}
}
/**
* 模拟一个自身占用时间不长,但调用却非常频繁的函数
*/
private void print(){
count=count++;
}
/**
* 模拟一个调用次数不多,但每次调用却需要花费很长时间的函数
*/
private void calculate(){
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 1000; j++) {
for (int l = 0; l < 1000; l++) {
if(longCount>10){
longCount=-10;
}
}
}
}
Log.e("MainActivity",String.valueOf(longCount));
}
}
现在来分析一下采集到的数据。先看线程面板
线程面板在线程面板上发现我们应用中的三个线程,main线程,这个线程是都会有的,还有printNum_thread,calculate_thread两个线程。
再看时间线面板
时间线面板- 备注
时间线面板以每个线程为一行,右边是该线程在整个过程中方法执行的情况,一行中有很多的小色块。这些色块代表采集过程中方法调用时间线,相同的颜色代表相同的方法
,其中的每一个小色块就代表一次方法的调用,色块的长度代表方法执行时间的长短
,左边为第一个色块代表方法执行开始,最右边色块代表最后一个方法执行结束,有时候可以根据色块长度来做个大致判断,哪一个方法执行时间相对来说比较长,你可以把鼠标放到色块上,就会显示该方法调用的详细信息,你可以随意滑动你的鼠标,滑倒哪里,左上角就会显示该方法调用的信息,并且可以按住CTRL键加鼠标滚轮进行放大。
如下图。
方法调用信息比如我放大后,现在鼠标停在一个红色的方块上,这个红色的方块是在printNum_thread线程条上,左上角显示了这个色块代表的是MainActivity的printNum方法,在0.883的时候调用了这个方法,下面还有一些详细时间信息,下面细说。如果想回到最初的状态,双击时间线就可以。
重头戏来了:最后看一下数据分析面板,在数据分析面板,你可以点击某个函数展开更详细的信息
数据分析面板将数据分析面板某个函数展开后,大多数有以下两个类别:
- Parents:调用该方法的父类方法
- Children:该方法调用的子类方法
如果该方法含有递归调用,可能还会多出两个类别: - Parents while recursive:递归调用时所涉及的父类方法
- Children while recursive:递归调用时所涉及的子类方法
至于数据分析面板红色框中,各个字段的含义如下:
红色框中代表的含义对于这些字段,我们最需要关心的数据是:
Calls + Recur
和 Calls / Total
以及Cpu Time / Call
因为我们最关心的有两点,一是调用次数不多,但每次调用却需要花费很长时间的函数。这个可以从Cpu Time / Call
反映出来。另外一个是那些自身占用时间不长,但调用却非常频繁的函数。这个可以从Calls + Recur
和Calls / Total
反映出来。
点击Calls + Recur Calls这一栏,可以按照方法调用次数排序,如下图,可以看出print方法执行了2000次。
调用次数最多点击Cpu Time / Call这一栏,可以按照方法调用时间排序,如下图,
占用CPU时间最多可以看到calculate方法执行了13s多,非常的耗时。
这是模拟的两个极端的情况,实际情况下,分析的难度比较大,但是当体验卡顿的时候,我们可以借助TraceView来定位问题。所以TraceView虽说不常用,但是还是很有意义的!
总结
关于应用启动加速,一般从以下几个方面来入手:
- 利用主题快速显示界面;
- 异步初始化组件;
- 梳理业务逻辑,延迟初始化组件、操作;
- 去掉无用代码、重复逻辑等。
开发过程中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。
网友评论