你敢信吗?R8让 Compose 协程直接快 2 倍!你敢相信吗?R8 让合成程度直接协调快速 2 倍!

AGP 9.2.0 开始,R8 会优化大多数 Atomic*FieldUpdater 调用。这个改动看起来离业务代码很远,却让 Compose LaunchedEffect 的协程启动和取消微基准提升了约 2 倍。

从 AGP 9.2.0 开始,R8 会优化大多数 Atomic*FieldUpdater 的调用。这个改动看起来离业务代码很远,却让 Compose LaunchedEffect 的协议动作启动和取消微基准提升约定增加了 2 倍。

不用改 launch、不用换协程库,升级构建工具后,优化发生在 R8 处理字节码的阶段。

不用改装发射、不用变更协调库、升级结构建工具后、优化发生在 R8 处理字节码的阶段。

协程慢在哪

Kotlin 协程依赖结构化并发维护父子 Job 关系。启动、挂起、取消、完成都要更新这棵无锁树,kotlinx.coroutines 通过 kotlinx.atomicfu 做这些原子读写。

Kotlin 协议依赖结构化和发展维护父子 Job 关系系统。启动、起动、取消、完成都需要更新这棵无锁树,kotlinx.coroutines 通过 kotlinx.atomicfu 这做一些原子阅读。

atomicfu 在 Android 上的一条实现路径会落到 java.util.concurrent.atomic.AtomicReferenceFieldUpdater。它的参数是持有者 Class、字段类型和字段名,运行时需要确认字段存在、类型匹配并且可访问。原子写入本身很快,前面的反射校验却会在高频协程操作里反复出现。

atomicfu 在 Android 上的一条实现路径会到达 java.util.concurrent.atomic.AtomicReferenceFieldUpdater。其参数为持有者类别、字段类型和字段名称、运行时需要确认字段类型存在、类型匹配且可访问。原子写入本身快速,前面的反射校验会在高频协程操作中反复出现。

一个空的 LaunchedEffect,也会经历创建协程、启动协程、正常完成三段调用;取消时还会创建 CancellationException。单次开销不大,但 Compose 的点击反馈、手势、动画等路径会频繁启动和取消内部协程,时间就累积起来了。

一个空的启动效应,也会经历创建协议、启动协议、正常完成三阶段调用;取消时还会创建取消异常。单次开销不大,但组合的点击反馈、手势、频率画等路径会复启和取消内部协议,就间累积起来了。

Compose 团队曾在 Modifier.clickable 的创建和更新分析中看到:约 80% 的时间花在内部协程的启动和取消,以及它们触发的 InteractionSource 更新上。随后 Compose 运行时做了不少调整,让默认路径少碰协程、把初始化推迟到真正需要时。

Compose 团队曾在 Modifier.clickable 的创建和更新分析中看到:约 80% 的时间花在内部协议的启动和取消,以及其他触发的 InteractionSource 更新。随后 Compose 运行时进行了不少调整,让默认路径少量协议,将初始化推迟到真正需要时。

这次 R8 优化处理的是另一部分成本:已经需要启动协程时,原子操作本身不再反复走反射校验。

一个纳秒级差距

原文用 Pixel 5(API 33)比较了 JDK 的 AtomicReferenceatomicfu 包装的原子引用。测试前会让 AtomicReferenceFieldUpdater[#compareAndSet](javascript:;) 经过 JIT 预热,避免把冷启动编译时间算进去。

原文用像素 5(API 33) 比较 JDK 的 AtomicReference 和 atomicfu 包装的原子引用。测试前会让 AtomicReferenceFieldUpdater#compareAndSet 经过 JIT 预热,避免将冷编译启动时间间隔计算进行。

下面是同类测试的最小写法。它测的是 compareAndSet,不是整个协程启动时间:

bash 复制代码
import androidx.benchmark.junit4.BenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.atomicfu.atomic
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicReference

@RunWith(AndroidJUnit4::class)
class AtomicReferenceBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    private val jdkRef = AtomicReference(false)
    private val atomicFuRef = atomic(false)

    @Test
    fun compareAndSetWithAtomicFu() = benchmarkRule.measureRepeated {


        atomicFuRef.compareAndSet(true, false)


        atomicFuRef.compareAndSet(false, true)


    }

}

同一组数据里,AtomicReference.compareAndSet50.7 nsatomicfu 的对应调用是 135 ns,约慢 2.7 倍。ART 的 JIT 并没有把这层 AtomicReferenceFieldUpdater 反射访问完全消掉。

这也解释了一个容易误判的地方:看调用栈时,AtomicReferenceFieldUpdater 每次出现都很短;但协程生命周期会碰到多次原子操作,短调用在频繁交互中同样会变成热点。

R8 怎么改写

AtomicReferenceFieldUpdater 支持动态、反射式用法,R8 不能把所有调用都直接替换。不过有一类写法的字段、持有者类型和字段名都写死在静态初始化中,编译器可以完整追踪。

bash 复制代码
class Example {
    volatile String data = "";

    static final AtomicReferenceFieldUpdater updater =


        AtomicReferenceFieldUpdater.newUpdater(


            Example.class, String.class, "data");

    void update() {


        updater.compareAndSet(this, "", "new");


    }

}

这里的 updaterstatic final,它访问的是 Example.data;字段名、字段类型和 holder 类型都能在编译期确认。R8 会为这个字段准备 offset,然后把满足条件的调用点换成内部 Unsafe 变体。调用时直接使用对象和字段 offset,不需要再做 updater 的反射安全检查。

替换不是粗暴地删掉原实现。R8 会保留无法静态证明的调用点:调用者传入的 holder 必须是原 holder 类型或其子类,写入值也必须满足原字段类型。静态分析不能排除 updater 或 holder 为 null 时,还要插入对应的空值检查,保持原有的异常行为。

最后还有一次清理。如果所有调用点都已改写,原 updater 字段和初始化可以移除;若只有一部分可改写,两套路径会同时保留。newUpdater()getDeclaredField() 都可能抛异常,不能当普通无用代码随便删掉,R8 对已确认安全的插桩字段做了专门处理。

Compose 影响了什么

R8 完成改写后,kotlinx.atomicfu 和大部分显式 AtomicIntAtomicLongAtomicReferenceFieldUpdater 用法,性能可以追上 JDK AtomicReferenceatomicfu 的编译器插件还能把部分 atomic 实例内联成字段,少一次对象分配,因此有些基准甚至会更快。

Compose runtime 有一组持续跟踪协程性能的微基准。切换到新的 R8 后,LaunchedEffect 启动并取消协程的耗时降低约一半。图里的数字应理解为这个特定微基准,并不等于页面点击或动画整体都快 2 倍;真实页面还包含重组、布局、绘制和业务逻辑。

对应用代码来说,受益路径通常是已有的 Compose 和协程代码。比如下面的 LaunchedEffect 不需要改写成特殊 API:

bash 复制代码
@Composable
fun Notice(visible: Boolean, onDismiss: () -> Unit) {


    LaunchedEffect(visible) {
        if (visible) {


            delay(2_000)


            onDismiss()


        }


    }

}

频繁变化的 key 会触发旧协程取消、新协程启动。R8 只能降低协程运行时的原子操作成本,不能解决不必要的 key 变化、重复收集 Flow 或把高频状态直接放进 composition 造成的重组问题。这些仍然要从具体 trace 看。

最后

AGP 9.2.0 的收益来自 R8 改写了 Atomic*FieldUpdater 的常见静态用法。

你们都升级了吗?

#Android #Kotlin #Coroutines #R8 #JetpackCompose

相关推荐
张风捷特烈2 小时前
Flutter UI 解耦 - 天下大势,合久必分, 分久必合
android·前端·flutter
蓝速科技2 小时前
蓝速科技丨甲级写字楼会议预约屏尺寸选型与空间适配指南
android·科技
zhangjin11205 小时前
解决Android Studio gradle下载超时和缓慢问题(二)
android·ide·android studio
xingxiliang13 小时前
深入浅出 Android CTS 视频测试:从环境搭建、用例设计到底层通信原理
android·音视频
我命由我1234513 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
雨白14 小时前
深入理解 Kotlin 协程 (八):拾遗补阙,探秘官方框架的调度细节与取消闭环
android·kotlin
海天鹰16 小时前
PHP上传文件
android·开发语言·php
2501_9159184117 小时前
详解iOS App上架至App Store的全流程步骤与注意事项
android·macos·ios·小程序·uni-app·cocoa·iphone
码农coding18 小时前
android12 SystemUI之StatusBar(二)
android