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"。