Android:手把手教你如何优雅的实现APP启动速度优化

原文链接:https://www.jianshu.com/p/9772bfa87839

  随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关。应用的启动速度缓慢这是很多开发者都遇到的一个问题,比如启动缓慢导致的黑屏,白屏问题,大部分的答案都是做一个透明的主题,或者是做一个Splash界面,但是这并没有从根本上解决这个问题, 只是从视觉上让用户以为黑屏白屏问题得到了解决。那么如何从根本上解决这个问题或者做到一定程度的缓解?

应用的启动方式

应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!

1. 冷启动

冷启动指的是应用程序从头开始:系统的进程没有,直到此开始,创建了应用程序的进程。 在应用程序自设备启动以来第一次启动或系统杀死应用程序等情况下会发生冷启动。 这种类型的启动在最小化启动时间方面是最大的挑战,因为系统和应用程序比其他启动状态具有更多的工作。

2. 热启动

与冷启动相比,热启动应用程序要简单得多,开销更低。在热启动,系统会把你活动放到前台,如果所有应用程序的活动仍驻留在内存中,那么应用程序可以避免重复对象初始化,UI的布局和渲染。
热启动显示与冷启动场景相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。

3. 温启动

用户退出您的应用,但随后重新启动。该过程可能已继续运行,但应用程序必须通过调用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" />

<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中已经使用的错误,因此这种情况建议延迟到使用前再去初始化;

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

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">三方组件调用优化示例代码</figcaption>

3.启动加速之定位问题

启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。

如果对TraceView的使用不是很清楚的,可以查看下文TraceView的使用 - 数据采集与数据分析

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">优化之前应用启动trace文件分析图</figcaption>

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">优化之前应用启动trace文件分析图</figcaption>

左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。
注意两列:Real Time/Call(实际发生时间),Calls+RecurCalls/Total(发生次数);

  • 可以直观看到MainThread的时间轴很长,说明大多数任务都是在MainThread中执行;

  • 通过Real Time/Call 降序排列可以看到程序中的部分代码确实非常耗时;

  • 在下一页可以看出来部分三方SDK也比较耗时;

结合第二种实现方式,可以对项目中耗时严重的代码进行优化。通过以上三步及三方组件的优化:Application以及首屏Activity回调期间主线程就没有耗时、争抢资源等情况了。

TraceView的使用 - 数据采集与数据分析

TraceView是什么,TraceView 是 Android平台特有的数据采集和分析工具,主要用做热点分析,找出最需要优化的点。TraceView 从代码层面分析性能问题,针对每个方法来分析,比如当我们发现我们的应用出现卡顿的时候,我们可以来分析出现卡顿时在方法的调用上有没有很耗时的操作,通过TraceView,可以得到两种数据。

单次执行最耗时的方法

执行次数最多的方法


image.png

要打开上面的面板,代码中一般有两种方式:

第一种方式:

首先选择跟踪范围,在想要根据的代码片段之间使用以下两句代码

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
image.png

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));
}

}
现在来分析一下采集到的数据。先看线程面板


image.png

在线程面板上发现我们应用中的三个线程,main线程,这个线程是都会有的,还有printNum_thread,calculate_thread两个线程。

再看时间线面板
image.png

备注
时间线面板以每个线程为一行,右边是该线程在整个过程中方法执行的情况,一行中有很多的小色块。这些色块代表采集过程中方法调用时间线,相同的颜色代表相同的方法,其中的每一个小色块就代表一次方法的调用,色块的长度代表方法执行时间的长短,左边为第一个色块代表方法执行开始,最右边色块代表最后一个方法执行结束,有时候可以根据色块长度来做个大致判断,哪一个方法执行时间相对来说比较长,你可以把鼠标放到色块上,就会显示该方法调用的详细信息,你可以随意滑动你的鼠标,滑倒哪里,左上角就会显示该方法调用的信息,并且可以按住CTRL键加鼠标滚轮进行放大。

如下图。
image.png

比如我放大后,现在鼠标停在一个红色的方块上,这个红色的方块是在printNum_thread线程条上,左上角显示了这个色块代表的是MainActivity的printNum方法,在0.883的时候调用了这个方法,下面还有一些详细时间信息,下面细说。如果想回到最初的状态,双击时间线就可以。

重头戏来了:最后看一下数据分析面板,在数据分析面板,你可以点击某个函数展开更详细的信息

image.png

将数据分析面板某个函数展开后,大多数有以下两个类别:

Parents:调用该方法的父类方法

Children:该方法调用的子类方法
如果该方法含有递归调用,可能还会多出两个类别:

Parents while recursive:递归调用时所涉及的父类方法

Children while recursive:递归调用时所涉及的子类方法

至于数据分析面板红色框中,各个字段的含义如下:


image.png

红色框中代表的含义
对于这些字段,我们最需要关心的数据是:
Calls + Recur 和 Calls / Total以及Cpu Time / Call

因为我们最关心的有两点,一是调用次数不多,但每次调用却需要花费很长时间的函数。这个可以从Cpu Time / Call反映出来。另外一个是那些自身占用时间不长,但调用却非常频繁的函数。这个可以从Calls + Recur和Calls / Total反映出来。

点击Calls + Recur Calls这一栏,可以按照方法调用次数排序,如下图,可以看出print方法执行了2000次。

image.png

调用次数最多

点击Cpu Time / Call这一栏,可以按照方法调用时间排序,如下图,
image.png

占用CPU时间最多
可以看到calculate方法执行了13s多,非常的耗时。
这是模拟的两个极端的情况,实际情况下,分析的难度比较大,但是当体验卡顿的时候,我们可以借助TraceView来定位问题。所以TraceView虽说不常用,但是还是很有意义的!

总结关于应用启动加速,一般从以下几个方面来入手:

利用主题快速显示界面;

异步初始化组件;

梳理业务逻辑,延迟初始化组件、操作;

去掉无用代码、重复逻辑等。

开发过程中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。

一个来自火星有点厉害的攻城狮:有兴趣的加入Android工程师交流Q群:935654177 主要针对Android开发人员提升自己,突破瓶颈,相信你来交流,会有提升和收获。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容