你全新的应用在Google Play上遭遇用户投诉,称其经常卡顿或变慢?请按照本文中的提示进行修复。
Hello! 我叫Sergey Panov,是IceRock的移动开发者。今天,我将使用我们的Campus应用作为示例,演示Jetpack Compose的性能分析和优化技术。
Campus 是一个允许学生查看课程表的应用程序。其主要功能是由两个 pagers 组成的时间表屏幕,用于显示周和天。当用户尝试滑动它们时,导致应用程序挂起,但我们已经修复了这个问题。
以下是文章中讨论的主题:
- 重新组合计数。准确定位过多的重新组合。 2. 编写编译器指标。识别过多重新组合的根本原因。 3. CPU 分析。查找“热点”方法并释放 CPU。 4. GPU 分析。了解哪些组件绘制时间较长。 5. 有关使用分析工具识别的错误修复的更多提示。
重新组合计数。准确定位过多的重新组合
问题。 我们需要强调的第一个概念是recomposition。
在Compose中渲染屏幕包括遍历可组合函数的图形。如果图形节点更改其状态(即可组合方法的参数、MutableState的值或动画更改),则更新子图,即再次调用函数。这样的更新称为重新组合。
重新组合速度快,不应出现任何问题,但频繁重新组合可能导致应用程序变慢。
这是 Compose 的一个独特特性和一个陷阱。毕竟,开发人员可能会犯一些错误,导致过多的重组,例如,重新创建一个对象而不是重用它。这会导致不必要的计算和性能问题。
解决方案。 在大型项目中,这些错误可能很难通过肉眼检测到,因此Android Studio为我们的眼睛提供了一种辅助工具——一个名为Recomposition Counts的工具。
在布局检查器中可以启用它,以查看可组合函数被调用或跳过的频率。按照此指南显示这些统计数据。
应用程序。 让我们以 Campus 为例,检查是否存在过多的重组。我们在模拟器中运行项目并导航到布局检查器。在启用了重组计数后,会出现两列新列,显示每个可组合方法的重组次数和跳过次数。现在我们可以看到,每次向下滑动到下一天时,ScheduleDayPage 和 RemoteStateContent 方法会重组三次,而不是一次,并且根本不会被跳过。
这意味着我们已经成功找出了问题,并确定了需要更仔细查看的方法:
Compose编译器指标. 识别过多重组成的根本原因
稳定的数据类型
问题. 要理解为什么一个方法可以被多次重组,我们需要学习 稳定性 的概念。
稳定性使编译器确保类型要么不改变,要么在发生更改时通知组合。如果所有参数都是稳定的,编译器不会检查组合方法是否已更改。
因此,稳定的数据类型是那些要么生成不可变实例,要么通知组合对象其状态更改的类型。除了稳定和不稳定的数据类型外,还有第三种类型——不可变类型。这是一种更严格的类型,确保对象根本不会改变。
更严格地说,稳定类型必须满足以下条件:
- equals 的结果对于相同的两个实例始终返回相同的结果。
- 当类的公共字段发生变化时,将会通知组合。
- 所有公共字段类型都是稳定的。
要满足第三个条件,必须有稳定的类型供开发人员使用,以创建他们自己的稳定数据类型。Jetpack Compose编译器认为以下类型是稳定的:原始类型、String、函数类型、枚举。
要深入了解重新组合和稳定数据类型概念,我建议阅读Denis Golubev的这篇文章。它演示了以下示例:
开发人员还可以使用 @Stable 和 @Immutable 注解标记类。
解决方案。 为了识别稳定和不稳定类型,以及可跳过和可重新启动的方法,我们可以使用Compose编译器指标工具,该工具可以检测到我们使用的Recomposition Counts。
为了获取项目的统计数据,我们需要在_app/build.gradle.kts_中添加一个任务,并按照文章中描述的方式启用相应的编译器标志运行发布构建。
因此,我们将在_build/compose_metrics_文件夹中看到四个包含以下内容的文件:
- app_release-classes.txt 包含有关类稳定性的信息:
- app_release-composables.txt 包含了方法是否可重组或可跳过的信息:
- app_release-composables.csv 包含相同的细节,但以表格形式呈现。
- app_release-module.json 包含有关项目的一般信息:
应用程序。 让我们回到校园:我们对 ScheduleDayPage 方法感兴趣。要查找有关它的信息,我们将检查 app_release-composables.txt 文件:
正如我们所看到的,该方法不可跳过,在适用时将被重新组合。我们还可以注意到状态不是一个稳定的参数。
为了解决这个问题,我们可以在确保这些类在构造后不会更改之后,将RemoteState和ScheduleDay类标记为@Immutable。
不要将此注释贴在具有 var 字段或包含列表的字段的类上。
这将解决类不稳定性的问题,但我们还没有完成这个方法。该指标将其标记为可跳过,但在布局检查器中,我们仍然可以看到过多的重组。
不稳定列表
问题. 有一种方法可以使用注解来覆盖类的稳定性,但无法解决列表、集合和映射的稳定性问题。
解决方案。 Chris Ward提供了解决此问题的方法,该方法使用了kotlinx-collections-immutable库,该库允许您指定一个组合方法应该将不可变列表作为参数。
不稳定的lambda表达式
问题. 我们的 ScheduleDayPage 类也有函数参数,您在 Compose 中要小心处理。
让我们来看看方法初始化部分:
看看我们如何将函数传递给我们的 ScheduleDayPage 方法。
Compose有一个不稳定lambda的概念,由Justin Breitfeller进行了详细描述。
他的文章中一个亮点是编译器如何处理 lambda;即,它创建一个带有 invoke() 方法的匿名类,将 lambda 内容放入其中。换句话说,每次传递 lambda 时,我们都会创建一个匿名类的对象,该对象没有哈希值,无法让编译器在重组阶段进行比较。因此,编译器认为图节点状态已更改,需要重新组合。
因此,Compose编译器指标不会将lambda标记为不稳定,但重新组合仍会发生。
除了传递的参数外,lambda表达式可能还有外部参数(例如上下文),这可能导致编译器生成的类之间存在差异。
解决方案。 同一篇文章提出了四种处理这个问题的方法。
1. 方法引用。 通过使用方法引用而不是lambda表达式,我们可以防止生成新的类。方法引用是稳定的函数类型,在重新组合之间保持等效。
2. 记住。 另一个选择是在重新组合之间记住 lambda 实例。这将确保 lambda 的确切实例在进一步的组合中被重用。
3. 静态函数。 如果 lambda 只是调用顶层函数,编译器会认为它是稳定的,因为顶层函数不接受像上下文这样的外部参数。
4. 在 lambda 中使用 @Stable 类型。 只要 lambda 仅捕获其他稳定类型,编译器在图重新组合时不会对其进行更新。
应用。 回到校园,让我们利用这些新知识来修复不正确的 lambda 传递,就像这样:
CPU性能分析。查找“热点”方法并释放CPU
CPU 专家分析器
CPU性能分析 是解决应用程序卡顿问题最强大的工具。此外,优化应用程序的CPU使用率还有许多其他好处,如更快更流畅的用户体验和改善设备的电池续航时间。
您可以使用分析器来检查应用程序的 CPU 使用情况以及与应用程序交互期间的线程活动。
应用程序。 Takeshi Hagikura的这篇文章 是一个很好的指南,可以帮助你启动和运行CPU分析器。让我们以Campus为例,看看这些统计数据有什么用。在模拟器中运行项目,然后转到Profiler选项卡。
一旦应用程序初始化,将显示 CPU、内存和能量图表。我们将专注于 CPU 统计信息。在其详细视图中,您应该看到以下内容:
点击“记录”以记录统计数据,与应用程序进行交互一段时间,然后点击“停止”.
记录创建后,您应该看到以下屏幕显示在记录间隔(1)内的 CPU 使用情况,应用程序交互统计(2),线程(3)和详细线程分析(4)。
火焰图
接下来,我们将专注于 Flame Chart 选项卡,其中包含了带有关联 CPU 使用时间的函数调用图表。该图表有助于识别运行时间超出预期的进程,从而进行优化。
首先,在 CPU 使用率窗口中,我们选择感兴趣的时间间隔。然后我们可以在线程窗口中找到它。让我们选择最突出的条形图。
我们应该专注于以下价值观:
1. 组件绘制时间。 大多数智能手机屏幕的标准刷新率为60 Hz。这意味着显示的图像每秒完全更新60次,即每16.67毫秒更新一次。
因此,为了使用户界面更加流畅,这应该是绘制组件的最长时间。因此,请注意“最热门”的方法。
计算绘图时间时,请考虑 Flame Chart 中数值的比例与所选时间间隔的秒数。确切的时间间隔可在摘要选项卡中找到。
2. CPU使用率。 尽量让CPU大部分时间处于空闲状态,让它保持“冷静”。
3. 我们可以影响的组件。 您可以使用搜索栏进行选择。例如,如果您搜索项目名称,分析器将以颜色突出显示“火焰”中与您方法相关的部分,并将方法栏中的文本加粗显示。
4. 我们可以将方法移到另一个线程。 一些密集型任务可以分离到另一个线程中。例如,可能是与数据库的交互。
阅读 此文章 以获取有关 CPU 分析器功能的更多详细信息。
应用。 现在让我们回到校园。在我们的情况下,我们在时间表屏幕上滑动,我们希望优化。让我们在 CPU 使用情况窗口中选择一个,在线程窗口中选择主线程。然后我们可以搜索我们的项目:这会突出显示三个占用大量 CPU 时间的方法。
我们将仅检查其中一个。WeekPage 方法在4秒间隔中占用了长达400毫秒。为了更精确地计算,我们应该对几个值进行平均。请注意此方法使用的 CPU 时间的大致值:95 毫秒。
查看我们的代码,我们可以看到一个明显的缺陷:SimpleDateFormat 在每个Row的循环体中被初始化。我们可以通过将初始化移出Row并使用remember来修复这个问题。
修复后,让我们来检查结果。通过这样做,我们将绘制WeekPage所需的时间缩短到60-70毫秒(图像显示大约1秒的间隔):
System Trace
您还可以获取仅关注可组合方法的 CPU 使用情况统计。要做到这一点,请使用 Jetpack Compose 组合跟踪工具。
Jetpack Compose Composition Tracing is available starting with the following versions:
- Android Studio Flamingo Canary 1,
- Compose UI 1.3.0-beta01,
- Compose Compiler 1.3.0.
Jetpack Compose 组合跟踪可以在 System Trace Android Studio Flamingo 中显示 Jetpack Compose 的可组合函数。请参考 Ben Trengrove 的文章中的说明,安装适当版本的 Android Studio,然后将依赖项添加到 app/gradle.kts。
应用程序。 在 CPU 分析器中选择系统跟踪配置,点击记录,与应用程序进行交互一段时间,然后点击停止。
记录创建后,您应该看到以下屏幕:
在检查系统跟踪时,您可以查看线程时间轴中的跟踪事件,以查看每个线程中发生的事件的详细信息。
我们有几个新标签:显示(1)、帧生命周期(2)、CPU核心(3)和进程内存(4)。现在,线程标签看起来有点不同,显示了可组合函数调用的图表。
这些选项卡显示每个核心的活动情况,应用程序当前使用的物理内存大小等信息。
Threads 选项卡显示调用图表,而详细的线程统计数据还显示 Flame Chart,其中仅显示可组合方法统计数据。
在 Threads 中,您可以单击一个方法来搜索整个记录的时间间隔,并查看该线程被调用的次数以及每次调用的平均耗时。
阅读 官方文档 以获取有关系统跟踪功能的更多详细信息。
GPU性能分析。学习哪些组件绘制时间较长
另一个重要的性能分析工具是GPU性能分析器。
文档说明,Profile GPU Rendering 工具显示一个滚动条形图,显示用户界面渲染每帧所需的时间,与每帧的参考值 16.67 毫秒进行对比。
要使用此分析器,您需要运行Android 4.1(API级别16)或更高版本的设备。有关如何启用它的指南,请参阅文档。
绿色水平线代表16.67毫秒的数值。要达到每秒60帧,每帧的垂直条都必须保持在这条线以下。每当一条条超过这条线时,动画可能会出现卡顿。
文档描述了条形颜色编码。
应用。 以校园为例,我们可以注意到突出的蓝色、浅绿色和深绿色条形。
这表明创建和更新列表(蓝色条)需要很长时间,这可能是因为我们有许多自定义视图或onDraw方法执行了一些密集的工作。处理onLayout和onMeasure方法(浅绿色条)也需要很长时间,这可能表明绘制了一个复杂的视图层次结构。此外,大量时间花费在为视图执行的动画和处理输入回调(深绿色)上;在滚动期间进行的视图绑定,例如RecyclerView.Adapter.onBindViewHolder(),通常也发生在此阶段,并且是其中最常见的减速来源。
接下来的图片展示了在上述各种优化措施描述之后的应用程序图表。
结果显示仍有优化空间。
使用性能分析工具识别的错误修复更多提示
我从官方文档和Mukesh Solanki的这篇文章中收集了一些小贴士。
1. 在可组合的方法中始终使用 remember。 重新组合可能因多种原因随时发生。如果您有一个值应该在重新组合后保留,remember 将帮助您保持这种状态。
2. 仅在必要时使用惰性布局。 对于包含五个项目的列表,使用 LazyRow 可能会显著减慢渲染速度。
3. 如果可能的话,请尽量避免使用 ConstraintLayout。 改用 Column 和 Row。ConstraintLayout 是一种线性方程系统,需要比逐个生成元素更多的计算。
在我们的博客中 阅读更多关于设计.