分析应用程序启动

一旦我们建立了触发应用程序缓慢启动的指标和场景,下一步就是提高性能。

要了解是什么导致应用程序启动缓慢,我们需要对其进行分析。 Android Studio 提供了几种类型的分析器录制配置:

Trace System Calls(又名 systrace、perfetto):对运行时的影响很小,非常有助于了解应用程序如何与系统和 CPU 交互,但不了解应用程序 VM 内部发生的 Java 方法调用。

Sample C/C++ Functions(又名 Simpleperf):我不感兴趣,我处理的应用程序运行的字节码比本机代码多得多。 在 Q+ 上,这现在也应该以低开销的方式对 Java 堆栈进行采样,但我还没有设法让它工作。

Trace Java Methods:这会捕获所有 VM 方法调用,这些调用引入了如此多的开销,结果没有多大意义。

Sample Java Methods:开销比跟踪少,但显示了 VM 内部发生的 Java 方法调用。 这是我在分析应用程序启动时的首选选项。

在应用程序启动时开始录制

Android Studio profiler 有通过连接到已经运行的进程来启动跟踪的 UI,但没有明显的方式在应用程序启动时开始记录。

该选项存在但隐藏在应用程序的 run configuration:在 profiling 选项卡中选中启动时启动此记录。

然后通过 Run > Profile app 部署应用程序。

分析 release builds

Android 开发人员通常在日常工作中使用调试构建类型,调试构建通常包括 LeakCanary 等额外库等。

开发人员应该分析发布版本而不是调试版本,以确保他们正在解决客户面临的实际问题。

不幸的是,发布版本是不可调试的,因此 Android 分析器无法记录发布版本的跟踪。

以下是解决该问题的几个选项。

1. 创建可调试的发布版本

我们可以暂时使我们的发布构建可调试,或者创建一个新的发布构建类型来进行分析。

android {
buildTypes {
release {
debuggable true
// ...
}
}
}

如果 APK 是可调试的,库和 Android 框架代码通常会有不同的行为。 ART 禁用了许多优化以启用连接调试器,这会显着且不可预测地影响性能。因此该解决方案并不理想。

2. 在有 root 设备上调试设备

Root 设备允许 Android Studio 分析器记录不可调试构建的跟踪。

通常不建议在模拟器上进行分析 - 每个系统组件的性能都会不同(cpu 速度、缓存大小、磁盘性能),因此“优化”实际上可以通过将工作转移到手机上较慢的东西来使事情变慢 . 如果您没有可用的 root 物理设备,您可以创建一个没有 Play 服务的模拟器,然后运行 adb root。

3. 在 Android Q 上使用 simpleperf

有一个名为 simpleperf 的工具,如果它们有一个特殊的清单标志,据说可以在非根 Q+ 设备上启用分析版本构建。 该文档将其称为 profileableFromShell,XML 示例有一个带有 android:shell 属性的 profileable 标记,官方清单文档没有显示任何内容。

<manifest ...>
<application ...>
<profileable android:shell="true" />
</application>
</manifest>

我查看了 cs.android.com 上的清单解析代码:

if (tagName.equals("profileable")) {
sa = res.obtainAttributes(
parser,
R.styleable.AndroidManifestProfileable
);
if (sa.getBoolean(
R.styleable.AndroidManifestProfileable_shell,
false
)) {
ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL;
}
}

如果清单具有 <profileable android:shell="true" /> (我没有尝试过),您似乎可以从命令行触发分析。 据我了解,Android Studio 团队仍在努力与此新功能集成。

分析下载的 APK

在 Square,我们的版本是用 CI 构建的。 正如我们之前看到的,从 Android Studio 分析应用程序启动需要检查运行配置中的一个选项。 我们如何使用下载的 APK 来做到这一点?

事实证明,这是可能的,但隐藏在 File > Profile or Debug APK 下。 这将打开一个包含解压缩 APK 的新窗口,您可以从中设置运行配置并开始分析。

Android Studio 分析器会减慢一切

不幸的是,当我在一个生产应用程序上测试这些方法时,即使在最近的 Android 版本上,Android Studio 的分析也会大大减慢应用程序的启动速度(大约慢 10 倍)。 我不知道为什么,也许是“高级分析”,它似乎不能被禁用。 我们需要另辟蹊径!

从代码分析

我们可以直接从代码开始跟踪,而不是从 Android Studio 进行分析:

val tracesDirPath = TODO("path for trace directory")
val fileNameFormat = SimpleDateFormat(
"yyyy-MM-dd_HH-mm-ss_SSS'.trace'",
Locale.US
)
val fileName = fileNameFormat.format(Date())
val traceFilePath = tracesDirPath + fileName
// Save up to 50Mb data.
val maxBufferSize = 50 * 1000 * 1000
// Sample every 1000 microsecond (1ms)
val samplingIntervalUs = 1000
Debug.startMethodTracingSampling(
traceFilePath,
maxBufferSize,
samplingIntervalUs
)

// ...

Debug.stopMethodTracing()

然后我们可以从设备中提取跟踪文件并将其加载到 Android Studio 中。

什么时候开始取样

我们应该在应用程序生命周期中尽早开始记录跟踪。 最早可以在 Android P 之前在应用程序启动时运行的代码是 ContentProvider,而在 Android P+ 上它是 AppComponentFactory。

Android P / API < 28

class AppStartListener : ContentProvider() {
override fun onCreate(): Boolean {
Debug.startMethodTracingSampling(...)
return false
}
// ...
}
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<provider
android:name=".AppStartListener"
android:authorities="com.example.appstartlistener"
android:exported="false" />
</application>

</manifest>

在定义提供者时,我们可以设置一个 initOrder 标记,并且最高的数字首先被初始化。

Android P+ / API 28+

@RequiresApi(28)
class MyAppComponentFactory() :
androidx.core.app.AppComponentFactory() {

@RequiresApi(29)
override fun instantiateClassLoader(
cl: ClassLoader,
aInfo: ApplicationInfo
): ClassLoader {
if (Build.VERSION.SDK_INT >= 29) {
Debug.startMethodTracingSampling(...)
}
return super.instantiateClassLoader(cl, aInfo)
}

override fun instantiateApplicationCompat(
cl: ClassLoader,
className: String
): Application {
if (Build.VERSION.SDK_INT < 29) {
Debug.startMethodTracingSampling(...)
}
return super.instantiateApplicationCompat(cl, className)
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:appComponentFactory=".MyAppComponentFactory"
tools:replace="android:appComponentFactory"
tools:targetApi="p">
</application>

</manifest>

在哪里存储采样

val tracesDirPath = TODO("path for trace directory")
  • API < 28:广播接收器可以访问上下文,我们可以在该上下文上调用 Context.getDataDir() 将跟踪存储在应用程序目录中。

  • API 28: AppComponentFactory.instantiateApplication() 负责创建一个新的应用程序实例,所以目前还没有可用的上下文。 我们可以直接硬编码到 /sdcard/ 的路径,但这需要 WRITE_EXTERNAL_STORAGE 权限。

  • API 29+:当面向 API 29 时,硬编码 /sdcard/ 停止工作。 我们可以添加 requestLegacyExternalStorage 标志,但无论如何 API 30 都不支持它。 建议在 API 30+ 上尝试 MANAGE_EXTERNAL_STORAGE。 无论哪种方式,AppComponentFactory.instantiateClassLoader() 都会传递一个 ApplicationInfo,因此我们可以使用 ApplicationInfo.dataDir 将跟踪存储在应用程序目录中。

何时停止采样

当应用程序的第一帧完全加载时,冷启动结束。 我们可以根据这个条件停止方法跟踪:

class MyApp : Application() {

override fun onCreate() {
super.onCreate()

var firstDraw = false
val handler = Handler()

registerActivityLifecycleCallbacks(
object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
if (firstDraw) return
val window = activity.window
window.onDecorViewReady {
window.decorView.onNextDraw {
if (firstDraw) return
firstDraw = true
handler.postAtFrontOfQueue {
Debug.stopMethodTracing()
}
}
}
}
})
}
}

我们还可以记录比应用程序启动时间更长的固定时间,例如 5秒:

Handler(Looper.getMainLooper()).postDelayed({
Debug.stopMethodTracing()
}, 5000)

使用 Nanoscope 进行分析

另一个用于分析应用程序启动的选项是 uber/nanoscope。 这是一个带有内置低开销跟踪的 Android 图像。 它很棒,但有一些限制:

它只跟踪主线程。

大型应用程序将溢出内存跟踪缓冲区。

应用启动步骤

一旦我们有了启动跟踪,我们就可以开始调查什么操作耗费时间。 应该期待 3 个主要部分:

ActivityThread.handlingBindApplication() 包含 Activity 创建之前的启动工作。 如果这很慢,那么我们可能需要优化 Application.onCreate()。

TransactionExecutor.execute() 负责创建和恢复 Activity ,包括填充视图层次结构。

ViewRootImpl.performTraversals() 是框架执行第一次测量、布局和绘制的地方。 如果这很慢,则可能是视图层次结构过于复杂,或者具有需要优化的自定义绘图的视图。

如果注意到服务是在第一次视图遍历之前启动的,那么延迟该服务的启动可能是值得的,以便它在视图遍历之后发生。

结论

一些要点:

分析发布版本专注于实际问题。

Android 上的分析应用程序启动状态远非理想。 基本上没有好的开箱即用解决方案,但 Jetpack Benchmark 团队正在努力解决这个问题。

从代码开始采样,以防止 Android Studio 拖慢一切。

0 个评论

要回复文章请先登录注册