App启动速度优化

一: App启动方式

通常来说, 一个App启动会分如下不同的状态:

    1.  冷启动
    App没有启动过或App进程被killed, 总之是系统中不存在该App进程, 此时启动App即为冷启动。

  • 冷启动的流程即是App启动流程的全过程, 需要创建App进程, 加载相关资源, 启动Main Thread, 初始化首屏Activity等.

  • 在这个过程中, 屏幕会显示一个空白的窗口(颜色基于主题), 直至首屏Activity完全启动.

  • 下图展示了冷启动的时间线:

     

    App启动速度优化_第1张图片

    2. 热启动

  • 当启动应用时,后台已有该应用的进程,比如按下home键,这种在已有进程的情况下,这种启动会从已有的进程中来启动应用,这种启动方式叫热启动, 系统只是将其从后台带到前台, 展示给用户.

    类同与冷启动, 在这个过程中, 屏幕会显示一个空白的窗口(颜色基于主题), 直至activity渲染完毕.

  • 温启动
    介于冷启动和热启动之间, 一般来说在以下两种情况下发生:

    • 用户back退出了App, 然后又启动. App进程可能还在运行, 但是activity需要重建.
    • 用户退出App后, 系统可能由于内存原因将App杀死, 进程和activity都需要重启, 但是可以在onCreate中将被动杀死锁保存的状态(saved instance state)恢复.

通过三种启动状态的相关描述, 可以看出我们要做的启动优化其实就是针对冷启动. 热启动和温启动都相对较快.

二. 哪些地方是App快速启动的敌人

根据冷启动的时间图, 可以看出, 对于App来说, 我们可以控制的启动时间线的点无外乎:

  • Application的onCreate
  • 首屏Activity的渲染

而我们现在的App动不动集成了很多第三方服务, 启动时需要检查广告, 注册状态等等一系列接口都是在Application的onCreate或是首屏的onCreate中做的.

三:分析工具Traceview

3.1 Traceview介绍

Traceview是一个性能分析工具, 主要是分析当前线程情况, 各个方法执行时间等. 如下:

App启动速度优化_第2张图片

traceview

指标说明:

  • Incl(Inclusive) Cpu Time
    方法本身和其调用的所有子方法占用CPU时间.
  • Excl(Exclusive) Cpu Time
    方法本身占用CPU时间.
  • Incl Real Time
    方法(包含子方法)开始到结束用时.
  • Excl Real Time
    方法本身开始到结束用时.
  • Call + Recursion Calls/Total
    方法被调用次数 + 方法被递归调用次数.
  • Cpu Time/Call
    方法调用一次占用CPU时间.
  • Real Time/Call
    方法调用一次实际执行时间.

一般来说, 我们使用Real Time/Call排序来找出耗时多的方法

有必要解释下CPU Time和Real Time:
CPU Time 方法实际执行时间(不包括io等待时间)
Real Time 方法开始结束时间差(包括等待时间)

3.2 Traceview使用

有两种方式来使用Traceview:
1. 通过DDMS:

App启动速度优化_第3张图片

start traceview

这种方式启动再结束后界面会自动弹出分析界面

点击开始时会弹出一个选择trace模式的框, 默认选中"Sample based profiling"即可:

App启动速度优化_第4张图片

traceview option

  • Sample based profiling(基于样本分析)
    根据采样时间间隔来规律的打断VM来记录方法调用栈(Call Stack), 开销和采样频率成比例.

  • Trace based profiling(基于完整trace数据分析)
    记录每个方法的出入口, 每个方法执行时都开启记录, 无论多小的方法, 因此开销很大.

2. 使用代码:

// 在自己想要开始调试的地方start
Debug.startMethodTracing("GithubApp");

// 在合适的地方stop
Debug.stopMethodTracing();

注: 以上方法开启trace的方式相当于"Trace based profiling", 会记录每个方法的执行. Android 4.4及以上可以调用startMethodTracingSampling()来用代码开启"Sample based profiling"的trace方式.

再需要统计时间的代码前后添加上面的代码

运行程序, 会在sdcard上生成一个"GithubApp.trace"的文件.

注意: 需要给程序加上写存储的权限:


通过adb pull将其导出到本地

adb pull /sdcard/GithubApp.trace ~/temp

打开DDMS分析trace文件

ddms_open_trace

分析trace文件

App启动速度优化_第5张图片

traceview_ui

  1. 在下方的方法区点击"Real Time/Call", 按照方法每次调用耗时降序排.
  2. 耗时超过500ms都是值得注意的.
  3. 点击每个方法, 可以看到其父方法(调用它的)和它的所有子方法(它调用的).
  4. 点击方法时, 上方的该方法执行时间轴会闪动, 可以看该方法的执行线程及相对时长.

四: 应用的启动时间统计

adb shell am start -W PackageName/PackageName.MainActivity

执行成功后将返回三个测量到的时间:


1、ThisTime:一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。 
2、TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。 
3、WaitTime:一般比TotalTime大点,包括系统影响的耗时。

五: 代码分析

因为这个App集成了Bugly, Push, Feedback等服务, 所以Application的onCreate有很多第三方平台的初始化工作...

public class GithubApplication extends MultiDexApplication {

    @Override
    public void onCreate() {
        super.onCreate();

        // init logger.
        AppLog.init();

        // init crash helper
        CrashHelper.init(this);

        // init Push
        PushPlatform.init(this);

        // init Feedback
        FeedbackPlatform.init(this);

        // init Share
        SharePlatform.init(this);

        // init Drawer image loader
        DrawerImageLoader.init(new AbstractDrawerImageLoader() {
            @Override
            public void set(ImageView imageView, Uri uri, Drawable placeholder) {
                ImageLoader.loadWithCircle(GithubApplication.this, uri, imageView);
            }
        });
    }
}

当前冷启动效果:

 

App启动速度优化_第6张图片

code_start_before_optimize

可以看到启动时白屏了很长时间.

通过在onCreate方法前后添加日志,用traceview进行分析启动速度

看左边的方法名, 可以看到耗时大户就是我们调用的第三方SDK的初始化方法, 特别是Bugly, 还加载native的lib, 用ZipFile操作等.

调整Application onCreate再试

既然已经知道了哪些地方耗时长, 我们不妨调整下Application的onCreate实现, 一般来说我们可以将这些初始化放在一个单独的线程中处理, 为了方便今后管理, 这里我用了一个InitializeService的IntentService来做初始化工作.

明确一点, IntentService不同于Service, 它是工作在后台线程的.

InitializeService.java代码如下:

package com.anly.githubapp.compz.service;

import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.ImageView;

import com.anly.githubapp.common.wrapper.AppLog;
import com.anly.githubapp.common.wrapper.CrashHelper;
import com.anly.githubapp.common.wrapper.FeedbackPlatform;
import com.anly.githubapp.common.wrapper.ImageLoader;
import com.anly.githubapp.common.wrapper.PushPlatform;
import com.anly.githubapp.common.wrapper.SharePlatform;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader;

/**
 * Created by mingjun on 16/8/25.
 */
public class InitializeService extends IntentService {

    private static final String ACTION_INIT_WHEN_APP_CREATE = "com.anly.githubapp.service.action.INIT";

    public InitializeService() {
        super("InitializeService");
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, InitializeService.class);
        intent.setAction(ACTION_INIT_WHEN_APP_CREATE);
        context.startService(intent);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent != null) {
            final String action = intent.getAction();
            if (ACTION_INIT_WHEN_APP_CREATE.equals(action)) {
                performInit();
            }
        }
    }

    private void performInit() {
        AppLog.d("performInit begin:" + System.currentTimeMillis());

        // init Drawer image loader
        DrawerImageLoader.init(new AbstractDrawerImageLoader() {
            @Override
            public void set(ImageView imageView, Uri uri, Drawable placeholder) {
                ImageLoader.loadWithCircle(getApplicationContext(), uri, imageView);
            }
        });

        // init crash helper
        CrashHelper.init(this.getApplicationContext());

        // init Push
        PushPlatform.init(this.getApplicationContext());

        // init Feedback
        FeedbackPlatform.init(this.getApplication());

        // init Share
        SharePlatform.init(this.getApplicationContext());

        AppLog.d("performInit end:" + System.currentTimeMillis());
    }
}

GithubApplication的onCreate改成:

public class GithubApplication extends MultiDexApplication {

    @Override
    public void onCreate() {
        super.onCreate();

        // init logger.
        AppLog.init();

        InitializeService.start(this);
    }
}

看看现在的效果:

App启动速度优化_第7张图片

可以看到提升了很多, 但是启动的时候会有一个白屏, 如果手机较慢的话, 这个白屏就会持续一段时间

给我们的应用窗口弄一个PlaceHolder

Android最新的Material Design有这么个建议的. 建议我们使用一个placeholder UI来展示给用户直至App加载完毕.

怎么做呢?

给Window加上背景

 当App没有完全起来时, 屏幕会一直显示一块空白的窗口(一般来说是黑屏或者白屏, 根据App主题).

这个空白的窗口展示跟主题相关, 那么我们是不是可以从首屏的主题入手呢? 恰好有一个windowBackground的主题属性, 我们来给Splash界面加上一个主题, 带上我们想要展示的背景.

做一个logo_splash的背景:



    
    
    
    
    
        
    

弄一个主题:


将一个什么不渲染布局的Activity作为启动屏

写一个什么都不做的LogoSplashActivity.

public class LogoSplashActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 注意, 这里并没有setContentView, 单纯只是用来跳转到相应的Activity.
        // 目的是减少首屏渲染
        
        if (AppPref.isFirstRunning(this)) {
            IntroduceActivity.launch(this);
        }
        else {
            MainActivity.launch(this);
        }
        finish();
    }
}

在AndroidManifest.xml中设置其为启动屏, 并加上主题:


  
      
      
  

5.最终的效果

让我们来看下最终的效果:

App启动速度优化_第8张图片

 

相比之前, 呈现给用户的不再是一个白屏了, 带上了logo, 当然这个背景要显示什么, 我们可以根据实际情况来自定义.

这种优化, 对于有些Application内的初始化工作不能移到子线程做的情况, 是非常友好的. 可以避免我们的App长时间的呈现给用户一个空白的窗口.

 

你可能感兴趣的:(Android性能优化)