一、背景
相信很多小伙伴在app性能方面都有一些需求,这篇文章主要是BTrace和Perfetto结合使用分析性能
btrace(又名 RheaTrace) 是抖音基础技术团队自研的一款高性能 Android Trace 工具,它基于 Systrace 实现,并针对 Systrace 不足之处加以改进,核心改进点如下。
- 效率提升:编译期间为 App 方法自动注入自定义事件,并提供高效、灵活配置规则。
- 性能提升:改进 Systrace 文件实时写 atrace 数据方式,性能提升最大 400 % 以上。
- 实用性提升:额外提供更详细 IO 等数据,大幅提升方法耗时归因效率;使用独创方案彻底来解决方法因执行异常引起 trace 数据不闭合问题。
Perfetto:是一个新的、低开销的 Trace 采集工具,旨在优化 Systrace 的性能表现。Perfetto 的目标是提供比 Systrace 更快、更细粒度的 Trace 采集,并支持与其他跨平台工具集成。Perfetto 采用二进制格式记录 Trace 数据,并使用基于 ProtoBuf 的数据交换格式进行数据导出,可与 Grafana、SQLite、BigQuery 等其他分析和可视化工具集成。Perfetto 采集的数据种类非常广泛,包括 CPU 使用情况、网络字节流、触摸输入、渲染等等。与 Systrace 相比,Perfetto 在性能和可定制性方面更为出色。
btrace:github.com/bytedance/b...
perfetto:ui.perfetto.dev/
二、BTrace的作用
1、渲染监控
在RenderThread中可以直观分析是具体影响渲染问题的业务代码
2、Binder监控
btrace的Binder增强目标是将Binder调用的接口名称与方法名称进行解析与展示
3. 阻塞监控
btrace可以检索到wait/park等原因导致线程等待状态
4. 线程创建监控
btrace可以检索到线程创建时机
三、BTrace接入
1. 接入BTrace
在您项目的根目录下build.gradle文件中增加 rhea-gradle-plugin 作为依赖
gradle
buildscript {
dependencies {
classpath 'com.bytedance.btrace:rhea-gradle-plugin:2.0.3-rc02'
}
}
接着,在app/build.gradle文件中应用如下所示插件和依赖
gradle
dependencies {
// rheatrace core lib
implementation "com.bytedance.btrace:rhea-core:2.0.3-rc02"
}
...
apply plugin: 'com.bytedance.rhea-trace'
...
rheaTrace {
compilation {
//
traceFilterFilePath = "${project.rootDir}/trace-filter/preciseTraceFilter.txt"
needPackageWithMethodMap = true
applyMethodMappingFilePath = ""
}
}
preciseTraceFilter.txt配置请参考 github.com/bytedance/b...
2. 执行
- 确保电脑已集成 adb 与 Java 环境。
- 连接手机至电脑,并保证可被 adb devices 识别。
- 安装集成 RheaTrace 2.0 的 APK 至手机。
- 下载"脚本管理rhea-trace-shell-2.0.0.jar" 最新脚本至电脑。
- 在电脑脚本所在目录执行如下命令。
java
java -jar rhea-trace-shell.jar -a ${your app package name} -t 10 -o output.pb -r rhea.all sched -fullClassName
- trace 产物使用 ui.perfetto.dev/ 即可打开分析。 将output.pb使用perfetto打开,选择Open trace file
四、手把手教性能分析
1、渲染监控
xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/fl1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/ll2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/fl3"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/ll4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World! One"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World! Two"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
这个xml是MainActivity的布局文件,从代码中可以看出层级嵌套5层,布局层级过深,会导致性能降低,那看下在Perfetto中表现如何
在RenderThread中找到对应的activity_main,可以明显看到有5层布局
2、Binder监控
如上图,我的demo中binder通信比较少,不过可以看见binder调用的
3、阻塞监控
java
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
attachTimeStart = SystemClock.uptimeMillis();
SystemClock.sleep(2_000);
attachTimeEnd = SystemClock.uptimeMillis();
Log.d(TAG, "attachBaseContext time = " + (attachTimeEnd - attachTimeStart));
}
在demo里,Application的函数attachBaseContext调用sleep将主线程睡2000,可以在图片中看到Thread#Sleep
xml
<provider
android:authorities="com.test.one"
android:name=".Test.Test1FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>>
<provider
android:authorities="com.test.two"
android:name=".Test.Test2FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<provider
android:authorities="com.test.three"
android:name=".Test.Test3FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<provider
android:authorities="com.test.four"
android:name=".Test.Test4FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<provider
android:authorities="com.test.five"
android:name=".Test.Test5FileProvider"
android:grantUriPermissions="true"
android:exported="false" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
......
在demo里,清单文档声明了10个FileProvider,FileProvider继承了ContentProvider,在启动阶段会执行FileProvider的attach,在attach中调用getPathStrategy加载xml,在图片中可以看到getPathStrategy执行耗时
java
@Override
public void onCreate() {
super.onCreate();
createTimeStart = SystemClock.uptimeMillis();
Log.d(TAG, "contentProvider time = " + (createTimeStart - attachTimeEnd));
new Thread("timlione") {
@Override
public void run() {
super.run();
readAssetFile(getAssets(), "woed.txt");
}
}.start();
new Thread("timlitwo") {
@Override
public void run() {
super.run();
readAssetFile(getAssets(), "woed.txt");
}
}.start();
new Thread("timlithree") {
@Override
public void run() {
super.run();
readAssetFile(getAssets(), "woed.txt");
}
}.start();
new Thread("timlifore") {
@Override
public void run() {
super.run();
readAssetFile(getAssets(), "woed.txt");
}
}.start();
new Thread("timlifive") {
@Override
public void run() {
super.run();
readAssetFile(getAssets(), "woed.txt");
}
}.start();
readAssetFile(getAssets(), "woed.txt");
createTimeEnd = SystemClock.uptimeMillis();
Log.d(TAG, "onCreate time = " + (createTimeEnd - createTimeStart));
}
public static String readAssetFile(AssetManager assetManager, String fileName) {
synchronized (MyApplication.class) {
// 打开资产文件
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open(fileName)))) {
String line;
// 逐行读取文件内容并拼接成字符串
StringBuilder content = new StringBuilder();
while ((line = reader.readLine())!= null) {
content.append(line).append("\n");
}
// 返回读取的内容
return content.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
在demo里,Application中的onCreate开了5个线程加上主线程一共6个线程,调用readAssetFile函数,在readAssetFile函数中使用synchronized关键字加了锁,在图片中我们可以看到主线程被锁住了,矩形的长度就是被锁住的时间,每个线程中矩形的长度不同,代码锁住时间不同,长度短的在下一轮锁竞争中获取到了锁
4、线程创建
如图在主线程中创建25352线程
至此结束,赶快结合项目分析下吧