一. 概述
性能优化是 Android 中的一个重要知识,也是衡量一个 Android 工程师水平的重要依据,简单的性能优化,可能很多人都会。比如以下几个优化 UI 渲染的方法,想必很多人都知道
使用“设置 --> 开发者选项 --> 调试 GPU 过度绘制”,根据屏幕显示的不同颜色来区分是存在过度绘制,从而排查该界面的 xml 文件,去除不必要的 background,消除过度绘制
通过 Layout Inspector 查看布局层级,排查是否存在多层无用的嵌套(由于 Hierarchy Viewer 已经被废弃,如果使用 3.1 及更新版本的 Android Studio,使用 Layout Inspector 查看布局会更加方便)
在 xml 中使用 ViewStub & merge 标签,优化布局层级
......
上面的这些点当然很重要,但是在某种程度下,上面的这些做法已经力不从心了,我们需要通过其他方式来达到优化性能的目的
俗话说的好,工欲善其事,必先利其器,使用一个好的工具当然可以让我们事半功倍,由于 TraceView 过于严重的运行时开销,使得 TraceView 测量的很多数据偏差较大,所以 Google 现在强推 systrace,systrace 是一个非常强大的性能分析工具。
systrace 可以从系统层面上,收集并分析设备运行时的所有进程的时间信息,它从 Android 内核中,比如:CPU 调度、磁盘活动和 app 线程中收集信息,然后生成如下图所示的 html 文件,需要说明的是:生成的 trace.html 文件必须用 Chrome 浏览器打开才可以正常的浏览使用.
图片来源:systrace。如上图所示,‘Frames’ 那一行里面的每一个小圆圈就代表着每一帧,用不同的颜色来代表是否正常的渲染,如果某一个小圆圈用黄色/红色表示,则表明这一帧的渲染可能存在问题
好,接下来我们来看下如何使用 systrace 工具
二. 如何使用
我使用的 Mac 电脑,所以以下操作都是在 Mac 上进行的,在 Windows 系统上应该也大同小异。
2.1 准备工作
在使用 systrace 之前,需要做以下几个准备工作
较新的 Android SDK Tools
需要 PC 端配合,PC 端安装了 Python 且配置在了系统环境变量中
调试的设备需要是 4.3(API Level 18)以上的,系统越高,可以收集到的信息越多,越有利于分析,分析的应用需要是 debug 包
通过 usb 将 Android 设备和 PC 连接成功,处于可调试的状态
至此,准备工作已完成
2.2 使用
2.2.1 使用方法
通过 Terminal 进入到 /Android/sdk/platform-tools/systrace/ 目录下
这个时候,在设备上操作应用,使应用进入到待调试的状态,比如需要调试某个页面 RecyclerView ,则进入该页面
然后在 Terminal 里面运行如下命令,其中 [options] [categories]
都是需要输入的参数
./systrace.py [options] [categories]
比如,执行如下命令,其中 -o mynewtrace.html -t 10
属于 [options]
参数,sched freq idle am wm gfx view binder_driver hal dalvik camera input res
属于 [categories]
参数
./systrace.py -o mynewtrace.html -t 10 sched freq idle am wm gfx view binder_driver hal dalvik camera input res
在运行 10s 之后,就会将记录的设备活动生成一个名为 mynewtrace.html 文件。
2.2.2 参数说明
那么 [options]
和 [categories]
都包括哪些参数呢?
[options]
参数是固定的,常用的包括以下几个
options | 缩写 | 含义 |
---|---|---|
-o <file> | 无 | 指定输出的文件,如:-o mynewtrace.html 。如果没有指定此参数,文件默认名称是 trace.html |
--time=<T> | -t <T> | 指定 systrace 的持续时间,如 -t 10 ,表示记录 10s 钟,<T>的单位是 s 秒。如果没有指定此参数,在按下回车键 Enter 健时结束 systrace |
--buf-size=<N> | -b <N> | 指定 systrace 的 buffer 是 N kb。指定在 systrace 过程中,收集的数据的总容量 |
--app=<app-name> | -a <app-name> | 指定特定的应用,比如:-a com.lijiankun24.shadowlayout 。如果在此应用中使用了 Trace.beginSection("tag") 和 Trace.endSection ,默认情况下,这些标签是不会生效的,除非你通过此命令指定该应用,在 systrace 输出的 html 文件中才会记录该标签标记的方法的信息 |
在介绍 [categories]
参数之前,先介绍 systrace 两个有用的指令
Global options | 缩写 | 含义 |
---|---|---|
--help | -h | 查看帮助信息 |
--list-categories | -l | 因为不同的设备,Android 系统版本也不一样,支持的 [categories] 参数也不同。可以通过此命令查看连接的设备支持哪些 [categories] 参数 |
比如,我设备的 Android 系统是 Android 7.1.2,运行如下命令以后,可以得到如下图所示的 [categories]
参数信息
./systrace -l
上面的这些 [categories]
参数指明此设备支持哪些可以被记录的模块,常用的有以下几个模块
categories | 全称 | 含义 |
---|---|---|
sched | CPU Scheduling | CPU 的调度信息,可以看到 CPU 的每个核在具体的时间点执行了什么线程 |
gfx | Graphics | Graphics 渲染系统,包括 SurfaceFlinger、VSync、Texture、RenderThread 的信息 |
input | Input | 输入事件系统,记录键盘输入、触摸等事件信息 |
view | View System | View 视图系统,常见的 View 的 onMeasure、onLayout、onDraw 都记录在此系统中 |
wm | Window Manager | WindowManager 的调用信息记录在此模块中 |
am | Activity Manager | ActivityManager 的调用信息记录在此模块中 |
dalvik | Dalvik VM | 虚拟机相关信息,比如 GC 垃圾回收信息 |
生成 trace.html 文件大概就是这样,并不复杂,下面介绍几个查看此 html 文件的快捷键,通过下面几个常用的快捷键,可以方便的查看 html 文件
快捷键 | 含义 |
---|---|
W | 放大时间轴 |
S | 放大时间轴 |
A | 左移时间轴 |
D | 右移时间轴 |
Right Arrow | 选中所选时间轴上的下一个事件 |
Left Arrow | 选中所选时间轴上的上一个事件 |
三. 分析 trace.html
3.1 简单分析 trace.html 文件
我们该如何分析生成的 trace.html 文件呢?
如下图所示,是一个放大后的 trace.html 的局部图。我们都知道 Android 系统中的 60 fps 概念,也就是 1s 内会渲染 60 帧,渲染一帧需要 16.6 ms,下图中用红色框起来的就是每一个 frame,如果在 16.6 ms 内完成了渲染,则该帧是绿色的,如果渲染超过了 16.6 ms,则呈现出黄色或者红色
图片来源 systrace
在上图中,选中存在问题的黄色帧以后,需要注意两部分,如下所示
第一个红色框中,高亮的部分是这一帧在 UI 线程和 RenderThread 线程中都调用了哪些方法
第二个红色框中,展示了一些信息,包括非常有用的该帧出问题的原因(Alert & Description),这些都是系统给出的存在的问题和优化建议
如果我们在上图中,选中右上角的 Alerts tab,会出现如下图所示的信息,它告诉我们在这段时间内该问题出现的频次,比如下图所示的:Inefficient ListView recycling/rebinding
共出现了 55 次。
可以把 Alerts tab 当做一个需要处理的 bug 列表,这个列表中的问题都不同程度上的对我们的帧渲染造成了问题。有时候可能只是几行代码的微小改动和优化,却可以优化我们很多的问题
3.2 为自己的应用添加 Trace 信息
默认情况下,systrace 都只能记录、收集系统层面的信息,比如 WindowManager
、ActivityManager
、以及 Dalvik 等等模块的,有没有什么办法也记录收集自己应用中的一些信息呢?
Android 是提供了这样的 Api 的,这个类是 Trace
类,使用 Trace
类记录自己应用中的信息其实并不难,如下所示,有如下几点需要注意
Trace.beginSection(String sectionName)
和Trace.endSection()
需要成对出现,为保证每个Trace.beginSection(String sectionName)
都会有对应的Trace.endSection()
,建议使用try {……} finally {……}
如果在
Trace.endSection()
之前有多个Trace.beginSection(String sectionName)
,Trace.endSection()
会匹配离它最近的一个未匹配过的Trace.beginSection(String sectionName)
Trace.beginSection(String sectionName)
和Trace.endSection()
需要在同一线程中
public class CardViewListActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Trace.beginSection("CardViewListActivity_onCreate");
try {
setContentView(R.layout.activity_card_view_list);
RecyclerView recyclerView = findViewById(R.id.rv_card_view);
recyclerView.setLayoutManager(new LinearLayoutManager(CardViewListActivity.this));
recyclerView.setAdapter(new CardViewListAdapter());
} finally {
Trace.endSection();
}
}
private static class CardViewListAdapter extends RecyclerView.Adapter<CardViewHolder> {
@NonNull
@Override
public CardViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Trace.beginSection("CardViewListAdapter_onCreateViewHolder");
CardViewHolder viewHolder;
try {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_view_list, null);
Trace.beginSection("CardViewListAdapter_onCreateViewHolder_newHolder");
try {
viewHolder = new CardViewHolder(view);
} finally {
Trace.endSection();
}
} finally {
Trace.endSection();
}
return viewHolder;
}
……
}
……
}
在自己应用的代码中添加如上代码之后并没有结束,还有一点非常重要,在执行 systrace 命令的时候,需要通过 -a <package_name>
指定应用包名,这样才会记录、收集到自己应用中添加的 trace 信息,如下所示:
./systrace.py -t 10 -o mytrace.html -a com.lijiankun24.shadowlayout sched freq idle am wm gfx view binder_driver hal dalvik camera input res
在生成的 trace.html 文件中,可以通过右上角的查找,找到 sectionName,就可以查到该 Trace 的记录信息
3.3 原理浅析
其实 systrace 的思想很简单,就是在一些关键路径中打 log,通过 log 的开始和结束就可以得到一个方法的执行时间信息,然后将这些 log 收集起来,就可以得到关键路径的运行时间信息,进而得到整个系统的运行性能信息。
在 Android 应用、Android Framework 和 native 层通过不同的方法或类打 log
3.3.1 Android Framework
import android.os.Trace;
Trace.traceBegin(long traceTag, String methodName)
Trace.traceEnd(long traceTag)
比如在 ActivityThread
中的内部类 H.handleMessage(Message msg)
方法如下所示
在 Android Framework 中是通过 Trace.traceBegin(long traceTag, String methodName)
方法打 log 的,传入的 traceTag 是 Trace
类中的常量类,如下所示
其实这里的 Trace
常量值,和我们在执行 ./systrace [options] [categories]
时,传入的 [categories]
值对应的
3.3.2 Android 应用
对应的 traceTag 名称是 TRACE_TAG_APP
,在使用 systrace.py 命令运行时,需要通过 -a <package-name>
指定应用的包名,才可以收集到埋的 tag
import android.os.Trace;
Trace.beginSection(String sectionName)
Trace.EndSection()
Trace
类的源码如下,可见 traceBegin(long traceTag, String methodName)
、traceEnd(long traceTag)
、beginSection(String sectionName)
、endSection()
最后都调用了 native 方法 nativeTraceBegin(long tag, String name)
和 nativeTraceEnd(long tag)
public final class Trace {
@FastNative
private static native void nativeTraceBegin(long tag, String name);
@FastNative
private static native void nativeTraceEnd(long tag);
private Trace() {
}
......
public static void traceBegin(long traceTag, String methodName) {
if (isTagEnabled(traceTag)) {
nativeTraceBegin(traceTag, methodName);
}
}
public static void traceEnd(long traceTag) {
if (isTagEnabled(traceTag)) {
nativeTraceEnd(traceTag);
}
}
......
public static void beginSection(String sectionName) {
if (isTagEnabled(TRACE_TAG_APP)) {
if (sectionName.length() > MAX_SECTION_NAME_LEN) {
throw new IllegalArgumentException("sectionName is too long");
}
nativeTraceBegin(TRACE_TAG_APP, sectionName);
}
}
public static void endSection() {
if (isTagEnabled(TRACE_TAG_APP)) {
nativeTraceEnd(TRACE_TAG_APP);
}
}
}
3.3.3 native 层
其实 systrace 本质上是对其他工具的封装,包括 PC 端的 atrace
和设备端的 ftrace
,ftrace
是 Linux 内核中的主要跟踪机制。systrace 使用 atrace
开启追踪,然后读取 ftrace
的缓存,并且把它重新转换成HTML格式
#include<utils/Trace.h>
ATRACE_CALL();
这里介绍了 systrace 基本的分析方法和基本原理,更深入的分析方法和原理会在后面系列中更新,敬请期待.
四. 参考
https://developer.android.com/studio/command-line/systrace