应用启动优化

基础知识

Understanding App Launch Time

Launch-Time Performance

启动方式

安卓应用的启动方式分为三种:冷启动、暖启动、热启动,不同的启动方式决定了应用UI对用户可见所需要花费的时间长短。顾名思义,冷启动消耗的时间最长。基于冷启动方式的优化工作也是最考验产品用户体验的地方。谈及优化之前,我们先看看这三种启动方式的应用场景,以及启动过程中系统都做了些什么工作。

冷启动 (Cold start)

在安卓系统中,系统为每个运行的应用至少分配一个进程 (多进程应用申请多个进程) 。从进程角度上讲,冷启动就是在启动应用前,系统中没有该应用的人和进程信息 (包括 Activity、Service 等) 。所以,冷启动产生的场景就很容易理解了,比如设备开机后应用的第一次启动,系统杀掉应用进程 (如:系统内存吃紧引发的 kill 和 用户主动产生的 kill) 后 的再次启动等。那么自然这种方式下,应用的启动时间最长,因为相比另外两种启动方式,系统和我们的应用要做的工作最多。

应用发生冷启动时,系统有三件任务要做:

  1. 开始加载并启动应用;

  2. 应用启动后,显示一个空白的启动窗口

  3. 创建应用进程信息;

系统创建应用进程后,应用就要做下面这些事情:

  1. 初始化应用中的对象 (比如 Application 中的工作);

  2. 启动主线程 (UI 线程) ;

  3. 创建第一个 Activity;

  4. 加载内容视图 (Inflating) ;

  5. 计算视图在屏幕上的位置排版 (Laying out);

  6. 绘制视图 (draw)。

一旦app进程完成了第一次绘制,系统进程就会用main activity替换已经展示的background window。之后用户才可以使用app。

下图展示了系统和app进程互相如何工作的,展示了app启动时期的几个重要部分,在创建app和main activity之间,我们可以提升性能问题

这其中有两个 creation 工作,分别为 Application 和 Activity creation。从图中看出,他们均在 View 绘制展示之前。所以,在应用自定义的 Application 类和 第一个 Activity 类中,onCreate() 方法做的事情越多,冷启动消耗的时间越长。

Application的创建

当应用启动的时候,空白的window在app第一次完成绘制之前都会存在。在那之后,系统进程才会替换启动窗口,允许用户开始和app交互。

如果你复写了 Application.oncreate() 方法,app启动的时候,会调用该方法。之后,app会孵化主线程(UI线程),并通过它来创建main activity。

从这之后,系统和app级别的进程将会按照app lifecycle stages 执行。

Activity的创建

在app进程创建了Activity之后,Activity将会执行以下操作

  1. 初始化值

  2. 调用构造函数

  3. 调用毁掉方法,比如Activity.onCreate()。

通常,onCreate方法会对加载时间有比较大的影响。因为它将执行繁重的工作:加载和填充view,并初始化Activity运行期间需要用的对象。

热启动

相对于冷启动,热启动会简单的多。如果app的所有Activities还存在内存中,那么系统需要做的就是将activity切换到前台。这样app会避免进行的对象初始化,布局填充和渲染

但是,如果一些内存在触发内存回调方法的时候被回收了,比如onTrimMemory(),那么这些对象就需要重新创建。

热启动会和冷启动有相同的行为。系统也会展示一个空白的window,直到app完成Activity的渲染

温启动

温启动做的工作介于冷热启动之间。这里列举几种可能被认为是温启动的状态:

  1. 用户离开了app,然后重新启动它。这时进程还在继续运行,但是Activity被回收了,app需要重新创建activity。

  2. 系统将你的app回收了,然后用户重新启动app。进程和Activity都需要重新启动,但它们可以从onCreate方法保存的bundle中恢复

优化启动性能

为了确定启动时间的性能问题,我们需要先确定app启动花费了多少时间。

Time to initial display

从4.4(API 19)开始,logcat会输出带有Displayed的log。该值代表从app启动进程到完成Activity第一次绘制的时间。该时间内完成了一下流程:

  • 启动进程

  • 初始化对象

  • 创建和初始化Activity

  • 填充布局

  • 第一次绘制app

打出的log如下:

03-20 16:23:14.732 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.A_StartActivity: +5s154ms
03-20 16:23:20.356 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.main.MainActivity: +2s580ms
03-20 16:23:33.932 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.A_StartActivity: +542ms
03-20 16:23:38.508 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.main.MainActivity: +748ms
03-20 16:25:51.100 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.A_StartActivity: +277ms
03-20 16:25:55.862 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.main.MainActivity: +813ms
03-20 16:26:06.720 878-914/? I/ActivityManager: Displayed com.medlighter.medicalimaging/.activity.A_StartActivity: +204ms

Displayed值并没有捕获所有资源都被加载和展示的总时间。那些不在layout文件中或者创建app初始化所需要对象的时间不包含在内。因为这些资源是在一个内部进程中加载的,并且不会阻塞app的初始化展示。

Time to full display

你可以调用reportFullyDrawn())方法去测量从应用启动到所有资源和view层级都被绘制出来的时间。这对于app执行懒加载的情况很有用。在懒加载中,app不会阻塞window的初始化绘制,但同步进行资源加载和view的更新会阻塞。

由于懒加载,app的初始化展示不会包含所有的资源。你可以考虑完全加载并展示所有资源和view的时候作为一个考量。比如,UI可能完全加载了,包括一些text的绘制,但是由于图片需要从网络获取,这时还没有展示。

为了处理这种情况,你可以手动地调用reportFullyDrawn方法让系统知道你的activity已经通过懒加载完成了。但你是用该方法的时候,logcat展示的时间就包含从应用被创建到reportFullyDrawn方法被调用的时间。

定位瓶颈

两种方式可以帮助你定位问题:AndroidStudio中的Method Tracer和内嵌tracing代码的方式。更多可以参考documentation.

如果无法使用Method Tracer Tool ,或者觉得trace的时机不够准确,那么你可以通过在app和Activity的onCreate方法中嵌入代码进行追踪,比如写下追踪代码。更多信息,可以参考Trace 、Systrace

常见问题

繁重的App初始化

http://www.lightskystreet.com/2016/10/15/android-optimize-start/

测量应用的启动时间

使用命令行来启动app,同时进行时间测量。单位:毫秒

    adb shell am start -W [PackageName]/[PackageName.MainActivity]
    adb shell am start -W com.medlighter.medicalimaging/.activity.A_StartActivity

就是用命令启动Activity。

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.medlighter.medicalimaging/.activity.A_StartActivity }
Status: ok
Activity: com.medlighter.medicalimaging/.activity.A_StartActivity
ThisTime: 204
TotalTime: 204
WaitTime: 265
Complete

ThisTime: 165 指当前指定的MainActivity的启动时间 TotalTime: 165 整个应用的启动时间,Application+Activity的使用的时间。 WaitTime: 175 包括系统的影响时间---比较上面大

这样,我们分析一下,热启动和冷启动的一个时间对比:

Activity: com.medlighter.medicalimaging/.activity.A_StartActivity
ThisTime: 2572
TotalTime: 2572
WaitTime: 2583


Activity: com.medlighter.medicalimaging/.activity.A_StartActivity
ThisTime: 204
TotalTime: 204
WaitTime: 265

你可以看到,差别简直非常大。

应用启动的流程

main()->Application:attachBaseContext()->onCreate()->Activity:onCreate()->onStart()->onResume()->performTraversals(layout mesure)->performTraversals(draw绘制)(显示第一帧)

减少应用的启动时间的耗时

  1. 不要在Application的attachBaseContext()、onCreate()里面进行初始化耗时操作。

  2. MainActivity,由于用户只关心最后的显示的这一帧,对我们的布局的层次要求要减少,自定义控件的话测量、布局、绘制的时间。不要在onCreate、onStart、onResume当中做耗时操作。由于在获取到第一帧前,需要对contentView进行测量布局绘制操作,尽量减少布局的层次,考虑StubView的延迟加载策略

  3. 对于SharedPreference的初始化。因为他初始化的时候是需要将数据全部读取出来放到内存当中。所以,其实SharedPreference要慎用,因为它虽然快,但是,它是全部加载到内存中的,所以才快,什么时间加载内存中,第一次访问它的时侯

    优化1:可以尽可能减少sp文件数量(IO需要时间);2.像这样的初始化最好放到线程里面;3.大的数据缓存到数据库里面。

其实对于SharedPreference而言,建议不要在Application或者在启动的Activity就去初始化SharedPreference,因为需要将数据全部读取出来放到内存当中,其它用到的时间再初始化即可

核心思路是不要卡主线程。

采用Splash

这个也是最最常用的方案。

app启动的耗时主要是在:Application初始化 + MainActivity的界面加载绘制时间。

由于MainActivity的业务和布局复杂度非常高,甚至该界面必须要有一些初始化的数据才能显示。 那么这个时候MainActivity就可能半天都出不来,这就给用户感觉app太卡了。

我们要做的就是给用户赶紧利落的体验。点击app就立马弹出我们的界面。 于是乎想到使用SplashActivity--非常简单的一个欢迎页面上面都不干就只显示一个图片。

但是SplashActivity启动之后,还是需要跳到MainActivity。MainActivity还是需要从头开始加载布局和数据。 想到SplashActivity里面可以去做一些MainActivity的数据的预加载。然后需要通过意图传到MainActivity

可不可以再做一些更好的优化呢? 耗时的问题:Application+Activity的启动及资源加载时间;预加载的数据花的时间。

如果我们能让这两个时间重叠在一个时间段内并发地做这两个事情就省时间了。

将SplashActivity和MainActivity合并

因为耗时时间可能是必须的,即使你在前面加了一层Splash,还是要启动Activity。所以合并。

一进来还是现实的MainActivity,SplashActivity可以变成一个SplashFragment,然后放一个FrameLayout作为根布局直接现实SplashFragment界面。SplashFragment里面非常之简单,就是现实一个图片,启动非常快。当SplashFragment显示完毕后再将它remove。同时在splash的2S的友好时间内进行网络数据缓存。这个时候我们才看到MainActivity,就不必再去等待网络数据返回了。

问题:SplashView和ContentView加载放到一起来做了 ,这可能会影响应用的启动时间。 解决:可以使用ViewStub延迟加载MainActivity当中的View来达到减轻这个影响。

ViewStub的设计就是为了防止Activity的启动加载资源太耗时了。延迟进行加载,不影响启动,用户友好。但是viewStub加载也需要时间。等到主界面出来以后。viewStub.inflate(xxxx);

关于应用DelayLoad延迟加载的几种方式

1、View.postDelayed(); 2、Handler.postDelayed(); 3、getWindow().getDecorView().post(){Handler.postDelay()}

而前两种方式虽然都有Delay效果,但并不是真正Delay了我们设置的时间,而第三种方式才是正确的Delay了我们想要的时间,原因如下: 关于Activity界面的启动,首先是在onCreate()方法中会对contentView、DecorView和ActionBar等进行初始化,比如:contentView进行inflate,我们便可以在这个方法中通过findViewById来找到对应的控件了,但是此时我们只是把那些对应的控件创建了一个对象而已,它们并没有add在界面上。而真正把contentView 添加到界面上的操作是在OnResume()方法执行时候,包括ViewRootImpl的初始化。而contentView的绘制会在onResume()方法执行完后的二次performTraversals()方法进行绘制

所以要想达到DelayLoad懒加载效果,是在所有的View绘制完成后进行Delay效果,而上面的第一第二种方式都是在第一次performTraversals()后执行,该次只是为第二次调用performTraversals()方法做一些准备工作,这样就达不到准确的Delay效果了,因为第二次的performTraversals()就是真正的进行View的测量布局和绘制了,这肯定是需要时间的,所以第一和第二种方式Delay的时间是需要再减去这个View绘制的时间的

如果是想在界面刚绘制完成后做一些事情,或者有些事情必须在UI绘制完成显示后做的话,那可以通过这个方法:

 getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //do something
                    }
                });
            }
        });


    getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //do something
                    }
                },3000);
            }
        });

之前有把代码写在onPostCreate什么地方,这其实都没有那么好。这个才是所有界面UI绘制结束的时机。

 private Handler mHandler = new Handler();
//...
    final SplashFragment splashFragment = new SplashFragment();
    final FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(R.id.frame, splashFragment);
    transaction.commit();   
//...
    mHandler.postDelayed(new DelayRunnable(this, splashFragment, mProgressBar), 2500);
//...
    static class DelayRunnable implements Runnable {
        private WeakReference<Context> contextRef;
        private WeakReference<SplashFragment> fragmentRef;
        private WeakReference<ProgressBar> progressBarRef;

        public DelayRunnable(Context context, SplashFragment splashFragment, ProgressBar progressBar) {
            contextRef = new WeakReference<Context>(context);
            fragmentRef = new WeakReference<SplashFragment>(splashFragment);
            progressBarRef = new WeakReference<ProgressBar>(progressBar);
        }

        @Override
        public void run() {
            ProgressBar progressBar = progressBarRef.get();
            if (progressBar != null)
                progressBar.setVisibility(View.GONE);
            Activity context = (Activity) contextRef.get();
            if (context != null) {
                SplashFragment splashFragment = fragmentRef.get();
                if (splashFragment == null)
                    return;
                final FragmentTransaction transaction = context.getFragmentManager().beginTransaction();
                transaction.remove(splashFragment);
                transaction.commit();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

Android性能优化之Splash页应该这样设计

Last updated