深入浅出 Compose Compiler(5) 类型稳定性 Stability

简介: 深入浅出 Compose Compiler(5) 类型稳定性 Stability

前言

上一篇文章中我们提到 Compose Compiler 在生成 Composable 函数体时,会通过 Stability#stabilityOf 判断参数的类型稳定性:

parameters.forEachIndexed { slotIndex, param ->
    val stability = stabilityOf(param.varargElementType ?: param.type)
    stabilities[slotIndex] = stability
    val isRequired = param.defaultValue == null
    val isUnstable = stability.knownUnstable()
    val isUsed = scope.usedParams[slotIndex]
    //...
    if (isUsed && isUnstable && isRequired) {
        // if it is a used + unstable parameter with no default expression, the fn
        // will _never_ skip
        mightSkip = false
    }
}

如果有不稳定的参数存在时,则 mightSkip = false,函数体中则不会生成 skipToGroupEnd 的代码,也即函数无法跳过重组,必须参与执行。

什么是稳定类型?

按照官方文档的定义,稳定类型必须符合以下条件:

developer.android.com/jetpack/com…

  • 对于相同的两个实例,其 equals 的结果将始终相同。
  • 如果类型的某个公共属性发生变化,组合将收到通知。
  • 所有公共属性类型也都是稳定。

另外,以下这些类型被 Compiler 默认为是稳定类型:

  • 所有基元值类型:Boolean、Int、Long、Float、Char 等。
  • 字符串
  • 所有函数类型 (lambda)

一个不稳定的类型,意味着 equals 结果不能保证始终相同,所以对于 Composable 参数来说,不稳定类型的比较结果不值得信赖,因此将始终参与重组。

接下来我们看一下 Stability#stabilityOf 中是如何判断类型稳定性的。

Stability 类型

Stability#stabilityOf 的结果是一个 Stability 类型,它有以下几种取值:

sealed class Stability {
    // class Foo(val bar: Int)
    class Certain(val stable: Boolean) : Stability() 
    // class Foo(val bar: ExternalType) -> ExternalType.$stable
    class Runtime(val declaration: IrClass) : Stability() 
    // interface Foo { fun result(): Int }
    class Unknown(val declaration: IrClass) : Stability()
    // class <T> Foo(val value: T)
    class Parameter(val parameter: IrTypeParameter) : Stability() 
    // class Foo(val foo: A, val bar: B)
    class Combined(val elements: List<Stability>) : Stability()
    companion object {
        val Stable: Stability = Certain(true)
        val Unstable: Stability = Certain(false)
    }
}
  • Certain:有明确的稳定性,Stable 或者 Unstable。
  • Runtime:成员中存在外部类型(来自三方jar包),外部类型稳定性在编译期无法确定,所以需要依靠运行时判断。Compiler 会生成基于 ������判断稳定性的运行时代码,stable判断稳定性的运行时代码,stable 本身也是 Compiler 为 Class 生成的静态变量,后文会提到。
  • Unknown:接口类型,不知道具体实现,无法得知稳定性
  • Parameter:类型带有泛型,稳定性由泛型参数类型决定
  • Combined:有多各成员,稳定性有多个成员共同决定,Combined 的中任意 element 不稳定,则整个类型不稳定

Stability#stabilityOf 推断稳定性

接下来看一下 stabilityOf 的实现,是如何决定 Class 的 Stability

private fun stabilityOf(
    declaration: IrClass,
    substitutions: Map<IrTypeParameterSymbol, IrTypeArgument>,
    currentlyAnalyzing: Set<IrClassifierSymbol>
): Stability {
    //...
    val symbol = declaration.symbol
    if (currentlyAnalyzing.contains(symbol)) return Stability.Unstable
    if (declaration.hasStableMarkedDescendant()) return Stability.Stable
    if (declaration.isEnumClass || declaration.isEnumEntry) return Stability.Stable
    if (declaration.defaultType.isPrimitiveType()) return Stability.Stable
    val analyzing = currentlyAnalyzing + symbol
    //...
 }

首先,以下这些类型,可以立即返回 Stability.Stable

  • hasStableMarkedDescendant(), 即添加了 @Stable 注解
  • isEnumClass 或者 isEnumEntry
  • isPrimitiveType():基本类型

stabilityOf 是一个递归调用,currentlyAnalyzing 记录当前类型,如果在递归中发现有当前类型的记录,则意味着此类型无法在递归逻辑中推断稳定性(上一轮不确定,这一轮自然也无法确定),所以判定为 Unstable。

接下来,需要获取类型的 mask 掩码信息。

    val fqName = declaration.fqNameWhenAvailable?.toString() ?: ""
    val stability: Stability
    val mask: Int
    if (stableBuiltinTypes.contains(fqName)) {
        //带有泛型的接口
        mask = stableBuiltinTypes[fqName] ?: 0
        stability = Stability.Stable
    } else {
        // IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB :外部类型
        mask = declaration.stabilityParamBitmask() ?: return Stability.Unstable
        stability = Stability.Runtime(declaration)
    }

这里 if..else 针对类型获取了 mask,mask 表示类型中存在泛型参数需要验证其类型的稳定性,1 表示需要验证。 下面分别来看一下 ifelse 代码块中是哪些类型,以及其 mask 来自哪里:

  • if 中的 mask 来自 stableBuiltinTypes 。这其中预定义了一些常见的带有泛型接口及其 mask:
private val stableBuiltinTypes = mapOf(
    "kotlin.Pair" to 0b11,
    "kotlin.Triple" to 0b111,
    "kotlin.Comparator" to 0,
    "kotlin.Result" to 0b1,
    "kotlin.ranges.ClosedRange" to 0b1,
    "kotlin.ranges.ClosedFloatingPointRange" to 0b1,
    // Guava
    "com.google.common.collect.ImmutableList" to 0b1,
    "com.google.common.collect.ImmutableEnumMap" to 0b11,
    "com.google.common.collect.ImmutableMap" to 0b11,
    "com.google.common.collect.ImmutableEnumSet" to 0b1,
    "com.google.common.collect.ImmutableSet" to 0b1,
    // Kotlinx immutable
    "kotlinx.collections.immutable.ImmutableList" to 0b1,
    "kotlinx.collections.immutable.ImmutableSet" to 0b1,
    "kotlinx.collections.immutable.ImmutableMap" to 0b11,
    // Dagger
    "dagger.Lazy" to 0b1,
)
  • else 中的 mask 来自 @StabilityInferred 注解,这个注解也是 Compiler 生成的,待会儿介绍。
private fun IrAnnotationContainer.stabilityParamBitmask(): Int? =
    (annotations.findAnnotation(ComposeFqNames.StabilityInferred)
        ?.getValueArgument(0) as? IrConst<*>
        )?.value as? Int

有了 mask 之后,我们看一下 mask 如何用的

return stability + Stability.Combined(
    declaration.typeParameters.mapIndexedNotNull { index, irTypeParameter ->
        if (mask and (0b1 shl index) != 0) {
            val sub = substitutions[irTypeParameter.symbol]
            if (sub != null)
                stabilityOf(sub, substitutions, analyzing)
            else
                Stability.Parameter(irTypeParameter)
        } else null
    }

如上,基于 mask 来决定哪些类型参数需要进一步判断稳定类型,参数的稳定型类型会合并到 Stability 返回。+ 运算符重载的实现如下:

operator fun plus(other: Stability): Stability = when {
    other is Certain -> if (other.stable) this else other
    this is Certain -> if (stable) other else this
    else -> Combined(listOf(this, other))
}

继续看 stabilityOf 的剩余实现

if (declaration.origin == IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) {
    //对于Java的类型,没法在Kotlin Compiler中推断稳定性,返回 Unstable
    return Stability.Unstable
}
if (declaration.isInterface) {
    //如果是接口类型,由于不知道具体实现是否是稳定类型,故返回 Unknown
    return Stability.Unknown(declaration)
}
var stability = Stability.Stable
//如果 Class 有成员,则判断成员稳定性
for (member in declaration.declarations) {
    when (member) {
        is IrProperty -> {
            member.backingField?.let {
                if (member.isVar && !member.isDelegated) return Stability.Unstable
                stability += stabilityOf(it.type, substitutions, analyzing)
            }
        }
        is IrField -> {
            stability += stabilityOf(member.type, substitutions, analyzing)
        }
    }
}

如上,当类型有成员或者属性时,会依次判断成员的 Stability 并合并返回。值得注意的是,如果类型中有 var成员,则该类型被认为是 Stability.Unstable 的。

如果是一个 Kt 的接口,则被认为是 Stability.Unknown,因为其实现类的稳定性不可知。Unknown 后续也会被当做 Unstable 处理。

此外,如果是一个Java类型,因为无法在 Kt 编译器中推断稳定性,也被认为是 Unstable 的。 掘金的 Pika 大佬曾在我的《Jetpack Compose 从入门到实战》一书中发现一处错误,还特意联系我进行了指正:

image.png

现在通过 Compose Compiler 的源码,可以找到答案了

Foo 的参数 List 是一个 Java 类型,所以被认为是 Unstable,编译后的函数体中也不会有 skipToGroupEnd 的相关代码:

@Composable 
fun Foo(bar: List<String>, $composer: Composer<*>, $changed: Int) {
    $composer.startRestartGroup(405544596)
    bar.forEach { Text(it) }
    $composer.endRestartGroup().updateScope {
        Foo(bar, $changed)
    }
}

在此也特别感谢 Pika 的反馈和指正! 后续如果加印会在书中勘误

@StabilityInferred 注解

前面提到了部分 Class 的 mask 来自 @StabilityInferred 注解,这个注解是 Compiler 为 Class 添加的辅助信息,帮助分析稳定性。相关实现在 ClassStabilityTransformer#visitClass 中,这里除了添加 @StabilityInferred 注解,还会为 Class 生成 $stable 静态变量,服务于 Stability.Runtime 的代码生成。

override fun visitClass(declaration: IrClass): IrStatement {
    val result = super.visitClass(declaration)
    val cls = result as? IrClass ?: return result
    if (
        cls.visibility != DescriptorVisibilities.PUBLIC ||
        cls.isEnumClass ||
        cls.isEnumEntry ||
        cls.isInterface ||
        cls.isAnnotationClass ||
        cls.isAnonymousObject ||
        cls.isExpect ||
        cls.isInner ||
        cls.isFileClass ||
        cls.isCompanion ||
        cls.defaultType.isInlineClassType()
    ) return cls
    if (declaration.hasStableMarker()) {
        return cls
    }
    //...
 }

对于符合上述条件的类型,则直接 return,不会生成辅助信息。因为这些类型的稳定性是明确的。

val stability = stabilityOf(declaration.defaultType).normalize()
var parameterMask = 0 //生成 mask,添加到 @StabilityInferred 注解
val stableExpr: IrExpression //生成 $stable = xx
if (cls.typeParameters.isNotEmpty()) {
    val symbols = cls.typeParameters.map { it.symbol }
    var externalParameters = false
    stability.forEach {
        when (it) {
            is Stability.Parameter -> {
                val index = symbols.indexOf(it.parameter.symbol)
                if (index != -1) {
                    // the stability of this parameter matters for the stability of the
                    // class
                    parameterMask = parameterMask or 0b1 shl index
                } else {
                    externalParameters = true
                }
            }
            else -> {
                /* No action necessary */
            }
        }
    }
    stableExpr = if (externalParameters)
        irConst(UNSTABLE)
    else
        stability.irStableExpression { irConst(STABLE) } ?: irConst(UNSTABLE)
} else {
    stableExpr = stability.irStableExpression() ?: irConst(UNSTABLE)
}

生成 mask 之后,添加 @StabilityInferred 注解等:

//添加 @StabilityInferred 注解,注解中包含 mask
cls.annotations = cls.annotations + IrConstructorCallImpl(
    UNDEFINED_OFFSET,
    UNDEFINED_OFFSET,
    StabilityInferredClass.defaultType,
    StabilityInferredClass.constructors.first(),
    0,
    0,
    1,
    null
).also {
    it.putValueArgument(0, irConst(parameterMask))
}
//生成 $stable 静态变量
val stabilityField = makeStabilityField().also { f ->
    f.parent = cls
    f.initializer = IrExpressionBodyImpl(
        UNDEFINED_OFFSET,
        UNDEFINED_OFFSET,
        stableExpr
    )
}
if (context.platform.isJvm()) {
    cls.declarations += stabilityField
}

测试 Compose 类型稳定性

上面的分析可以感受到 Compiler 对于类型稳定性的判断非常复杂,因为这对于提升 Compose 重组至关重要,稳定类型越多,重组的性能越好。Compose 1.2 之后新增了工具 Compose Compiler Metrics,可以帮助我们查看代码中类型的稳定性信息

参考:chris.banes.dev/posts/compo…

使用方式很简单,只要在 root 的 build.gradle 中添加一下配置

subprojects {
    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
        kotlinOptions {
            freeCompilerArgs += [
                    "-P",
                    "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination="
                            + project.buildDir.absolutePath + "/compose_metrics"
            ]
            freeCompilerArgs += [
                    "-P",
                    "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination="
                            + project.buildDir.absolutePath + "/compose_metrics"
            ]
        }
    }
}

然后执行 .gradlew assemble 命令时,就可以在 build/compose_metrics 目录中生成静态分析的信息

image.png

例如下面代码:

class Foo(val value: Int)
class Foo2(var value: Int)
class Foo3<T>(val value: T)
@Stable
class Foo4(var value: Int)

检测的结果是:

stable class Foo {
  stable val value: Int
  <runtime stability> = Stable
}
unstable class Foo2 {
  stable var value: Int
  <runtime stability> = Unstable
}
runtime class Foo3 {
  runtime val value: T
  <runtime stability> = Parameter(T)
}
stable class Foo4 {
  stable var value: Int
}

另外,谷歌工程师测试驱动意识很强,Compose Compiler 源码中提供了 ClassStabilityTransform 配套的单元测试,可以帮我们对类型稳定性的理解更清楚:

cs.android.com/androidx/pl…

目录
相关文章
|
7月前
|
编译器 PHP
深入浅出 Compose Compiler(4) 智能重组与 $changed 参数
深入浅出 Compose Compiler(4) 智能重组与 $changed 参数
108 5
深入浅出 Compose Compiler(4) 智能重组与 $changed 参数
|
7月前
|
编译器 PHP Kotlin
深入浅出 Compose Compiler(3) IR & IrGenerationExtension
深入浅出 Compose Compiler(3) IR & IrGenerationExtension
146 0
|
5月前
|
运维 监控 测试技术
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
|
6月前
|
机器学习/深度学习 前端开发 调度
技术好文:TVM适配NN编译Compiler缺陷
技术好文:TVM适配NN编译Compiler缺陷
|
7月前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
220 1
|
7月前
|
缓存 编译器 Go
Build实战指南:优雅编译,高效开发
Build实战指南:优雅编译,高效开发
135 0
|
7月前
|
前端开发 编译器 PHP
深入浅出 Compose Compiler(2) 编译器前端检查
深入浅出 Compose Compiler(2) 编译器前端检查
89 0
|
安全 编译器 Android开发
[译] Compose之稳定性(Stability)的解释(一)
[译] Compose之稳定性(Stability)的解释
248 0
[译] Compose之稳定性(Stability)的解释(一)
|
编译器 API Kotlin
[译] Compose之稳定性(Stability)的解释(二)
[译] Compose之稳定性(Stability)的解释
134 0
|
Rust 开发工具 git
性能的极致,Rust的加持,Zed-Dev编辑器快速搭建Python3.10开发环境
快就一个字,甚至比以快著称于世的Sublime 4编辑器都快,这就是Zed.dev编辑器。其底层由 Rust 编写,比基于Electron技术微软开源的编辑器VSCode快一倍有余,性能上无出其右,同时支持多人编辑代码。
性能的极致,Rust的加持,Zed-Dev编辑器快速搭建Python3.10开发环境