以前做手机的时候,我非常重视app的性能优化。其实一直以来,在工作中我总会去强调性能优化的重要性。但是,很多时候,由于一些外界因素,我们对app的一些性能指标不会那么重视。但是,性能优化依然是做好一个产品的重中之重。试想一下,如果用户费了很多时间和流量下载了我们的app,当人家安装好启动app时,却发现我们的app点了之后,很长时间没反应。那如果我是用户,我会二话不说卸载掉。因此,app的性能优化还是很重要且很有必要的,我接下来会总结一下Android性能优化的一些相关技术和知识,这篇博客主要总结一下启动优化。
一、前言在我总结启动优化之前,我先说些题外话。可能有很多朋友,工作很多年了,也没接触过或者没有实际做过性能优化。我说这个,并不是想展示自己多牛逼,相反,恰恰是因为自己初入职场时太菜。我自己接触性能优化算是职业生涯中比较早的时候吧,而这竟完全是因为自己第一份工作时,写出了非常烂的代码。校招入职后一个多月,公司6个月的培养计划我早就提前执行完了,信心满满的我,主动找主管要了一个开发工作,是基于某公司的算法库实现某种照片美化的功能。当然,那时候还没有正式排期,只是我先拿到算法库开始集成。JNI,NDK,查一些资料,最后搭建好了NDK开发环境。用了没几天时间,开发好了。拿去给我主管看,我主管看后说,你这速度太慢了,掐着秒表给我计算,足足7S!!!我当时不解了,做功能不就是把功能做出来就好了吗,什么是性能,不知道啊。我主管就让我去尝试优化一下,看看能不能优化到1S以内。我当时一听,7S到1S,虽然我数学不好,但是听到这个,我心里还是咯噔了一下,这怎么可能,这已经是动用了我所有的能力了。好,也就是从那时候开始,正式接触性能优化,这也伴随了我很长一段时间的职业生涯。
二、app启动首先,我们引用一下谷歌给出的app启动的三种方式:冷启动,热启动,温启动。啥?app还有这么多启动方式,冷,热,温,难道是跟启动app时的环境温度有关?当然了,这么说我觉得也是说的过去的。只不过,这个环境,不是我们理解的室内外环境,而是系统环境。
1、冷启动什么是冷启动,就是在系统中不存在当前app进程的情况下,点击app图标启动app。比如初次安装完app启动app或者清除app数据后启动app,这样app的启动需要经过两个步骤:(1)application的创建(2)activity生命周期。在当前系统中不存在任何该app的进程实例,不存在任何的activity实例,所以说,当前的系统环境是“冷”的,这样的app启动速度也是最慢的。
2、温启动温启动,其启动速度是介于冷启动和热启动之间的。温启动,就是说在application存在的情况下去启动app,这样只会走activity的生命周期,也就是冷启动的第二阶段。例如:某些手机系统的app,点击系统返回键退出app,再重新启动app。这种时候,app的进程还是存在的,只执行activity的生命周期。所以说,当前的系统环境是“温”的,因为进程还在。
3、热启动毫无疑问,这是启动最快的了。热启动,就是在application和activity都存在的情况下启动app,这样只会走activity生命周期的一部分。例如,最常见的就是点击系统home键或者recent键后再次进入app,其实就是前后台的切换。所以说呢,当前的系统环境是热的,因为我的进程和activity都在。
三、优化方向在说优化防线之前,再详细说一下冷启动的过程。其实,我们上面说要经过两个步骤,有点不太准确。在创建application之前,系统还会做一些准备工作。
(1)创建application前,系统会做一些准备工作,具体如下:启动app——>创建空白Window——>创建进程。
(2)创建了application后,接下来的一系列流程如下:创建进程——>启动主线程——>启动Activity
(3)启动Activity后,我们就知道基本的流程了,那就是执行Activity生命周期,在各个生命周期中加载布局,展示布局。
通过上面对冷启动流程的总结,毫无疑问,我们优化的方向,就是针对application和Activity来进行,更详细点,就是针对application和activity的生命周期来进行。
四、启动时间的测量 1、adb命令使用如下adb命令可以获取app启动的时间:adb shell am start -W [package]/[.MainActivity]。例如:
adb shell am start -W com.example.tuduoptimize/.MainActivity
使用上述命令,启动我的tuduoptimize项目,打印出的信息如下:
(1)ThisTime:是打开最后一个Activity的时间。
(2)TotalTime:是打开所有Activity的时间。
(3)WaitTime:AMS启动activity的总耗时。
2、打印activity启动时间这种方式,其实就是在我们认为开始启动的时间点打印当前系统时间,在我们认为启动完成后的地方打印当前系统时间,取二者的和,得到启动耗时。
对于上面打印系统时间的方法,写一个工具类,方便我们打印:
package com.example.tuduoptimize;
import android.util.Log;
public class LunchTimeUtil {
private static final String TAG = "LunchTimeUtil";
private static long startTime;
public static void startRecord() {
startTime = System.currentTimeMillis();
}
public static void endRecord(String msg) {
long costTime = System.currentTimeMillis() - startTime;
Log.d(TAG, "CostTime:" + costTime + "msg:" + msg);
}
}
(1)开始启动
开始启动的时间,我们一般放在Application的attachBaseContext方法中。
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LunchTimeUtil.startRecord();
}
(2)结束启动
这个结束启动的时间,我们需要根据我们具体的项目来确定。例如我的空项目,只是加载了一个布局,那么我放在onWindowsFocusChanged()中。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
LunchTimeUtil.endRecord("onWindowFocusChanged");
}
五、性能优化工具TraceView
进行性能优化,需要借助一些工具,帮助我们分析app启动耗时以及耗时的地方。相信大家都或多或少的用过或者听说过一些性能优化工具。在这里呢,我介绍一下TraceView的使用。
在使用TraceView分析trace文件之前,我们首先得得到一个trace文件。那么如何生成trace文件呢?其实很简单,我们在我们需要生成trace文件的方法中,简单的两行代码即可生成trace文件。例如我在Application的onCreate中,分析initSdk这个方法:
@Override
public void onCreate() {
super.onCreate();
Debug.startMethodTracing("tudu");
initSdk();
Debug.stopMethodTracing();
}
写好上述两行代码后,运行我们的app,即可生成trace文件。生成文件的路径:Android/data/packagename/file,记得在运行后刷新一下:
双击打开trace文件,进入TraceView主界面:
(1)最上面浅蓝色区域:就是我们选取抓trace文件的起止时间段
(2)THREADS:显示当前所有的线程,后面的长条是耗时
(3)下面的四个tab,可以显示方法及所耗时间,不同的tab有不同的作用,主要介绍Call Chart和Top Down:
Call Chart:
大家可以发现颜色不一样,这个颜色是有讲究的:绿色的就是我们自己写的方法,蓝色的是系统的方法。
Top Down:
在我的测试app中,我写了四个方法模拟sdk初始化(均让主线程休眠一段时间),这四个方法又放在了initSdk中,通过这个tab,我们可以很清晰的看到各个方法的耗时。
六、启动优化在这个章节中,介绍几种启动优化的技巧。当然,我是以一个demo项目来介绍,这与我们实际的项目开发过程中会有点不同。但是,基本的思路是一样的。不知道大家有没有遇到测试提过如下问题:应用启动白屏(或黑屏时间)太久,希望优化。这个启动白屏(黑屏)时间太久,就是我们启动优化要做的工作。
1、闪屏什么是闪屏?上面我们提到过,在application启动前,系统会创建一个空白的window,而我们的闪屏,就是在这个空白的window上做文章。
首先,我们看一下优化之前,我的demo运行效果,如下图所示。可以看到,点击图标启动app后,有2秒多的白屏时间。当然,这个是因为我在代码中动了手脚,在Application中模拟耗时操作2秒钟。
接下来,我们操作一下,如何通过闪屏来优化这个体验:
(1)我们在drawable中创建一个drawable,命名为splash_bg:
(2)自定义Theme,并且在Manifest中为我们的MainActivity配置好:
@drawable/splash_bg
(3)在MainActivity的onCreate中,手动把Theme修改为我们原有的Theme:
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setContentView(R.layout.activity_main);
}
我们看一下优化后的效果,是不是给人一种秒开的错觉?对,实际上,我们只是把原来的白屏替换为我们自己定义的一个drawable,并且在activity的onCreate中,再替换为正确的Theme。但,这样会让用户更好去接受。
2、异步优化
先简单的介绍一下异步优化,异步优化,顾名思义,就是把线性执行的操作改为异步执行。例如,我们在主线程中做了2000ms的耗时操作。假如我们创建几个子线程,让子线程同步去进行sdk初始化等耗时操作。当然,为了比较优雅的实现多线程,我们简单的使用一下线程池。
private void initSdkWithThreadPool(){
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
initSdk1();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initSdk2();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initSdk3();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initSdk4();
}
});
}
我们看一下这样做后的优化效果:
看到上面的优化结果,大家是不是非常开心?是不是觉得不管有多少个耗时操作,我们都可以使用线程池异步来加载,就能达到上面秒开的效果?不好意思,答案是否定的。在很多情况下,我们是不能简单地使用线程池来达到我们的优化目的的。那么,什么情况呢?答案就是,我们的某些操作必须要在主线程中进行或者我们必须在主线程中用到该操作的某个产物。
针对上面的答案,可能有些朋友不太明白。我举个简单的例子,我们要在splash界面使用initSdk1方法的某个产物,而由于是异步执行,很有可能我们需要这个产物的时候,InitSdk1方法尚未执行完成。那这样的情况下,肯定会得到我们不想要的结果。例如,我们的initSdk1方法会给我们返回一个String值,而我们会通过Toast展示这个字符串,如下:
private String initSdk1() {
try {
//模拟sdk初始化等耗时操作
Thread.sleep(500);
result = "sdk1 init success";
} catch (InterruptedException e) {
e.printStackTrace();
}
mCountDownLatch.countDown();
return result;
}
如果我们异步执行,并且在异步方法后面去打印Toast,那么肯定拿不到“sdk1 init success”,拿到的是null,这样是不对的。那么,initSdk1这个方法我们就不让他异步执行了。我们单独把他拿出来,让他执行完后,我们再去打印我们的Toast。
initSdk1();
initSdkWithThreadPool();
Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
这篇文章总结了app启动优化的一些知识,包括启动的几种方式,获取启动时间以及启动优化用到的一个性能分析工具TraceView,并且通过一个简单的demo总结了两种启动优化的方法:闪屏和异步优化。其实闪屏和异步优化只是启动优化的最常规方式,相信很多一线团队都有自己的一些启动优化的独门秘笈。
作者:heart荼毒