深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
本文译自:chris.banes.dev/composable-…
原标题:Composable Metrics
译:FunnySaltyFish
当一个团队开始使用Jetpack Compose
时,他们中的大多数人最终会发现少了一块拼图:如何测量可组合项(Composable
)的性能。
在 Jetpack Compose 1.2.0
中,Compose 编译器添加了一个新功能,它可以在构建时输出各种与性能相关的指标,让我们能够窥视幕后,看看潜在的性能问题在哪些地方。在这篇博文中,我们将探索新的指标,看看我们能找到什么。
在开始阅读之前需要了解的一些事项:
- 最终写完后的结果显示,这是一篇很长 的博文,涵盖了 Compose 的许多工作原理。所以阅读这篇文章可能得花点时间。
- 本文仅仅设立了一些预期,到结尾也没有真正做成什么“明显的成效”😅。但是,希望您能更好地了解您在设计上的选择将如何影响 Compose 的工作方式。
- 如果您没有立即理解这里的所有内容,请不要感到难过——这是一个高级 主题!如果您有什么疑惑,我已尝试列出相关资源以供进一步阅读。
- 我们在这里捣鼓的一些事情可以被认为是“细微优化”。与任何涉及优化的任务一样:首先profile(分析)和test(测试)! 新的JankStats 库是一个很好的切入点。如果您在真实设备上的性能没有问题,那么在这上面您可能无需做太多事情。
有了这个,让我们开始吧…🏞
启用指标
我们的第一步是通过一些编译器标志启用新的编译器指标。对于大多数应用程序,在所有模块上启用它的最简单方法是使用全局 开/关 开关。
在您的根目录build.gradle
中,您可以粘贴以下内容:
subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {if (project.findProperty("myapp.enableComposeCompilerReports") == "true") {
freeCompilerArgs += ["-P","plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_metrics"]
freeCompilerArgs += ["-P","plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_metrics"]}}}
}
每当您在myapp.enableComposeCompilerReports属性被启用的情况下运行 Gradle 构建时,这都会启用必要的 Kotlin 编译器标志,如下所示:
./gradlew assembleRelease -Pmyapp.enableComposeCompilerReports=true
一些注意事项:
- 请在release版本上运行它,这很重要。 我们稍后会看到为什么。
- 您可以根据需要重命名该myapp.enableComposeCompilerReports属性。
- 您可能会发现您需要同时使用 --rerun-tasks 选项运行上述命令,以确保 Compose 编译器即使在有缓存的情况下也正常运行。
相应指标和结果报告将被写入每个模块的构建目录中的compose_metrics
文件夹。一般情况来说,它将位于<module_dir>/build/compose_metrics
. 如果您打开其中一个文件夹,您会看到如下内容:
注意:从技术上讲,报告 ( module.json ) 和指标(其他 3 个文件)是单独启用的。我已将它们合并为一个标志并将它们设置为输出到同一目录以方便使用。如果需要,您可以拆分它们。
解释报告
如上所示,每个模块有 4 个文件输出:
module-module.json
,其中包含一些整体统计数据。module-composables.txt
,其中包含每个函数声明的详细输出。module-composables.csv
,这是文本文件的表格版本module-classes.txt
,其中包含从可组合项引用的类的稳定性信息。
这篇博文不会深入探讨所有文件的内容。为此,我建议通读“解释 Compose 编译器指标”文档,也是本篇的参考文档:
androidx/compiler-metrics.md at androidx-main · androidx/androidx
相反,我将依次过一下上面文档中“注意事项”部分中列出的信息的要点👑 ,并看看我的Tivi 应用程序的某个模块是个什么情况。
我要研究的ui-showdetails
模块是包含“显示详细信息”页面的所有 UI 的模块。它是我在 2020 年 4 月 转成 Jetpack Compose 的首批模块之一,所以我确信还有一些需要改进的地方!
好的,所以首先要注意的是…
是restartable但不是skippable 的函数
首先,让我们定义术语 “可重启(restartable)
” 和 “可跳过(skippable)
”。
在学习 Compose 时,您会学习到重组——它是 Compose 工作方式的基础:
重组是当输入改变时再次调用你的可组合函数的过程。当函数的输入发生变化时会发生这种情况。当 Compose 基于新输入进行重构时,它只调用可能已更改的函数或 lambda,并跳过其余部分。
可重启
“可重启”的函数是重组的基础。当 Compose 检测到函数输入发生变化时, 它便使用新输入重新启动(重新invoke)此函数。
更进一步地看看 Compose 的工作原理,可重启的函数标志着composition“范围”的边界。Snapshot (比如 MutableState)被读取到的“范围”很重要,因为它定义了在 快照(snapshot)更改时被重新运行的代码块。理想情况下,快照更改将尽可能仅触发最近的 函数/lambda 重启,使得被重新运行的代码最少化。如果宿主代码块无法重启,则 Compose 需要遍历树以找到最近的祖先可重启的“范围”。这可能意味着很多函数需要重新运行。实际上,几乎所有@Composable函数都可以重启。
可跳过
如果 Compose 发现自上次调用以来参数未更改,则它可以完全跳过调用此函数,则可组合函数是“可跳过的”。 这对于“顶级”可组合项的性能尤为重要,因为它们往往位于 Composable树的最上面一部分。如果 Compose 可以跳过“顶级”调用,则也不需要调用其之下任何函数。
在实践中,我们的目标是让尽可能多的可组合项可跳过,以允许 Compose ‘智能重组’。
蛋疼的事情是,参数值是否发生变化 是怎么定义的——我们需要引入另外两个术语:稳定性(Stablility)和不变性(Immutability)。
稳定性(Stablility)和不变性(Immutability)
可重启和可跳过是** Compose 函数** 的属性,而不变性和稳定性是对象实例的属性,尤指传递给可组合函数的对象。
不可变的对象意味着“所有public属性和字段在构造实例后都不会更改”。这个特征意味着 Compose 可以很容易地检测到两个实例之间的“变化”。
另一方面,稳定的对象不一定是不可变的。一个稳定的类可以保存可变数据,但所有可变数据都需要在发生变化时通知 Compose,以便在必要时进行重组。
当 Compose 检测到所有函数参数都是稳定或不可变时,它可以在运行时启用许多优化,这也正是函数能够被跳过的关键。Compose 会尝试自动推断一个类是不可变的还是稳定的,但有时它无法正确推断。当发生这种情况时,我们可以在类上使用@Immutable
和@Stable
注解
简要解释了这些术语后,让我们开始探索指标数据。
探索指标数据
我们将从module.json
文件开始以了解整体统计信息:
{
"skippableComposables": 64,
"restartableComposables": 76,
"readonlyComposables": 0,
"totalComposables": 76
}
我们可以看到该模块包含 76 个可组合项:它们都是可重启 的,其中 64 个是可跳过 的,剩下 12 个则是可重启 但不可 跳过 的函数。
现在我们需要找出具体的对应关系。我们有两种方法可以做到这一点:查看composables.txt
文件,或者导入composables.csv
文件并将其当做电子表格查看。我们稍后会查看文本文件,所以现在让我们看一下电子表格。
在过滤Composable列表后(工作表上有一个“不可跳过”的过滤视图),我们可以轻松找到不可跳过的函数:
ShowDetails()
ShowDetailsScrollingContent()
PosterInfoRow()
BackdropImage()
AirsInfoPanel()
Genres()
RelatedShows()
NextEpisodeToWatch()
InfoPanels()
SeasonRow()
使函数可跳过
现在我们的工作是依次查看上述的每一个函数,并确定它们不可跳过的原因。如果我们回到文档,它上面这样写到:
如果您看到一个可重启但不可跳过的函数,这并不总是一件坏事;反之,有时,这告诉我们该做做下面这两件事之一: 1。通过确保函数的所有参数稳定来使函数可跳过 2. 通过 @NonRestartableComposable
将函数标记为不可重启函数
现在,我们将专注于第一件事。所以让我们继续查看composables.txt
文件,并找到不可跳过的Composable之一:AirsInfoPanel()
:
restartable scheme("[androidx.compose.ui.UiComposable]") fun AirsInfoPanel(
unstable show: TiviShow
stable modifier: Modifier? = @static Companion
)
我们可以看到该函数有 2 个参数:modifier
参数是'stable'
(👍),但show
参数是'unstable'
(👎),这很可能就是导致 Compose 确定该函数不可跳过的原因。但是现在问题变成了:为什么 Compose 编译器会认为TiviShow是不稳定的?它只是一个只包含不可变数据的数据类而已啊。🤔
classes.txt
理想情况下,我们应该参考此处引用的module-classes.txt
文件以深入了解该类被推断为不稳定的原因。不幸的是,该文件的输出似乎零零散散的。在某些模块中,我可以看到必要的输出;但对于有些模块,它甚至可能是个空文件(这个模块就是)。
不过,我们可以换个不同模块的示例看看。它看起来就蛮有用的:
unstable class WatchedViewState {
unstable val user: TraktUser?
stable val authState: TraktAuthState
stable val isLoading: Boolean
stable val isEmpty: Boolean
stable val selectionOpen: Boolean
unstable val selectedShowIds: Set<Long>
stable val filterActive: Boolean
stable val filter: String?
unstable val availableSorts: List<SortOption>
stable val sort: SortOption
unstable val message: UiMessage?
<runtime stability> = Unstable
}
从classes.txt
输出的判断来看,Compose 编译器似乎只能推断 启用了 compose的模块下的类 的不变性和稳定性。Tivi 中的大多数Model类都构建在标准的 Kotlin 模块中(即没有包含 Android 或 Compose),然后在整个应用程序中使用。对于从外部库(比如ViewModel
)使用的类,我们也有类似的情况。
不幸的是,如果没有额外的工作,我们现在似乎无法解决这个问题。理想情况下,Compose 使用的注释(比如@Stable)将能被分离到一个纯 Kotlin 库中,允许我们在更多地方使用它们(如有必要,甚至可以是 Java 库)。
把类包装一下
如果您发现您的可组合项成了性能上的绊脚石,且启用可跳过性是实现无卡顿的关键时,您可以将被错误推断的、实际稳定的对象包装起来,例如:
@Stable
class StableHolder<T>(val item: T) {operator fun component1(): T = item
}
@Immutable
class ImmutableHolder<T>(val item: T) {operator fun component1(): T = item
}
缺点是您需要在可组合声明中这样使用它们:
@Composable
private fun AirsInfoPanel(
show: StableHolder<ShowUiModel>,
modifier: Modifier = Modifier,
)
不过,我们可以更进一步,探索许多团队推荐的模式:UI 相关的 Model 类。
UI Model 类
这些 Model 类是针对每个“屏幕”构建的,包含显示 UI 所需的最少信息。通常,您ViewModel
会将数据层模型映射到这些 UI 模型中,以便您的 UI 易于使用。更重要的是,它们可以直接写在您的可组合项旁边,这意味着 Compose 编译器可以推断出它需要的所有内容;或者即使其他所有方法都没用,我们也可以根据需要添加@Immutable
or@Stable
。
这正是我在以下PR中实现的:
github.com/chrisbanes/…
在我的数据层(比如数据库啥的)中,我们不再直接使用TiviShow
作为模型,而是将显示数据映射到仅包含 UI 所需的必要信息的ShowUiModel
中。
不幸的是,这还不足以让 Compose 编译器推断ShowUiModel
为可跳过 😔:
restartable scheme("[androidx.compose.ui.UiComposable]") fun AirsInfoPanel(
unstable show: ShowUiModel
stable modifier: Modifier? = @static Companion
)
同样不幸的是,指标中没有任何明显的东西可以说明为什么该类会被推断为不稳定。在查看了composables.txt
文件的其余部分后,我注意到另一个函数也被认为是不稳定的:
restartable scheme("[androidx.compose.ui.UiComposable]") fun Genres(
unstable genres: List<Genre>
)
我的新ShowUiModel类
是一个数据类,它包含许原始类型和枚举类,但一个属性略有不同,因为它包含枚举列表:genre: List<Genre>
. 似乎 Compose 编译器不把List当做稳定的(public issue)。
我发现强制让 Compose 认为ShowUiModel是稳定的唯一方法是:使用@Immutable或@Stable注解。因为其所有属性均不可变,所以我使用@Immutable,
@Immutable
internal data class ShowUiModel(// ...
)
之后,AirsInfoPanel()终于被认为是可以跳过的了😅:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun AirsInfoPanel(
stable show: ShowUiModel
stable modifier: Modifier? = @static Companion
)
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ic Companion
)
[外链图片转存中...(img-KBO7WraI-1715892889156)]
[外链图片转存中...(img-3UpmbDP2-1715892889157)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**