App的启动可分为:冷启动、温启动和热启动。其中冷启动是App启动耗时最长的启动情况,今天我们来讲解启动速度的优化也是专门针对冷启动来进行。
当App启动时,后台没有该App对应的进程,比如手机启动后第一次运行App、系统内存吃紧引发App进程被Kill掉后再次启动又或者是用户主动结束App进程的情况。这种情况意味着系统需要创建一个新的进程分配给该App,App需要走初始化流程。冷启动是App启动耗时最长的启动情况。
App的进程是存活着,但是当前的Activity已不存在。比如因为某些原因App被切换到后台,而当时Activity又可能因为内存不足被回收的情况。这时启动App是不需要重新创建进程,但是Activity需要重新创建对象。
App的进程是存活着,而且当前的Activity对象仍然存在内存中没有被回收。不需要再重新创建Activity和初始化。比如按Home或任务键离开的App做其他事情,然后再快速回到刚才的App上的情况。
我们在前面文章《Android应用程序启动详解(一)之Android系统和Android应用的启动过程》了解到,一个App的启动从startActivity到ActivityThread.main()都是系统所做的事情,我们无法去干预,而在创建App的进程后,文章《Android应用程序启动详解(二)之Application和Activity的启动过程》中一些源码分析大概可以了解到,其过程就是创建Application对象和第一个Activity对象,然后做一些相应的初始化工作和回调生命周期方法的过程。而Activity起来后就是创建Window、加载View,完成第一次绘制。当App完成第一次绘制后,就会替切黑白背景,展示出与用户交互的界面。
所以我们要进行启动速度的优化,着手点就定位在Application初始化过程、Activity创建后到展示视图过程、以及View绘制过程,在这些过程中避免做密集沉重的事件。换句话说,就是要对Application的attachBaseContext、onCreate;Activity的onCreate、onStart、onResume;界面的布局做相应的优化。
一些数据的预加载尽可能放在异步线程中去,数据加载完成后如需要再通过callback回调告知处理。
延时加载是指主要的点先加载处理,其余非首次必要的点可使用postDelayed来将其延后加载,从而达到让用户快速看到主要功能的效果。
1 第三方SDK的初始化处理
很多时候,我们都会往Application的attachBaseContext或onCreate去加载一些第三方SDK,这也是增加Application启动时间的元凶,所以应该尽量避免在Application的attachBaseContext或onCreate中去做同步的初始化操作。比较好的解决方案是先分析SDK是否首次必要,如果不是那么就对其进行延时加载或在哪正真使用时才赖加载。
2 MultDex分包情况
在App功能日益复杂的今天,MultiDex几乎是已经无法避免,而在5.0之前的手机的启动速度简直是一场恶梦。为了启动速度的优化,可以在Gradle脚本配置分包时通过multiDexKeepProguard或multiDexKeepFile属性来进行配置指定的类留在主的dex文件中,也就是指定首次必要功能的类留在主的dex中,从而可以考虑将MultiDex.install(this); 放置在异步线程去执行,但是要处理好在加载完成前,保证classes2.dex里的类不会被提前调用到。关于MultiDex的multiDexKeepProguard或multiDexKeepFile属性使用,可以见前面文章《Android Gradle使用详解(六) 之 如何解决65535方法限制》。
3 使用ViewStub按需加载界面
有时候一些View在界面上不是一直要显示的,只是在特定的条件下才展示给用户,就像一个请求错误的View,在正常流程下是完全不会出,也就是完成没有必要让它去加载。所以在这种场景下就可以使用ViewStub来处理了。ViewStub它非常轻量级且宽高都是0,而且它本身不参与任何的布局和绘制过程,也就是意义上的按需加载所需的布局文件。
4 在第一帧显示后再执行逻辑
首屏的Activity尽量简化,或在绘制完第一帧后才开始做事情,如代码:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 第一帧显示后执行
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
mHandle().post(...);
}
});
}
}
这里要进行解说一下,getDecorView()返回的就是根视图DecorView对象,因为DecorView是继承于View,所以这里的post方法会调用到View的Post方法中去,因为要在onResume中View才会被添加到Window去,所以这里的Post方法只是把 Runnable 封装成一个 HandlerAction 对象存入到 ArrayList 中,当执行到 executeActions 方法(executeActions方法是在performTraversals方法内执行,View的measure / layout / draw过程,每一帧的绘制都是走performTraversals方法)的时候,将存在这里的 HandlerAction 再通过 executeActions 方法传入的 Handler 对象重新进行 Post。所以这Post的runnable对象,最终是在第一个 performTraversals 方法执行的时候,加入到了 MainLooper 的 MessageQueue 里面了。
关于布局的优化,我们在前面文章《Andorid性能优化(一) 之 如何给App进行内存优化》中的“界面布局优化”已经介绍过,这里就不作重复说明了。
出现黑白屏的原因其实是Activity在启动时,windowbackground会比setContentView先加载,系统会默认设置windowbackground为白色或黑色,当App启动过慢就会看到黑白屏的现象。所以在这情况下,想要消除黑白屏可以在App启动时增加轻量级的Splash页面。消除黑白屏并不是速度优化的方案,但是它可以改善用户体验。
从Android 4.4版本开始,Logcat中会输出每个Activity显示到界面上所花费的时间。这个方法比较适合测量程序的启动时间。如图:
我们只要在要分析耗时的代码段中加入startMethodTracing开始和stopMethodTracing结束就通过生成的trace文件分析过程中每个方法的耗时情况,使用如:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Debug.startMethodTracing("test");
test();
test2();
Debug.stopMethodTracing();
}
private void test() {
try {
Thread.sleep(4);
} catch ( Exception ex) {
ex.printStackTrace();
}
}
private void test2() {
try {
Thread.sleep(3);
} catch ( Exception ex) {
ex.printStackTrace();
}
}
}
就这样在程序运行后,便会在Android/data/app包名/files目录下生成一个test.trace文件,通过Android Studio打开该trace文件便可以分析代码过程中每个方法所使用的时间,如下图:
使用adb命令:adb shell am start -W packageName/packageName.activity,便可以启动App的指定Activity,而且可以查看其启动时间,如:
WaitTime 表示总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。
Android 5.0 之前的手机是没有 WaitTime 这个值的,一般我们只要关心 TotalTime 即可,这个时间才是应用自己真正启动的耗时。