Android启动速度优化一启动速度测量
随着手机硬件的发展,手机硬件配置越来越高,计算速度,硬件性能越来越好,导致在开发过程中很容易让开发者不太去关注启动速度和性能问题。但是在发布到市场上后就会有用户反馈说启动速度慢,体验不好的问题。实际上性能问题、启动速度问题在高端机上依然存在,例如在手机内存吃紧的时候,再去启动一个APP的话还是会遇到这类问题,在低端机上就更不用说了。
这里是自己在开发过程中的一些经验积累,记录下来方便自己日常查阅,本篇是启动速度优化第一篇,主要记录APP启动速度的测量方法。
一、adb命令查看启动时间
Android本身提供有可以查看APP启动速度的命令,日常开发和调试中可以使用它们快速的查看信息,方便日常的开发。但是有一个缺点是adb命令查看的时间无法反应出启动过程中每个环节的时间消耗
1.1 adb shell am start -W
我们可以使用这个命令来启动目标Activity,启动后终端会显示对应的启动信息
adb shell am start -W packageName/ComponentName
demo
adb shell am start -W com.snail.memo/com.snail.memo.NoteStartActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.snail.memo/.NoteStartActivity }
Status: ok
Activity: com.snail.memo/.NoteStartActivity
ThisTime: 332
TotalTime: 332
WaitTime: 359
Complete
可以看到,在终端输入的这段信息,各信息详细意思
Status
启动状态,有两种取值,ok和timeout,正常启动为ok,启动超时或者异常为timeout
Activty
启动的目标Activity名
ThisTime
表示一连串启动 Activity 的最后一个 Activity 的启动耗时。 注:Android Q之后不再有该属性
TotalTime
所有Activity启动耗时,包括进程创建,Application初始化,Activity实例化并初始化,到界面显示整个过程的时间
WaitTime
执行本条命令开始到Activity启动完成的总时间,包括整个启动过程的所有时间(AMS内部的逻辑时间和Activity真正启动的时间),统计的方法实际上是在执行命令前后分别记录了startTime和endTime,执行完成后取两者的差值
adb shell am这条命令执行后在java层的调用代码如下:
代码路径:
frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
执行代码:
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
case "start":
case "start-activity":
return runStartActivity(pw);
........//其他命令代码
}
}
}
runStartActivity方法的开头会调用makeIntent方法,改方法会解析我们传递进来的option -W,匹配上-W后,把mWaitOption设置为true。然后在后面真正启动Activity的时候利用mWaitOption来处理不同的启动方式。
makeIntent方法中的关键代码:
return Intent.parseCommandArgs(this, new Intent.CommandOptionHandler() {
@Override
public boolean handleOption(String opt, ShellCommand cmd) {
if (opt.equals("-D")) {
mStartFlags |= ActivityManager.START_FLAG_DEBUG;
} else if (opt.equals("-N")) {
mStartFlags |= ActivityManager.START_FLAG_NATIVE_DEBUGGING;
} else if (opt.equals("-W")) {
mWaitOption = true;
执行完以上逻辑后就会进入runStartActivity真正启动Activity的过程
runStartActivity启动Activity代码片段,利用mWaitOption判断是调用AMS的哪个方法启动Activity,-W被指定时mWaitOption为true,因此就会调用startActivityAndWait方法进行启动Activity的操作
if (mWaitOption) {
result = mInternal.startActivityAndWait(null, null, intent, mimeType,
null, null, 0, mStartFlags, profilerInfo,
options != null ? options.toBundle() : null, mUserId);
res = result.result;
} else {
res = mInternal.startActivityAsUser(null, null, intent, mimeType,
null, null, 0, mStartFlags, profilerInfo,
options != null ? options.toBundle() : null, mUserId);
}
后面的事情就是启动Activity的流程,不在这里进行深入。
看到这里发现一个问题,只是单独指定-W的时候,如果要测试APP的冷启动时间,每次都要手动把进程停止掉,很麻烦。如果系统可以有更加便捷的方法提供给我们就完美了。果不其然,真的有提供这个操作,那就是-S 操作
adb shell am start -S -W packageName/ComponentName
如此一来没执行一次命令之前系统会先forceStop掉目标进程,就可以很方便的调试冷启动时间了
原因在于makeIntent方法中解析option的时候,如果有设置了-S,会把mStopOption设置为true。runStartActivity方法执行启动Activity之前会判断这个属性是否为true,为true的话就先调用forceStop方法停止掉目标进程
if (mStopOption) {
String packageName;
if (intent.getComponent() != null) {
packageName = intent.getComponent().getPackageName();
} else {
// queryIntentActivities does not convert user id, so we convert it here first
int userIdForQuery = mInternal.mUserController.handleIncomingUser(
Binder.getCallingPid(), Binder.getCallingUid(), mUserId, false,
ALLOW_NON_FULL, "ActivityManagerShellCommand", null);
List<ResolveInfo> activities = mPm.queryIntentActivities(intent, mimeType, 0,
userIdForQuery).getList();
if (activities == null || activities.size() <= 0) {
getErrPrintWriter().println("Error: Intent does not match any activities: "
+ intent);
return 1;
} else if (activities.size() > 1) {
getErrPrintWriter().println(
"Error: Intent matches multiple activities; can't stop: "
+ intent);
return 1;
}
packageName = activities.get(0).activityInfo.packageName;
}
pw.println("Stopping: " + packageName);
pw.flush();
mInterface.forceStopPackage(packageName, mUserId);
try {
Thread.sleep(250);
} catch (InterruptedException e) {
}
}
1.2 adb logcat
Activity启动的时候AMS会打印出对应的log信息,我们可以过滤出需要的信息来知道启动的时间
adb logcat -s ActivityManager:I | grep Displayed
或者在AndroidStudio中自己过滤Displayed信息出来
ActivityManager: Displayed com.snail.memo/.NoteStartActivity: +119ms
二、代码打点统计启动耗时
打点统计启动时间的方式比较简单,主要还是要先只掉Activity和APP的启动流程,然后在对应的方法中加入打点代码片段即可。
我们知道冷启动过程中APP端最先被调用到的方法是Application的attachBaseContext方法,然后才会到onCreate,后面才会到Activity相关的周期函数。因此在统计的时候我们可以从这里入手。但是代码打点需要注意的是,启动时间优化我们是要关注的不知冷冰冰的统计数据,而是用户从点击桌面图标到真正看到APP界面的时间。因此打点的结束时间应该是视图界面绘制完成的时间。而不是Activity onCreate方法结束的时间或者onResume被调用到的时间
2.1 直接插入代码统计
伪代码片段
public class TimeTrace{
public static long sStart;
public static long sEnd;
public static void startTracing(){
sStart = System.currentTimeMillis()
}
public static void endTracing(){
Log.d("StartTime","TotalTime:"+(System.currentTimeMillis() - sStart));
}
}
在Application的attachBaseContext记录开始时间
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
TimeTrace.startTracing();
}
在MainActivity的onCreate方法中找到一个对用于视觉感官比较强的View,在这个View进行绘制的时候进结束时间的打点和输出
findViewById(R.id.logo).getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
if( !hasCounted ){
hasCounted = true;
TimeTrace.startTracing();
}
}
});
2.2 AOP
2.1节中的方式并没有什么问题,只是需要修改原来的业务逻辑代码,不利于维护。网上有很多文章有介绍到使用AOP的形式进行统计。自己demo之后发现确实可行。这里不再重复。
提供一个快速集成AOP的插件,可以直接使用,非常方便:
https://github.com/alexluotututu/aop_plugin
三、Systrace
实际上可以对Android 性能进行检测的有很多工具,例如Profiler,TraceView等,但是这些工具因为统计的信息比较多,个人觉得无法真正对优化的方向进行指导,比如TraceView本身在统计信息的时候自己就会滑掉一部分时间。
如上图所示,冷启动时间我们从bindApplicaiton开始,到第二帧绘制结束即可。
启动分析过程中需要对每个方法进行耗时统计的话可以自己在目标方法中加入trace统计方法即可,例如我要统计initData这个方法的耗时,可以这样做
private void initData(){
TraceCompat.beginSection("InitData");
//do something
TraceCompat.endSection();
}
使用以下命令生成trace文件,利用Chrome浏览器打开,就可以查看详细信息了
python systrace.py -t 10 sched gfx view wm am app webview -a "com.snail.memo" -o ~/Documents/systrace_file/start_time.html
-t: 表示统计的时长
-a: 指定目标包名
-o: 指定输出文件
四、高速相机
高速相机主要是模拟人眼的场景,记录从用户点击桌面图标到看到真正画面的时间。测试工程师一般会用这个时间类评估APP冷启动时间。但是因为个人比较穷,没有用过,在此不做过多叙述