解构 Compose 编译器:@Composable 如何改变 Android 开发
@Composable:不止是一个注解
很多初学者认为 `@Composable` 类似于 Dagger 的 `@Inject`,是一个运行时注解。事实上,它更像 Kotlin 的 `suspend` 关键字,是一个改变函数类型的语言级特性。
`@Composable` 对应着一个 Kotlin 编译器插件(Compose Compiler Plugin)。在编译期间,这个插件会拦截所有被标记的函数,并重写其签名。最显著的变化是注入了一个隐式的参数:`Composer`。
`Composer` 是 Compose 运行时的核心上下文对象,它贯穿于整个 UI 树的构建过程,负责记录正在执行的节点位置、存储状态以及调度重组。这解释了为什么 Composable 函数只能在其他 Composable 函数中调用——因为它们需要这个隐式传递的 `Composer` 上下文。
插槽表 (Slot Table) 与间隙缓冲区
Compose 如何存储 UI 树?它并没有使用传统的对象树(如 View Hierarchy),而是使用了一种基于数组的线性数据结构,称为“插槽表”。
插槽表的设计灵感来源于文本编辑器中常用的“间隙缓冲区”(Gap Buffer)。它是一个包含数据的数组,但在当前操作位置保留了一段空的“间隙”。这使得 Compose 可以在 O(1) 时间复杂度内移动光标,并在当前位置高效地插入或删除数据。
这种结构极其适合 UI 的动态特性。当重组发生时,Compose 编译器生成的代码会按照执行顺序访问插槽表。如果发现数据(如状态或节点结构)没有变化,它就直接跳过;如果发生变化,它就利用间隙缓冲区高效地更新数组内容。
位置记忆 (Positional Memoization)
理解了插槽表,就能理解 `remember` 的工作原理。`remember` 并不是魔法,它利用了“位置记忆”技术。
因为 Composable 函数的执行顺序在重组期间通常是稳定的,Compose 可以利用函数在源代码中的“位置”作为 key,在插槽表中查找和存取值。当代码执行到 `remember { ... }` 时,Composer 会检查当前插槽位置是否已有缓存值。如果有且依赖未变,直接返回;否则,执行 lambda 计算新值并存入插槽。
这种机制使得函数式 UI 能够拥有“状态”,并且这种状态能够跨越多次重组而持久存在,直到该 Composable 从 UI 树中被移除。