侧边栏壁纸
博主头像
爱探索

行动起来,活在当下

  • 累计撰写 54 篇文章
  • 累计创建 14 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

Compose 编译器优化检测与去黑盒化指南

jelly
2026-07-02 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

Compose 编译器优化检测与去黑盒化指南

在Compose 开发中,编译器为了提高性能、安全等问题,会对Compose代码进行优化,如对 Composable Lambda 进行自动 memoize(remember 化)。这就导致了可能会出现一些意料之外的事情。为了避免“黑盒开发”,我们可以从 编译期报告、字节码反编译(最直接的铁证) 以及 运行期监测 三个维度,彻底让 Compose 编译器的行为透明化。

一、启用官方 Compose Compiler Metrics & Reports

Compose 编译器内置了强大的分析工具,可以在打包时将所有 Composable 函数的“可跳过性(Skippable)”以及参数的“稳定性(Stable)”导出为纯文本报告。

配置方式


android {
    // ...
}

kotlin {
    compilerOptions {
        // 定义报告输出路径
       composeCompiler {
            reportsDestination = layout.buildDirectory.dir("compose_metrics")
            metricsDestination = layout.buildDirectory.dir("compose_metrics")
        }
    }
}

配置之后编译代码,在你的 app/build/compose_metrics/ 目录下会生成四个文件。

  • *-classes.txt (类稳定性报告 TXT)
  • *-composables.txt (组件跳过性报告 TXT)
  • *-composables.csv (组件数据表 CSV)
  • *-module.json (全局统计 JSON)

*-classes.txt (类稳定性报告 TXT)

*-classes.txt 是 Compose 编译器对项目中所有数据模型(Model/Entity/State)进行静态稳定性推断的成绩单。一个类在报告中的稳定性直接决定了接收该类作为参数的 Composable 函数能否被跳过(Skip)重组。

*-classes.txt结构


[稳定性修饰符] [类类型] [包名].[类名] {
  [稳定性修饰符] [val/var] [属性名]: [属性类型]
  ...
  <runtime stability> = [运行时稳定性解析表达式]
}

稳定性修饰符:

稳定性修饰符代表含义编译器处理行为
stable绝对稳定保证这个东西的值不会在 Compose 无法感知的情况下发生改变。既然数据没变,重组时直接跳过,不用重新绘制
unstable绝对不稳定无法保证这个东西在运行期会不会被在暗中修改,因此为了安全起见,每次重组必须重新计算它。
runtime动态/运行时决定多见于泛型类。编译器在编译期无法给出定论,需要在运行期根据传入的实际泛型参数来动态计算稳定性。

类类型:

修饰符代表含义说明
class普通的类或数据类
interface接口由于接口可以有无数种未知的实现,默认一律被判为 unstable(除非声明了 @Stable)。
object单例对象默认一律判为 stable,它是 JVM 上的全局唯一实例,类型本身即代表唯一引用,Compose 编译器对所有 object 类型直接判定为 Stable 类型。

< runtime stability >

这一行是 Compose 运行时在内存中决定如何对待该类的终极算法表达式。它是编译器留给运行时的“计算公式”:

  • = Stable

    含义:运行时直接判定为稳定。
    示例:数据类中所有属性均为 stable val。

  • = Unstable

    含义:运行时直接判定为不稳定。

    示例:只要类包含 unstable var,或者不可避免的 unstable val。

  • = Parameter(T)

    含义:该类的稳定性,完全取决于泛型参数 T 的实际类型。

    Kotlin 源码:data class Wrapper< T >(val data: T),
    运行时行为:如果运行期你传入 Wrapper< String >,它就是 Stable;如果你传入 Wrapper< UnstableClass >,它在运行时就动态变成 Unstable。

一些常见状态的说明:
  1. stable val:属性是只读的,且其类型是稳定的基本类型(如 Int, String 等)或已被标记为 stable 的类
  2. unstable val (隐蔽刺客):虽然属性本身是只读的(val),但它的类型本身是不稳定的。例如:unstable val tags: List< String >(List 类型不稳定)或 unstable val config: UnstableConfig。
  3. unstable var (直接沦陷):使用了可变属性 var,不论其类型有多稳定,因为可随时被修改且不可追踪,直接导致所属类沦陷
  4. stable var (极少见的特例):仅当你将一个属性委托给了一个可以感知变化的系统,且满足特定条件,或者该属性是经过编译器特殊处理的合成属性时才会出现。

*-composables.txt

如果说 classes.txt 是数据模型的“基因图谱”,那么 *-composables.txt 就是 Compose 编译期对所有 UI 组件(Composable 函数)下达的“命运审判书”。

它详细记录了你写下的每一个 @Composable 函数在编译后的优化状态、参数稳定性,以及它们在重组发生时的表现

*-composables.txt结构说明

在 *-composables.txt 中,每一个 @Composable 函数经过编译器扫描后,都会严格按照以下格式输出:


[特性修饰符列表] scheme("[渲染模式]") fun [函数名](
  [稳定性修饰符] [参数名]: [参数类型] = [默认值评估]
)

特性修饰符:fun 最前方的修饰符,决定了该组件在 Compose 运行时(Runtime)中重组的底层行为

特性修饰符底层定义与运作机制调优目标
restartable可重启的(可作为重组边界)必须。大部分 UI 节点都需要此特性。
skippable可跳过的(性能终极目标),代表在重组发生时,如果该函数接收的所有参数与上一次相比都没有变化,Compose 运行时可以直接**跳过(Skip)**该函数的执行,直接复用上一次的渲染结果。核心优化指标。商业项目中的核心、高频组件必须实现
readonly只读的(极速轻量)。readonly 函数不生成 restart group,不作为重组边界,执行时不创建独立的 Composer 作用域,开销显著低于普通 Composable。常用于仅展示静态文本或纯图标的组件。静态 UI 的最高境界。

一个函数必须先是 restartable,才有可能被优化为 skippable。如果一个函数连“重启边界”都不是,那它在父组件刷新时必然会被强制重新执行。

渲染模式 (Scheme)

  • scheme("[类型引用]"):用是在 Slot Table 中辅助定位和识别该函数的调用组。

默认值评估 (Default Value Spec)

当你的 Composable 函数中存在带默认值的参数时(如 modifier: Modifier = Modifier),编译器会在等号(=)右侧输出默认值的静态/动态分析结果。这在真实报告中是唯一能看到 @static 和 @dynamic 的地方:

  • = @static [value]:

    含义:默认值表达式在编译期即可被判定为静态常量(例如 Modifier.Companion 或普通的字面量 0、true)。

  • = @dynamic [expression]:

    含义:默认值表达式是动态的,必须在运行期执行时去计算(例如默认值读取了 LocalContext.current,或调用了其他的 Composable 函数来获取初始状态)。

*-composables.txt实际用途

  1. 揪出“非 skippable”的性能黑洞

    在几万行的项目中,通过过滤 *-composables.txt 中只有 restartable 但没有 skippable 的函数,你可以一秒抓出哪些组件在“无脑重组”。

  2. 拆解“参数连带责任”

    如果一个很复杂的 UI 组件不能 skip,打开这个文件,它会把所有的参数列成一个清单。你只需要像看体检单一样,在清单里找到那个写着 unstable 的参数,它就是罪魁祸首

  3. 分析 Lambda 逃逸(Anonymous Classes)

    如果你的 Composable 接收了一个 Lambda 表达式作为回调:

    fun MyButton(onClick: () -> Unit)
    

    如果 onClick 被标为了 unstable,说明你的 Lambda 没有被编译器成功 Memoize(静态缓存)。这能帮你快速判定是不是因为 Strong Skipping 没开启,或者捕获了不稳定变量。

*-composables.csv (组件数据表 CSV)

package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defaultsGroup,groups,calls

  • package:该 Composable 函数所在的完整 Kotlin 包路径。
  • name:name (函数名)
  • composable:(可组合性),该函数是否标有 @Composable 注解。取值为 true 或 false。
  • skippable :可跳过性 ,是否为可跳过。取值为 true 或 false。当父重组发生时,若此组件传入的参数引用均未改变,它是否可以直接不执行。
  • restartable:可重启性,是否可作为重组边界。取值为 true 或 false。当该函数内部的状态(State)发生改变时,Compose 运行时是否能直接以此函数为起点进行重组,而不用往上刷新父节点。
  • readonly:只读性,该组件是否仅用于静态渲染。取值为 true 或 false。内部没有状态写入,也没有任何会导致重组的逻辑(多见于纯展示的文字、本地静态 Icon)。若为 true,Compose 运行时在处理它时开销极低。
  • inline :内联状态,该函数是否使用了 Kotlin 的 inline 关键字。取值为 true 或 false。如果为 true,说明它的代码会被直接平铺编译到调用处。内联函数没有自己的重组边界,因此它的 restartable 和 skippable 恒为 false。 这属于正常现象,无需进行稳定性优化。
  • isLambda :是否为匿名 Lambda,该函数是否为一个匿名 Composable Lambda 表达式(例如通过 content = { ... } 传递给子布局的块),如果一个高频调用的 Composable 产生的 Lambda 在这里大量显示为 skippable == false,说明你的闭包可能捕获了不稳定的外部变量,未能被编译器自动 remember
  • hasDefaults: 是否包含默认参数,函数是否声明了带有默认值的参数。取值为 true 或 false
  • defaultsGroup :编译器用于标记和解析默认值控制流的整数 ID 或标记。如果没有默认参数,一般为 -1
  • groups:Composer 组计数,该 Composable 函数内部在重组槽位表(Slot Table)中创建的 Composer Group(控制组) 的数量。数值越高,代表该函数内部的控制流结构(例如 if/else、when 分支和条件节点)越复杂。它是度量组件结构复杂性的硬核指标。
  • calls :子组件调用计数,该 Composable 函数在其函数体内部直接调用的其他 Composable 函数的个数。如果一个组件的 calls 数值极大(例如大于 15),通常说明这个组件承载了过多的视图层级,建议进行微型组件拆分。

几个可以快速简单定位、分析的组合

skippablerestartableisLambdainline诊断结论性能表现与调优建议
truetruefalsefalse健康状态最佳。属于标准的、可被完美优化的可跳过独立 UI 组件。
falsetruefalsefalse亚健康/病态重点攻坚对象! 该组件本该能 skip,却因为传入了 unstable 参数导致失败。请立即倒查 *-classes.txt 优化其参数。
falsefalsetruefalse可疑的匿名 Lambda代表你写在闭包里的布局不能被跳过。检查其是否捕获了 Unstable 状态(如普通 List 或未用 remember 的外部局部变量)。
falsefalsefalsetrue内联正常状态正常。Inline 辅助布局(如自定义小 Row/Column 包裹器),无需调优

核心用途

  1. 导入表格进行 KPI 式的精准排查
  2. CI/CD 团队流水线“红线拦截”(Prevent Regression),在多团队协同或组件化的大型项目中,可以在 CI 流程(GitLab CI/ Jenkins/ GitHub Actions)上配置 Python 脚本解析此 CSV,以防性能指标退化

*-module.json (全局统计 JSON)


{
  "skippableComposables": 35,
  "restartableComposables": 45,
  "readonlyComposables": 0,
  "totalComposables": 45,
  "restartGroups": 45,
  "totalGroups": 52,
  "staticArguments": 205,
  "certainArguments": 9,
  "knownStableArguments": 996,
  "knownUnstableArguments": 2,
  "unknownStableArguments": 1,
  "totalArguments": 999,
  "markedStableClasses": 0,
  "inferredStableClasses": 0,
  "inferredUnstableClasses": 1,
  "inferredUncertainClasses": 0,
  "effectivelyStableClasses": 0,
  "totalClasses": 1,
  "memoizedLambdas": 66,
  "singletonLambdas": 0,
  "singletonComposableLambdas": 7,
  "composableLambdas": 30,
  "totalLambdas": 66,
  "featureFlags": {
    "StrongSkipping": true,
    "IntrinsicRemember": true,
    "OptimizeNonSkippingGroups": true,
    "PausableComposition": true
  }
}

  1. totalComposables

    字面意思:可组合函数总数。

    底层内涵:编译器在当前模块中扫描到的、所有被打上了 @Composable 标记的普通函数的物理总和。

    实际用途:它是衡量当前模块 UI 规模的基础指标,也是计算“可跳过率(Skippable Rate)”时的基础分母。

  2. skippableComposables

    字面意思:可跳过的可组合函数数量。

    底层内涵:在重组发生时,如果该函数接收的所有参数与上一次相比都没有发生改变,Compose 运行时可以直接调用 $composer.skipToGroupEnd() 跳过它的执行,不消耗任何 CPU。

    实际用途:最核心的性能指标。 该数值越接近 totalComposables 越好。在你的数据中,有 10 个组件(45 - 35)由于参数不稳定,未能达成此状态。

  3. restartableComposables

    字面意思:可重启的可组合函数数量。

    底层内涵:编译器在底层为其注入了重启代码(startRestartGroup 和 endRestartGroup),使其在运行期能够作为一个独立的重组边界作用域。当它内部的状态(State)改变时,Compose 可以只重新运行它,而不用刷新它的父函数。

    实际用途:基本上所有无返回值的普通 UI 组件默认都应该是可重启的。

  4. readonlyComposables

    字面意思:只读的可组合函数数量。

    底层内涵:内部没有任何状态(State)读取、没有任何槽位表(Slot Table)缓存占用(内部没有remember的情况下)、也不调用任何不稳定组件的纯静态函数。编译器会剥离它的重组防护罩,使其像普通 Kotlin 函数一样轻量执行。

    实际用途:常用于纯静态的本地 Icon、分割线或无状态小文本。如果可以,应尽量将纯静态 UI 优化为只读。

  5. restartGroups

    字面意思:可重启控制组的数量。

    底层内涵:编译器在底层插入的、用于记录和管理重组边界作用域的回调节点(Restart Group)的实际个数,在运行时与 restartableComposables 一一对应。

    实际用途:用来在槽位表中标识出有多少个物理节点在运行时负责监听和响应 State 的变化。

  6. totalGroups

    字面意思:控制组总数。

    底层内涵:编译器在槽位表中生成的组(Group)总数。除了重启组(restartGroups),它还包含了由 if/else 分支、when 分支在重组时生成的条件保护控制组,以及用于 remember 的缓存组。

    实际用途:用来评估函数内部的控制流复杂度。

  7. totalArguments

    字面意思:参数总个数。

    底层内涵:该模块中,所有可组合函数在调用处接收的实际参数(Parameters)总计数。

    实际用途:评估整个模块参数传递的总体体量。

  8. staticArguments

    字面意思:静态参数个数。

    底层内涵:指在编译期即可判定物理引用绝对不可能改变的实参(例如常量硬编码、字面量值 0、true 或硬编码 String)。

    实际用途:这些参数在运行期重组时,完全不需要进行任何比对,开销为 0。

  9. certainArguments

    字面意思:确定性参数个数。

    底层内涵:介于静态与普通变量之间。代表这些参数虽然在运行期不是常量,但在当前上下文中,编译器可以高度确信它们的变化范围。

    实际用途:用于编译器进行内部链路优化。

  10. knownStableArguments

    字面意思:明确已知的稳定参数个数。

    底层内涵:参数的类型为已被编译器笃定为 Stable 的类型(如基本类型、String、或标有 @Stable 的类)。在重组时,运行时可以通过普通的 equals 进行可靠比对。

    实际用途:这个值越高,代表项目整体参数设计越干净。你的数据中 996 个参数是 Stable 的(占比 99.7%),表现极其优秀。

  11. knownUnstableArguments

    字面意思:明确已知的不稳定参数个数。

    底层内涵:参数的类型被判定为 Unstable(如包含 var 的类,或标准的 List 集合)。重组时,Compose 无法通过 equals 确认其内容是否改变,因而强制刷新。

    实际用途:性能劣化的直接突破口。 你的数据中仅有 2 个不稳定参数。就是它们拖累了 10 个组件无法被 skip 重组。

  12. unknownStableArguments

    字面意思:未知或有争议的稳定参数个数。

    底层内涵:通常发生于多级泛型嵌套或复杂的接口多态。编译器在静态期持保守中立态度,需要留给运行期动态计算。

    实际用途:辅助分析,正常情况下数量极低。

  13. totalClasses

    字面意思:核心类总数。

    底层内涵:编译器在当前模块中扫描到的、参与了数据流转或作为参数传递到 UI 层的业务数据模型类(Model / State Class)总数。

    实际用途:宏观把握当前模块数据模型的复杂度。

  14. markedStableClasses

    字面意思:手动标记稳定的类数量。

    底层内涵:被开发者通过在类声明上方手动书写 @Stable 或 @Immutable 注解来强行背书、判定为稳定的类。

    实际用途:评估开发团队主动对数据模型实施优化的频率。

  15. inferredStableClasses

    字面意思:编译器自动推导稳定的类数量。

    底层内涵:类中所有的属性都是 val 只读,且类型均为 Stable,被编译器静态算法自主分析、判定为稳定的类。

    实际用途:体现了项目默认代码风格的优良度(全 val 只读类越多,该值越高)。

  16. inferredUnstableClasses

    字面意思:编译器自动推导不稳定的类数量。

    底层内涵:因为类中包含了 var 可变属性、标准 List 集合,或者依赖了跨模块未打补丁的第三方类,导致被编译器静态判定为不稳定的类。

    实际用途:性能调优的终极靶向目标。 你的数据中仅有 1 个类被标为不稳定,它就是制造那 2 个不稳定参数、阻碍 10 个组件跳过重组的“病毒源头”。

  17. inferredUncertainClasses

    字面意思:推导不确定的类数量。

    底层内涵:编译器无法确定稳定性,保持保守怀疑态度的类。

    实际用途:多为泛型承载类。

  18. effectivelyStableClasses

    字面意思:实际等效稳定的类数量。

    底层内涵:手动强行标记稳定与编译器自主推导稳定的类总和。

    实际用途:从宏观上展示当前模块有多少比例的数据类能被 Compose 放心信任。

  19. totalLambdas

    字面意思:匿名 Lambda 闭包总个数。

    底层内涵:整个模块中扫描到的所有匿名闭包函数(如事件回调 { onClick() })的总和。

    实际用途:用于监控匿名类生成的整体密度。

  20. memoizedLambdas

    字面意思:成功被缓存的 Lambda 个数。

    底层内涵:编译器在底层自动帮你用 remember 进行了包裹,使它们在重组时能完美复用同一个 Lambda 实例,不重复 new 对象。

    实际用途:衡量闭包健康度的黄金指标。 你的数据中为 66(占 totalLambdas 的 100%!),说明你项目里没有发生任何因 Lambda 重复创建导致的不必要重组,表现堪称完美。

  21. singletonLambdas

    字面意思:单例 Lambda 数量。

    底层内涵:由于闭包内部没有引用、捕获任何外部局部变量,编译器在底层直接将其优化为一个全局唯一的 static 静态单例对象。

    实际用途:衡量无状态事件回调的占比。

  22. composableLambdas

    字面意思:可组合的匿名 Lambda 数量。

    底层内涵:带有 @Composable 标记的匿名 Lambda 闭包(多作为 UI 插槽,例如 content: @Composable () -> Unit)。

    实际用途:展示插槽化闭包的密度。

  23. singletonComposableLambdas

    字面意思:可组合单例 Lambda 数量。

    底层内涵:带有 @Composable 标记,且因为没有捕获任何外部变量,被底层优化为全局唯一静态单例的闭包。

    实际用途:衡量无状态静态 UI 插槽的占比。

  24. featureFlags

    字面意思:特性优化开关。

    底层内涵:Compose 2.0 编译器底层各项最新优化引擎是否处于开启状态。

    实际用途:用于对齐和确认编译器的运行期版本和优化档位。

  25. StrongSkipping

    字面意思:强跳过模式。

    底层内涵:开启后,即便组件接收了 unstable 的参数(例如你这 2 个不稳定参数),运行时也会尽力在重组时做深层内容比对(equals() 进行相等性判断),强行实现跳过重组。它还会自动对不稳定的 Lambda 进行强记住(strong memoization),这是它解决"Lambda 每次重组都 new 一个实例"问题的关键。

    实际用途:极大减轻因为集合类型(如 List)导致的无脑重组。你当前处于开启状态(true)。

  26. IntrinsicRemember

    字面意思:内联记住优化。

    底层内涵:将 remember 槽位查找逻辑直接在调用处代码层“内联展开”,消除传统的运行时函数嵌套调用。

    实际用途:极大地减轻重组时的调用栈与寄存器开销。你当前处于开启状态(true)。

  27. OptimizeNonSkippingGroups

    字面意思:优化非跳过控制组。

    底层内涵:针对那 10 个确认无法跳过重组的组件,编译器在底层剔除多余的槽位表控制节点,降低其在内存中的印记。

    实际用途:减少不可跳过组件在运行期的内存和计算负担。你当前处于开启状态(true)。

  28. PausableComposition

    字面意思:可挂起组合机制。

    底层内涵:首次组合(initial composition)耗时过长导致丢帧的问题——它允许把一个耗时的初次组合拆分到多帧完成,而不是一次性阻塞主线程

    实际用途:为极限交互情况下的流畅度提供底层调度屏障。你当前处于开启状态(true)。

0

评论区