前言
成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。
在性能优化的整个知识体系中,最重要的就是稳定性优化,在上一篇文章 《深入探索Android稳定性优化》 中我们已经深入探索了Android稳定性优化的疆域。那么,除了稳定性以外,对于性能纬度来说,哪个方面的性能是最重要的呢?毫无疑问,就是应用的启动速度。下面,就让我们扬起航帆,一起来逐步深入探索Android启动速度优化的奥秘。
一、启动优化的意义
如果我们去一家餐厅吃饭,在点餐的时候等了半天都没有服务人员过来,可能就没有耐心等待直接走了。
对于App来说,也是同样如此,如果用户点击App后,App半天都打不开,用户就可能失去耐心卸载应用。
启动速度是用户对我们App的第一体验,打开应用后才能去使用其中提供的强大功能,就算我们应用的内部界面设计的再精美,功能再强大,如果启动速度过慢,用户第一印象就会很差。
因此,拯救App的启动速度,迫在眉睫。
二、应用启动流程
1 、应用启动的类型
应用启动的类型总共分为如下三种:
- 冷启动
- 热启动
- 温启动
下面,我们来详细分析下各个启动类型的特点及流程。
冷启动
从点击应用图标到UI界面完全显示且用户可操作的全部过程。
特点
耗时最多,衡量标准。
启动流程
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
首先,用户进行了一个点击操作,这个点击事件它会触发一个IPC的操作,之后便会执行到Process的start方法中,这个方法是用于进程创建的,接着,便会执行到ActivityThread的main方法,这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。
热启动
直接从后台切换到前台。
特点
启动速度最快。
温启动
只会重走Activity的生命周期,而不会重走进程的创建,Application的创建与生命周期等。
特点
较快,介于冷启动和热启动之间的一个速度。
启动流程
LifeCycle -> ViewRootImpl
ViewRootImpl是什么?
它是GUI管理系统与GUI呈现系统之间的桥梁。每一个ViewRootImpl关联一个Window,
ViewRootImpl 最终会通过它的setView方法绑定Window所对应的View,并通过其performTraversals方法对View进行布局、测量和绘制。
2、冷启动分析及其优化方向
冷启动涉及的相关任务
冷启动之前
- 首先,会启动App
- 然后,加载空白Window
- 最后,创建进程
需要注意的是,这些都是系统的行为,一般情况下我们是无法直接干预的。
随后任务
- 首先,创建Application
- 启动主线程
- 创建MainActivity
- 加载布局
- 布置屏幕
- 首帧绘制
通常到了界面首帧绘制完成后,我们就可以认为启动已经结束了。
优化方向
我们的优化方向就是 Application和Activity的生命周期 这个阶段,因为这个阶段的时机对于我们来说是可控的。
三、启动耗时检测
1、查看Logcat
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
2、adb shell
使用adb shell获取应用的启动时间
// 其中的AppstartActivity全路径可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]
执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:
ThisTime
表示最后一个Activity启动耗时。
TotalTime
表示所有Activity启动耗时。
WaitTime
表示AMS启动Activity的总耗时。
一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
特点:
- 1、线下使用方便,不能带到线上。
- 2、非严谨、精确时间。
3、代码打点(函数插桩)
可以写一个统计耗时的工具类来记录整个过程的耗时情况。其中需要注意的有:
- 在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
- 在项目中核心基类的关键回调函数和核心方法中加入打点。
其代码如下所示:
/**
* 耗时监视器对象,记录整个过程的耗时情况,可以用在很多需要统计的地方,比如Activity的启动耗时和Fragment的启动耗时。
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitord = -1;
// 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int mMonitorId) {
Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
this.mMonitorId = mMonitorId;
}
public int getMonitorId() {
return mMonitorId;
}
public void startMonitor() {
// 每次重新启动都把前面的数据清除,避免统计错误的数据
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
/**
* 每打一次点,记录某个tag的耗时
*/
public void recordingTimeTag(String tag) {
// 若保存过相同的tag,先清除
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
Log.d(TAG, tag + ": " + time);
mTimeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recordingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
//写入到本地文件
}
}
public HashMap<String, Long> getTimeTags() {
return mTimeTag;
}
}
为了使代码更好管理,我们需要定义一个打点配置类,如下所示:
/**
* 打点配置类,用于统计各阶段的耗时,便于代码的维护和管理。
*/
public final class TimeMonitorConfig {
// 应用启动耗时
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
此外,耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据:
/**
* 采用单例管理各个耗时统计的数据。
*/
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;
public synchronized static TimeMonitorManager getInstance() {
if (mTimeMonitorManager == null) {
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager() {
this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
}
/**
* 初始化打点模块
*/
public void resetTimeMonitor(int id) {
if (mTimeMonitorMap.get(id) != null) {
mTimeMonitorMap.remove(id);
}
getTimeMonitor(id);
}
/**
* 获取打点器
*/
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorMap.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorMap.put(id, monitor);
}
return monitor;
}
}
主要在以下几个方面需要打点:
- 应用程序的生命周期节点。
- 启动时需要初始化的重要方法,例如数据库初始化,读取本地的一些数据。
- 其他耗时的一些算法。
例如,启动时在Application和第一个Activity加入打点统计:
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();
SoLoader.init(this, /* native exopackage */ false);
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}
第一个Activity打点
@Override
protected void onCreate(Bundle savedInstanceState) {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
super.onCreate(savedInstanceState);
initData();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}
@Override
protected void onStart() {
super.onStart();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}
特点
精确,可带到线上,但是代码有侵入性,修改成本高。
注意事项
-
1、在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
-
2、onWindowFocusChanged只是首帧时间,App启动完成的结束点应该是真实数据展示出来的时候(通常来说都是首帧数据),如列表第一条数据展示,记得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上可以使用addOnDrawListener),它会把任务延迟到列表显示后再执行,例如,在Awesome-WanAndroid项目的主页就有一个RecyclerView实现的列表,启动结束的时间就是列表的首帧时间,也即列表第一条数据展示的时候。这里,我们直接在RecyclerView的适配器ArticleListAdapter的convert(onBindViewHolder)方法中加上如下代码即可:
if (helper.getLayoutPosition() == 1 && !mHasRecorded) { mHasRecorded = true; helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this); LogHelper.i("FeedShow"); return true; } }); }
具体的实例代码可在 这里查看。
为什么不使用onWindowFocusChanged这个方法作为启动结束点?
因为用户看到真实的界面是需要有网络请求返回真实数据的,但是onWindowFocusChanged只是界面绘制的首帧时机,但是列表中的数据是需要从网络中下载得到的,所以应该以列表的首帧数据作为启动结束点。
4、AOP(Aspect Oriented Programming) 打点
面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。
1、作用
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。
2、AOP核心概念
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理。
2、切面(Aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象。
3、连接点(JoinPoint)
被拦截到的点(方法、字段、构造器)。
4、切入点(PointCut)
对JoinPoint进行拦截的定义。
5、通知(Advice)
拦截到JoinPoint后要执行的代码,分为前置、后置、环绕三种类型。
3、准备:接入AspectJx进行切面编码
首先,为了在Android使用AOP埋点需要引入AspectJ,在项目根目录的build.gradle下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
然后,在app目录下的build.gradle下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
4、AOP埋点实战
JoinPoint一般定位在如下位置
- 1、函数调用
- 2、获取、设置变量
- 3、类初始化
使用PointCut对我们指定的连接点进行拦截,通过Advice,就可以拦截到JoinPoint后要执行的代码。Advice通常有以下几种类型:
- 1、Before:PointCut之前执行
- 2、After:PointCut之后执行
- 3、Around:PointCut之前、之后分别执行
首先,我们举一个小栗子:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有Activity中on开头的方法。
其中execution是处理Join Point的类型,在AspectJx中共有两种类型,如下所示:
- 1、call:插入在函数体里面
- 2、execution:插入在函数体外面
如何统计Application中的所有方法耗时?
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
在上述代码中,我们需要注意 不同的Action类型其对应的方法入参是不同的,具体的差异如下所示:
- 当Action为Before、After时,方法入参为JoinPoint。
- 当Action为Around时,方法入参为ProceedingPoint。
Around和Before、After的最大区别:
ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法。
5、总结AOP特性
- 1、无侵入性
- 2、修改方便,建议使用
4、启动速度分析工具 — TraceView
1、使用方式
- 1、代码中添加:Debug.startMethodTracing()、检测方法、Debug.stopMethodTracing()。(需要使用adb pull将生成的.trace文件导出到电脑,然后使用Android Studio的Profiler进行加载)
- 2、打开 Profiler -> CPU -> 点击 Record -> 点击 Stop -> 查看Profiler下方Top Down/Bottom Up 区域,以找出耗时的热点方法。
2、Profile CPU
使用 Profile 的 CPU 模块可以帮我们快速找到耗时的热点方法,下面,我们来详细来分析一下这个模块。
1、Trace types
Trace types 有四种,如下所示。
1、Trace Java Methods
会记录每个方法的时间、CPU信息。对运行时性能影响较大。
2、Sample Java Methods
相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的