Android应用性能优化最佳实践.2.5 启动优化-阿里云开发者社区

开发者社区> 华章出版社> 正文

Android应用性能优化最佳实践.2.5 启动优化

简介:

2.5 启动优化


随着应用的功能越来越丰富、启动时需要初始化的工作多、界面的元素复杂等,启动速度不可避免地受到影响,比如一开始单击时出现黑屏或者白屏,甚至在低端机型上出现假死的现象,本节通过学习应用的启动流程、启动速度的监控,发现影响启动速度的问题所在,并优化启动的逻辑,提高应用的启动速度。

2.5.1 应用启动流程

Android应用程序的载体是APK文件,其中包含了组件和资源,APK文件可能运行在一个独立的进程中,也有可能产生多个进程,还可以多个APK运行在同一个进程中,可以通过不同的方式来实现。但有两点需要注意,第一,每个应用只对应一个Application对象,并且启动应用一定会产生一个Application对象;第二,应用程序可视化组件Activity是应用的基本组成之一,因此要分析启动的性能,就有必要了解这两个对象的工作流程和生命周期。

1.?Application

Application是Android系统框架中的一个系统组件,Android程序启动时,系统会创建一个Application对象,用来存储系统的一些信息。Android系统会自动在每个程序运行时创建一个Application类的对象,并且只创建一个,可以理解为Application是一个单例类。

应用可以不指定一个具体的Application,系统会自动创建,但一般在开发中都会创建一个继承于系统Application的类实现一些功能,比如一些数据库的创建、模块的初始化等。但这个派生类必须在AndroidManifest.xml中定义好,在application标签增加name属性,并添加自己的Application的类名,代码如下:

<application

android:name=".GmfApplication"

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

AndroidManifest.xml中的application标签很多,这些标签的说明在官网有详细的介绍,这里就不做讲解。

启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。在Application创建时初始化全局变量,同一个应用的所有Activity都可以取到这些全局变量的值,Application对象的生命周期是整个程序中最长的,它的生命周期就等于这个应用程序的生命周期,因为它是全局的单例的,所以在不同的Activity或者Service中获得的对象都是同一个对象。因此在安卓中要避免使用静态变量来存储长久保存的值,可以用Application,但并不建议使用太多的全局变量。

AndroidManifest.xml文件上的application标签指定了重写的Application类后,看看该类可以重载的几个抽象接口,代码清单2-7是一个自定义的Application。

代码清单2-7 自定义的Application

    public class GmfApplication extends Application {

    private static Context mContext = null;

    @Override

    protected void attachBaseContext(Context base) {

        super.attachBaseContext(base);

        mContext = this;

    }

    @Override

    public void onCreate() {

        super.onCreate();

    }

    @Override

    public void onTerminate() {

        super.onTerminate();

    }

    @Override

    public void onConfigurationChanged(Configuration newConfig) {

        super.onConfigurationChanged(newConfig);

    }

    @Override

    public void onLowMemory() {

        super.onLowMemory();

    }

    @Override

    public void onTrimMemory(int level) {

        super.onTrimMemory(level);

    }

    public static Context getContext(){

        return mContext;

    }

}

从代码清单2-7中可以看到几个重要的抽象接口,这些接口的调用时机如下:

attachBaseContext(Context base):得到应用上下文的Context,在应用创建时首先调用。

onCreate():应用创建时调用,晚于attachBaseContext()方法。

onTerminate():应用结束时调用。

onConf?igurationChanged():系统配置发生变化时调用。

onLowMemory():系统低内存时调用。

onTrimMemory(int level):系统要求应用释放内存时调用,level为级别。

从上面的抽象方法可以看出,这些方法都在这个应用生命周期之中,attachBaseContext和onCreate在应用创建时必须调用,而其他需要满足一定的触发时机。

在开发过程中,尽量使用Application中的Context实例,因为使用Activity中的Context可能会导致内存泄漏。也可以使用Activity的getApplicationContext方法。

2.?Activity

Activity大家都非常熟悉了,这里也不做太多解释,只需要理解它的生命周期,因为这在启动优化的过程中非常重要。

在Activity的生命周期中,系统会按类似于阶梯金字塔的顺序调用一组核心的生命周期方法,如图2-37所示。也就是说,Activity生命周期的每个阶段就是金字塔上的一阶。当系统创建一个新Activity实例时,每个回调方法会将Activity状态向顶端移动一阶。金字塔顶端是Activity在前台运行并且用户可以与其交互的时间点。当用户开始离开Activity时,系统调用其他方法在金字塔中将Activity状态下移,从而销毁Activity。在有些情况下,Activity将只在金字塔中部分下移并等待(如当用户切换到其他应用时),Activity可从该点开始移回顶端(如果用户返回到该Activity),并在用户停止的位置继续。

 

图2-37 Activty生命周期金字塔模型

大多数应用包含若干不同的Activity,用户可通过这些Activity执行不同的操作。无论Activity是用户单击应用图标时创建的主Activity,还是应用在响应用户操作时开始的其他Activity,系统都会调用其onCreate()方法创建Activity的每个新实例。因此必须实现onCreate()方法,执行后,在Activity整个生命周期中只需要出现一次基本应用启动逻辑。例如,onCreate()的实现应定义用户界面并且可能实例化某些类范围变量、声明用户界面(在XML布局文件中定义)、定义成员变量,以及配置某些UI。

onCreate()方法包括一个savedInstanceState参数,在有关重新创建Activity中非常有用。

从Application和Activity的介绍中,可以总结出应用启动的流程,如图2-38所示。

其中,启动分为两种类型:冷启动和热启动。

冷启动:因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化Main-Activity类(包括一系列的测量、布局、绘制),最后显示在界面上,如图2-38所示。

热启动:因为会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化MainActivity(包括一系列的测量、布局、绘制),即Application只会初始化一次,只包含Activity中的生命周期流程。

2.5.2 启动耗时监测

因为一个应用在启动或者跳入某个页面时是否流畅,时间是否太长,仅仅通过肉眼来观察是非常不准确的,并且在不同设备和环境会有完全不同的表现,所以要准确知道耗时,就需要有效准确的数据,首先通过shell来获取启动耗时。

1.?adb shell am

应用启动的时间会受到很多因素的影响,比如首次安装后需要解压apk文件,绘制时GPU的耗时等,所以在应用层很难获取到启动耗时,但借助ADB可以得到准确的启动时间。

使用adb shell获得应用真实的启动时间,代码如下:

adb shell am start -W [packageName]/[packageName.AppstartActivity]

执行后可以得到三个时间:

ThisTime:一般和TotalTime时间一样,如果在应用启动时开了一个过度的全透明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime小。

TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。

WaitTime:一般比TotalTime大些,包括系统影响的耗时。

但这个方法只能得到固定的某一个阶段的耗时,不能得到具体哪个方法的耗时,下面介绍第二个方案:代码打点输出耗时。

2.?代码打点

通过代码打点来准确获取记录每个方法的执行时间,知道哪些地方耗时,然后再有针对性地优化,下面通过一个简单的例子来讲解打点的方案。

以下代码是一个统计耗时的数据结构,通过这个数据结构记录整个过程的耗时情况。

    public class TimeMonitor {

    private final String TAG = "TimeMonitor";

    private int monitorId = -1;

    // 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间

    private HashMap<String, Long> mTimeTag = new HashMap<String, Long>();

    private long mStartTime = 0;

 

    public TimeMonitor(int id) {

        GLog.d(TAG,"init TimeMonitor id:" + id);

        monitorId = id;

    }

 

    public int getMonitorId() {

        return monitorId;

    }

 

    public void startMoniter() {

        // 每次重新启动,都需要把前面的数据清除,避免统计到错误的数据

        if (mTimeTag.size() > 0) {

            mTimeTag.clear();

        }

        mStartTime = System.currentTimeMillis();

    }

 

    // 打一次点,tag交线需要统计的上层自定义

    public void recodingTimeTag(String tag) {

        // 检查是否保存过相同的tag

        if (mTimeTag.get(tag) != null) {

            mTimeTag.remove(tag);

        }

        long time = System.currentTimeMillis() - mStartTime;

        GLog.d(TAG, tag + ":" + time);

        mTimeTag.put(tag, time);

    }

    public void end(String tag,boolean writeLog){

        recodingTimeTag(tag);

        end(writeLog);

    }

    public void end(boolean writeLog) {

        if (writeLog) {

            // 写入到本地文件

        }

        testShowData();

    }

    public void testShowData(){

        if(mTimeTag.size() <= 0){

            GLog.e(TAG,"mTimeTag is empty!");

            return;

        }

        Iterator iterator = mTimeTag.keySet().iterator();

        while (iterator != null && iterator.hasNext()){

            String tag = (String)iterator.next();

            GLog.d(TAG,tag + ":" +  mTimeTag.get(tag));

        }

    }

    public HashMap<String, Long> getTimeTags() {

        return mTimeTag;

    }

}

这个对象可以用在很多需要统计的地方,不仅可以统计应用启动的耗时,还可以统计其他模块,如统计一个Activity的启动耗时和一个Fragment的启动耗时。流程为:在创建这个对象时,需要传入一个ID,这个ID是需要统计的模块或者一个生命周期流程的ID,ID自定义并且是唯一的,一个TimeMonitor对应一个ID。其中end(Boolean writeLog)方法表示这个监控的流程结束,其中writeLog表示是否需要写入本地,建议实现这个方法,可以统计一系列的数据,最好上传到服务器,用来监控这个应用在外网的实际启动状况。

上传到服务器时建议抽样上报,比如根据用户ID的尾号来抽样上报,虽然不影响性能,但还是尽量不要全部上报,用后台下发抽样比较好。

比如现在要统计启动应用在各阶段的耗时,就自定义一个ID,为了使代码更好管理,编写一个专门定义所有ID的类,方便以后的维护,代码如下:

    public class TimeMonitorConfig {

    // 应用启动耗时

    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;

}

因为耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据,这里使用了一个单例类来实现:TimeMonitorManager,代码如下:

public class TimeMonitorManager {

    private static TimeMonitorManager mTimeMonitorManager = null;

    private static Context mContext  = null;

    private HashMap<Integer,TimeMonitor> timeMonitorList = null;

    public synchronized static TimeMonitorManager getInstance(){

        if(mTimeMonitorManager == null){

            mTimeMonitorManager = new TimeMonitorManager();

        }

        return mTimeMonitorManager;

    }

    public TimeMonitorManager(){

        timeMonitorList = new HashMap<Integer,TimeMonitor>();

    }

    // 初始化某个打点模块

    public void resetTimeMonitor(int id){

        if(timeMonitorList.get(id) != null){

            timeMonitorList.remove(id);

        }

        getTimeMonitor(id);

    }

    // 获取打点器

    public TimeMonitor getTimeMonitor(int id){

        TimeMonitor monitor = timeMonitorList.get(id);

        if(monitor == null){

            monitor = new TimeMonitor(id);

            timeMonitorList.put(id,monitor);

        }

       return monitor;

    }

}

在有需要的地方通过这个方法进行打点,为了得到有效的数据,总结起来主要在两个方面需要打点:

应用程序的生命周期节点,如Application的onCreate、Activity或Fragment的回调函(onCreate、onResume等)。

启动时需要初始化的重要方法,如数据库初始化、读取本地的一些数据等。

其他耗时的一些算法。

例如,在启动时加入统计,在Application和第一个Activity加入打点统计,结合前面讲过的启动生命周期,首先进入的是Application的attachBaseContext()方法,然后在Oncreate结束时打第一个点,在AppstartActivity结束打第二个点,在AppstartActivity中的onStart()打最后一个点,代码如下:

Application:

@Override

protected void attachBaseContext(Context base) {

    super.attachBaseContext(base);      TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);

}

@Override

public void onCreate() {

    super.onCreate();

    InitModule(); TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("ApplicationCreate");

}

第一个Activity:

@Override

protected void onCreate(Bundle savedInstanceState) {

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_create");

    super.onCreate(savedInstanceState);

 

    setContentView(R.layout.activity_app_start);

    mLogo = (ImageView) this.findViewById(R.id.logo);

    // mStartHandler.sendEmptyMessageDelayed(0,1000);

    // useAnimation();

    useAnimator();

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_createOver");

}

 

    @Override

protected void onStart() {

    super.onStart();

    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("AppStartActivity_start",false);

}

结果如下所示:

 

可以在项目中核心基类的关键回调函数和核心方法加入打点,另外插桩也是一种不错的方式。

2.5.3 启动优化方案

在Android应用开发中,应用启动速度对用户体验非常重要,也是一个应用给用户的第一个性能方面的体验,因此应用启动优化是非常有必要的。应用启动优化的核心思想就是要快,在启动过程中做尽量少的事。但是应用功能越丰富,模块越多,需要初始化的地方也越多,导致了应用启动变慢。

为了优化启动的速度,首先要了解启动时做了什么,来看一个例子,启动源码中性能优化的启动应用(应用源码在http://github.com/lyc7898/AndroidTech),通过打点,统计从单击打开应用到首页显示完成的时间,后面章节会讲到打点的注意事项和具体实现。表2-4是通过打点获取到这个应用启动时,各个模块占用的时间。

因为这个应用比较简单,没有什么模块和数据,所以大头是在绘制工作上,也就是闪屏页(显示启动LOGO的页面)和进入后首页的布局上,其次是一些初始化工作,但实际上一个稍大型的应用,模块初始化和数据准备工作的占比会高很多,因为这块的优化也是非常有必要。

从总体上看,启动主要完成三件事:UI布局、绘制和数据准备,因此启动速度的优化就是需要优化这三个过程,我们也可以通过一些启动界面策略进行优化。接下来从启动耗时最高的UI布局和启动加载逻辑两个方向优化,达到降低启动耗时的目的。因为这个应用非常简单,所以不具有代码性,但优化的流程和方案是通用的。

1.?UI布局

这个应用启动过程为:启动应用→Application初始化→AppstartActivity→HomePageActivity。AppstartActivity是应用的闪屏页(也叫启动页),我们看到大部分应用都有这么一个页面,为什么要有闪屏页呢?闪屏页的存在主要有两个好处:一是可以作为品牌宣传展示,如节日运营或热点事件运营,也可以做广告展示(不要太低端);其二,因为闪屏一般需要停留一段时间,在这段时间可以做很多事情,比如底层模块的初始化、数据的预拉取等。

首先需要优化AppstartActivity的布局,从前面的章节可以知道,要提高显示的效率,一是减少布局层级,二是避免过度绘制,因为前面已经有很详细的例子了,这里不做过多介绍,优化的步骤如下:

使用Prof?ile GPU Rendering检查启动时是否有严重的掉帧,见2.2.1节。

使用Hierarchy View检查布局文件(XML)分析布局并优化,见2.3.1节。

2.?启动加载逻辑优化

一个应用越大,涉及的模块越多,包含的服务甚至进程就会越多,如网络模块的初始化、底层数据初始化等,这些加载都需要提前准备好,有些不必要的就不要放到应用中。可以用以下四个维度分整理启动的各个点:

必要且耗时:启动初始化,考虑用线程来初始化。

必要不耗时:首页绘制。

非必要耗时:数据上报、插件初始化。

非必要不耗时:不用想,这块直接去掉,在需要用的时再加载。

把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如图2-39所示。

 

图2-39 启动优化方向

要提高应用的启动速度,核心思想是在启动过程中少做事情,越少越好。

在应用中,增加启动默认图或者自定义一个Theme,在Activity首先使用一个默认的界面可以解决部分启动短暂黑屏问题,如android:theme="@style/Theme.AppStartLoad"。


版权声明:如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:developerteam@list.alibaba-inc.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接