Android Jetpack Compose
Android Jetpack Compose 是 Google 推出的现代 UI 工具包,用于以声明式的方式构建 Android 应用的 UI。它摒弃了传统的 XML 布局方式,完全基于 Kotlin 编写,提供了更简洁、更强大的 UI 开发体验。以下是 Compose 的使用方式、原理和核心概念的详细解析。
1. Compose 的核心概念
1.1 声明式 UI
- Compose 采用声明式编程范式,开发者只需描述 UI 应该是什么样子,而不需要关心其具体实现。
- 与传统的命令式 UI(如 XML + View 系统)不同,Compose 的 UI 是动态的,可以根据状态自动更新。
1.2 Composable 函数
- Compose 的 UI 由
@Composable函数定义。这些函数是纯函数,接收输入参数并返回 UI 组件。 - 例如:
@Composable fun Greeting(name: String) { Text(text = "Hello, $name!") }
1.3 状态管理
- Compose 使用
State来管理 UI 的状态。当状态发生变化时,Compose 会自动重新绘制相关的 UI。 - 例如:
@Composable fun Counter() { val count = remember { mutableStateOf(0) } Button(onClick = { count.value++ }) { Text("Clicked ${count.value} times") } }
1.4 重组(Recomposition)
- 当状态发生变化时,Compose 会触发重组(Recomposition),重新调用相关的
@Composable函数来更新 UI。 - Compose 会自动优化重组过程,只更新需要变化的部分。
2. Compose 的基本使用
2.1 设置项目
在 build.gradle 中添加 Compose 的依赖:
dependencies {
implementation "androidx.compose.ui:ui:1.3.3"
implementation "androidx.compose.material:material:1.3.3"
implementation "androidx.compose.runtime:runtime:1.3.3"
implementation "androidx.activity:activity-compose:1.6.1"
}
2.2 创建 Composable 函数
定义一个简单的 UI 组件:
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
2.3 在 Activity 中使用 Compose
在 Activity 中设置 Compose 的内容:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting(name = "Compose")
}
}
}
3. Compose 的高级特性
3.1 布局
Compose 提供了多种布局组件,如 Column、Row、Box 等:
@Composable
fun ProfileCard() {
Column {
Text("John Doe")
Text("Software Engineer")
}
}
3.2 主题
可以通过 MaterialTheme 定义应用的主题:
@Composable
fun App() {
MaterialTheme {
Greeting(name = "Compose")
}
}
3.3 动画
Compose 提供了强大的动画支持:
@Composable
fun AnimatedButton() {
val enabled = remember { mutableStateOf(true) }
Button(onClick = { enabled.value = !enabled.value }) {
Text(if (enabled.value) "Enabled" else "Disabled")
}
}
3.4 状态提升
将状态提升到父组件中,以实现更灵活的状态管理:
@Composable
fun Parent() {
val count = remember { mutableStateOf(0) }
Counter(count = count.value, onIncrement = { count.value++ })
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Clicked $count times")
}
}
4. Compose 的工作原理
4.1 Compose 的架构
- Compose 基于 Kotlin 的编译器插件,将
@Composable函数转换为高效的 UI 渲染代码。 - Compose 使用 Slot Table 和 Gap Buffer 技术来管理 UI 的状态和更新。
4.2 重组机制
- 当状态发生变化时,Compose 会标记受影响的
@Composable函数,并重新调用它们。 - Compose 会通过比较前后两次调用的参数,决定是否需要重组。
4.3 状态管理
- Compose 使用
remember和mutableStateOf来保存和更新状态。 - 状态的变化会触发重组,从而更新 UI。
4.4 布局和绘制
- Compose 的布局系统基于 ConstraintLayout 和 Measure 机制,支持灵活的 UI 布局。
- 绘制过程使用 GPU 加速,性能高效。
前置学习
Kotlin remember
remember用于在组件的重新组合(Recomposition)过程中保留状态或计算结果,避免不必要的重复计算或初始化。
remember 的作用
remember 的作用是缓存一个值,并在组件的多次重新组合中保持该值不变,除非它的依赖项发生了变化。它通常用于管理组件的内部状态或缓存昂贵的计算结果。
remember 的语法
remember 的常见用法有两种:
不带依赖项的
remember:val value = remember { initialValue }- 这里的
initialValue是一个 lambda 表达式,它会在第一次组合时执行,并将结果缓存。在后续的重新组合中,remember会直接返回缓存的值,而不会重新执行 lambda。
- 这里的
带依赖项的
remember:val value = remember(key1, key2, ...) { initialValue }- 这里的
key1、key2等是依赖项。当这些依赖项发生变化时,remember会重新执行 lambda 并更新缓存的值;否则,直接返回缓存的值。
- 这里的
remember 的示例
缓存状态:
@Composable fun Counter() { val count = remember { mutableStateOf(0) } // 缓存一个状态 Button(onClick = { count.value++ }) { Text("Clicked ${count.value} times") } }- 这里使用
remember缓存了一个MutableState,确保在重新组合时不会重新初始化count。
- 这里使用
缓存计算结果:
@Composable fun ExpensiveCalculation(input: Int) { val result = remember(input) { // 缓存计算结果,依赖 input performExpensiveCalculation(input) } Text("Result: $result") }- 这里使用
remember缓存了一个昂贵的计算结果,只有当input发生变化时才会重新计算。
- 这里使用
缓存对象:
@Composable fun MyComponent() { val myObject = remember { MyObject() } // 缓存一个对象 Text("Object: $myObject") }- 这里使用
remember缓存了一个对象,避免在每次重新组合时重新创建。
- 这里使用
remember 的注意事项
remember只能在@Composable函数中使用,因为它依赖于 Compose 的重组机制。remember缓存的值只在当前组件的生命周期内有效。如果组件被移除或重建,缓存的值会丢失。remember不能用于跨组件的状态共享,如果需要跨组件共享状态,应该使用rememberSaveable或ViewModel。
remember 与 rememberSaveable
rememberSaveable 是 remember 的增强版,它可以在配置更改(如屏幕旋转)或进程重建时保存状态。例如:
@Composable
fun MyComponent() {
val state = rememberSaveable { mutableStateOf(0) }
Button(onClick = { state.value++ }) {
Text("Clicked ${state.value} times")
}
}
Compose添加动画
1. 状态驱动的动画
Compose 的动画是基于状态驱动的。通过监听状态的变化,Compose 会自动在状态之间进行平滑的过渡。
示例:简单的淡入淡出动画
@Composable
fun FadeInOutAnimation() {
var visible by remember { mutableStateOf(true) } // 状态控制显示/隐藏
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f, // 目标透明度
animationSpec = tween(durationMillis = 1000) // 动画配置
)
Column {
Button(onClick = { visible = !visible }) {
Text("Toggle Visibility")
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Blue)
.alpha(alpha) // 应用透明动画
)
}
}
animateFloatAsState:用于在状态变化时平滑过渡一个Float值。tween:定义动画的持续时间和缓动效果。
2. 过渡动画
Transition 用于在多个状态之间进行复杂的动画过渡。
示例:切换大小和颜色的动画
@Composable
fun TransitionAnimation() {
var toggled by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = toggled, label = "Toggle Transition")
val size by transition.animateDp(
transitionSpec = { tween(durationMillis = 500) },
label = "Size Animation",
targetValueByState = { isToggled -> if (isToggled) 200.dp else 100.dp }
)
val color by transition.animateColor(
transitionSpec = { tween(durationMillis = 500) },
label = "Color Animation",
targetValueByState = { isToggled -> if (isToggled) Color.Red else Color.Blue }
)
Column {
Button(onClick = { toggled = !toggled }) {
Text("Toggle Animation")
}
Box(
modifier = Modifier
.size(size)
.background(color)
)
}
}
updateTransition:创建一个过渡动画对象。animateDp和animateColor:分别在状态之间过渡Dp和Color值。
3. 无限循环动画
使用 InfiniteTransition 可以创建无限循环的动画。
示例:无限旋转的圆圈
@Composable
fun InfiniteRotationAnimation() {
val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Box(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.graphicsLayer {
rotationZ = rotation // 应用旋转动画
}
)
}
}
infiniteRepeatable:定义一个无限循环的动画。
4. 手势驱动的动画
Compose 支持将动画与手势结合,实现更自然的交互效果。
示例:拖动动画
@Composable
fun DraggableBox() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
) {
Box(
modifier = Modifier
.offset { IntOffset(offsetX.toInt(), offsetY.toInt()) }
.size(100.dp)
.background(Color.Red)
)
}
}
pointerInput和detectDragGestures:用于监听拖动事件。offset:根据拖动距离更新组件的位置。
5. 自定义动画
如果需要更复杂的动画,可以使用 Animatable 或 AnimationState 进行自定义控制。
示例:使用 Animatable 实现弹簧动画
@Composable
fun SpringAnimation() {
val animatable = remember { Animatable(0f, Dp.VectorConverter) }
LaunchedEffect(Unit) {
animatable.animateTo(
targetValue = 200.dp.value,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
}
Box(
modifier = Modifier
.offset { IntOffset(0, animatable.value.toInt()) }
.size(100.dp)
.background(Color.Green)
)
}
Animatable:用于创建自定义动画。spring:定义弹簧动画的特性。
6. 动画的可见性
Compose 提供了 AnimatedVisibility,可以方便地实现组件的显示和隐藏动画。
示例:滑动显示/隐藏动画
@Composable
fun AnimatedVisibilityExample() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text("Toggle Visibility")
}
AnimatedVisibility(
visible = visible,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Blue)
)
}
}
}
AnimatedVisibility:根据状态控制组件的显示和隐藏。slideInVertically和fadeIn:定义进入动画。
Slot Table
Slot Table 是 Jetpack Compose 中用于管理组件状态和结构的关键机制。它是 Compose 运行时(Compose Runtime)的核心部分,负责存储和更新组件的状态、布局信息以及其他元数据。理解 Slot Table 的工作原理有助于更好地掌握 Compose 的内部机制。
1. Slot Table 的作用
Slot Table 是 Compose 用于存储组件树(Composable Tree)的底层数据结构。它的主要功能包括:
- 存储组件状态:保存 Composable 函数的状态(如
mutableStateOf)。 - 管理组件结构:记录组件的层次结构和布局信息。
- 支持重组(Recomposition):在状态变化时,Compose 通过 Slot Table 高效地更新 UI。
2. Slot Table 的结构
Slot Table 可以理解为一个二维表格,其中包含以下部分:
- Slots:表格中的单元格,用于存储数据(如状态、布局信息等)。
- Groups:表格中的行,表示一个 Composable 函数或组件。每个 Group 包含多个 Slots。
示例:简单的 Slot Table
假设有以下 Composable 函数:
@Composable
fun MyComponent() {
val count = remember { mutableStateOf(0) }
Text("Count: ${count.value}")
}
在 Slot Table 中,它可能被表示为:
| Group | Slot 1 | Slot 2 |
|---|---|---|
| MyComponent | count (state) | Text (content) |
| Text | “Count: 0” | - |
- MyComponent 是一个 Group,它包含两个 Slots:
count(状态)和Text(子组件)。 - Text 是另一个 Group,它包含一个 Slot:
"Count: 0"(文本内容)。
3. Slot Table 的工作原理
初始组合(Initial Composition)
当 Composable 函数首次执行时,Compose 会:
- 创建 Slot Table,并记录组件树的结构和状态。
- 将状态和布局信息存储在 Slots 中。
重组(Recomposition)
当状态发生变化时,Compose 会:
- 根据 Slot Table 中的信息,判断哪些组件需要更新。
- 重新执行受影响的 Composable 函数,并更新 Slot Table 中的 Slots。
- 仅更新 UI 中发生变化的部分,避免不必要的重新绘制。
示例:重组过程
假设 count 从 0 变为 1,Compose 会:
- 找到
MyComponent的 Group,并更新count的 Slot。 - 重新执行
TextComposable,并更新其 Slots。 - 最终,UI 中的文本从
"Count: 0"更新为"Count: 1"。
4. Slot Table 的优势
- 高效的重组:通过 Slot Table,Compose 可以精确地知道哪些部分需要更新,避免不必要的重新绘制。
- 状态管理:Slot Table 集中管理所有组件的状态,确保状态的一致性和可预测性。
- 结构清晰:Slot Table 以树形结构记录组件层次,便于调试和分析。
5. 与 Slot Table 相关的概念
Positional Memoization
Compose 使用位置记忆(Positional Memoization)来跟踪 Composable 函数的执行。每个 Composable 在 Slot Table 中都有一个固定的位置,Compose 通过位置来识别和更新组件。
Gap Buffer
Slot Table 使用间隙缓冲区(Gap Buffer)来高效地插入和删除数据。这种数据结构使得 Composable 的增删操作更加高效。
State
状态(State)是 Slot Table 中存储的重要数据。Compose 通过 mutableStateOf 等 API 将状态存储在 Slot Table 中,并在状态变化时触发重组。
源码分析
1. Compose 的架构
Compose 的源码分为 编译器插件 和 运行时库 两部分:
1.1 Compose 编译器插件
Compose 编译器插件是 Compose 的核心,负责将 @Composable 函数转换为高效的 UI 渲染代码。它的主要功能包括:
- 代码生成:将
@Composable函数转换为可执行的 UI 渲染逻辑。 - 状态管理:自动插入状态管理和重组逻辑。
- 优化:通过静态分析和优化减少不必要的重组。
1.2 Compose 运行时库
Compose 运行时库提供了 Compose 的核心功能,包括:
- Slot Table:用于存储 UI 组件的状态和层次结构。
- Recomposition:管理 UI 的重组逻辑。
- Layout 和 Drawing:负责 UI 的布局和绘制。
2. Compose 的核心机制
2.1 Slot Table
- 作用:Slot Table 是 Compose 的核心数据结构,用于存储 UI 组件的状态和层次结构。
- 实现:它是一个线性表,存储了所有组件的状态和属性。
- 优化:通过 Gap Buffer 技术高效管理插入、删除和更新操作。
2.2 Recomposition(重组)
- 触发条件:当状态(如
State或MutableState)发生变化时,Compose 会触发重组。 - 过程:
- 标记受影响的
@Composable函数。 - 重新调用这些函数,生成新的 UI 组件树。
- 比较新旧组件树,只更新变化的部分。
- 标记受影响的
- 优化:Compose 通过静态分析和缓存机制减少不必要的重组。
2.3 状态管理
remember:用于在重组之间保存状态。mutableStateOf:用于创建可变状态,当状态变化时触发重组。- 状态提升:将状态提升到父组件中,以实现更灵活的状态管理。
3. Compose 的关键源码解析
3.1 @Composable 函数
@Composable 函数是 Compose 的基本构建块。编译器插件会将 @Composable 函数转换为以下结构:
@Composable
fun Example() {
// 原始代码
Text("Hello, Compose!")
}
// 编译后
fun Example(composer: Composer, key: Int) {
composer.start(key)
Text(composer, "Hello, Compose!")
composer.end()
}
composer:用于管理 UI 组件的状态和层次结构。key:用于标识组件的唯一性。
1. Composer 的作用
Composer 的主要职责包括:
- 管理 Composable 函数的执行:通过调用 Composable 函数,生成 UI 组件树。
- 状态管理:存储和更新 Composable 函数中的状态(如
mutableStateOf)。 - 支持重组(Recomposition):在状态变化时,重新执行受影响的 Composable 函数,并更新 UI。
- 协调布局和绘制:将生成的组件树传递给布局和绘制系统。
2. Composer 的核心方法
Composer 接口定义了一系列方法,用于管理 Composable 函数的执行和状态存储。以下是关键方法及其作用:
start 和 end
fun start(key: Int, group: Int)
fun end()
start:标记一个 Composable 函数的开始,并分配一个唯一的key和group。end:标记一个 Composable 函数的结束。- 这两个方法用于构建组件树的层次结构。
createNode
fun createNode(factory: () -> T)
- 创建一个 UI 节点(如
LayoutNode或TextNode),并将其插入组件树。
setValue
fun setValue(value: Any?)
- 将状态值存储到 Slot Table 中。
changed
fun changed(value: Any?): Boolean
- 检查状态值是否发生变化,以决定是否需要触发重组。
remember
fun <T> remember(value: T): T
- 将值存储在 Slot Table 中,并在后续执行中返回相同的值(除非依赖项发生变化)。
updateScope
fun updateScope(scope: () -> Unit)
- 注册一个更新作用域,用于在重组时重新执行 Composable 函数。
3. Composer 的实现
Composer 的核心实现是 ComposerImpl 类,它负责具体的逻辑。以下是 ComposerImpl 的关键机制:
Slot Table
ComposerImpl使用 Slot Table 来存储 Composable 函数的状态和元数据。- Slot Table 是一个二维表格,包含
Slots和Groups,分别用于存储数据和描述组件结构。
Gap Buffer
- Slot Table 使用 Gap Buffer 来高效地插入和删除数据,支持动态组件树的构建。
Positional Memoization
ComposerImpl使用 位置记忆 来跟踪 Composable 函数的执行。每个 Composable 函数在 Slot Table 中都有一个固定的位置,Composer 通过位置来识别和更新组件。
重组机制
- 当状态发生变化时,
ComposerImpl会重新执行受影响的 Composable 函数,并更新 Slot Table 和 UI。
4. Composable 函数的执行流程
以下是 Composable 函数在 Composer 中的执行流程:
- 开始 Composable 函数:
- 调用
start方法,分配key和group,标记 Composable 函数的开始。
- 调用
- 创建 UI 节点:
- 调用
createNode方法,生成 UI 节点并插入组件树。
- 调用
- 存储状态:
- 调用
setValue方法,将状态值存储到 Slot Table 中。
- 调用
- 检查状态变化:
- 调用
changed方法,判断是否需要触发重组。
- 调用
- 结束 Composable 函数:
- 调用
end方法,标记 Composable 函数的结束。
- 调用
- 触发重组:
- 如果状态发生变化,重新执行受影响的 Composable 函数,并更新 UI。
5. 源码分析示例
以下是一个简单的 Composable 函数及其在 Composer 中的执行过程:
Composable 函数
@Composable
fun MyComponent() {
val count = remember { mutableStateOf(0) }
Text("Count: ${count.value}")
}
执行流程
- 调用
start,标记MyComponent的开始。 - 调用
remember,将count存储在 Slot Table 中。 - 调用
start,标记Text的开始。 - 调用
createNode,创建TextNode并插入组件树。 - 调用
end,标记Text的结束。 - 调用
end,标记MyComponent的结束。
3.2 Slot Table
Slot Table 是 Compose 的核心数据结构,源码位于 androidx.compose.runtime 包中。它的主要实现包括:
SlotTable.kt:定义 Slot Table 的结构和操作。GapBuffer.kt:实现 Gap Buffer 技术,用于高效管理插入和删除操作。
SlotTable 是 Jetpack Compose 运行时的核心数据结构,用于存储 Composable 函数的状态、组件结构和其他元数据。它是 Compose 实现高效重组(Recomposition)和状态管理的关键组件。通过分析 SlotTable 的源码,可以深入理解 Compose 的内部工作机制。
1. SlotTable 的作用
SlotTable 的主要职责包括:
- 存储 Composable 函数的状态:如
mutableStateOf的值。 - 记录组件树的结构:描述 Composable 函数的层次关系。
- 支持高效的重组:在状态变化时,快速定位和更新受影响的组件。
- 管理动态数据:支持插入、删除和更新操作。
2. SlotTable 的结构
SlotTable 是一个二维表格,包含以下部分:
- Slots:表格中的单元格,用于存储数据(如状态、布局信息等)。
- Groups:表格中的行,表示一个 Composable 函数或组件。每个 Group 包含多个 Slots。
数据结构
SlotTable 的核心字段包括:
class SlotTable {
private val slots: IntArray // 存储 Slot 数据
private val groups: IntArray // 存储 Group 元数据
private var groupsSize: Int // Group 的数量
private var slotsSize: Int // Slot 的数量
}
- Slots:
slots是一个IntArray,用于存储具体的数据(如状态值、文本内容等)。 - Groups:
groups是一个IntArray,用于存储 Group 的元数据(如类型、父 Group、子 Group 等)。
3. Group 的表示
每个 Group 在 groups 数组中占用了多个连续的槽位,用于存储以下信息:
- Group 类型:表示 Group 的种类(如 Composable 函数、布局节点等)。
- 父 Group:指向父 Group 的索引。
- 子 Group:指向子 Group 的索引。
- 数据范围:指向
slots数组中与该 Group 相关的数据。
Group 的布局
在 groups 数组中,一个 Group 的布局如下:
| 索引 | 字段 | 描述 |
|---|---|---|
| 0 | Key | Group 的唯一标识符 |
| 1 | Parent | 父 Group 的索引 |
| 2 | FirstChild | 第一个子 Group 的索引 |
| 3 | NextSibling | 下一个兄弟 Group 的索引 |
| 4 | DataStart | 数据在 slots 中的起始索引 |
| 5 | DataEnd | 数据在 slots 中的结束索引 |
4. SlotTable 的核心操作
SlotTable 提供了一系列方法,用于管理数据和组件结构。
插入 Group
fun insertGroup(key: Int, parent: Int, dataStart: Int, dataEnd: Int): Int
- 插入一个新的 Group,并返回其索引。
- 更新
groups和slots数组,确保数据的一致性。
删除 Group
fun removeGroup(group: Int)
- 删除指定的 Group。
- 更新
groups和slots数组,回收资源。
更新 Slot 数据
fun setSlot(index: Int, value: Any?)
- 更新
slots数组中指定索引的值。 - 如果值发生变化,标记 Group 为需要重组。
查找 Group
fun findGroup(key: Int, parent: Int): Int
- 根据
key和parent查找 Group。 - 如果找到,返回 Group 的索引;否则返回
-1。
5. SlotTable 的重组机制
SlotTable 通过以下机制支持高效的重组:
- 位置记忆(Positional Memoization):
- 每个 Composable 函数在 Slot Table 中都有一个固定的位置。
- 在重组时,Compose 根据位置快速定位和更新组件。
- Gap Buffer:
SlotTable使用 Gap Buffer 来高效地插入和删除数据。- Gap Buffer 将数组分为两部分,中间留出一个“间隙”,使得插入和删除操作的时间复杂度为 O(1)。
- 状态变化检测:
- 当状态发生变化时,
SlotTable会标记受影响的 Group 为“脏数据”,并在重组时重新执行相应的 Composable 函数。
- 当状态发生变化时,
6. 源码分析示例
以下是一个简单的 Composable 函数及其在 SlotTable 中的表示:
Composable 函数
@Composable
fun MyComponent() {
val count = remember { mutableStateOf(0) }
Text("Count: ${count.value}")
}
SlotTable 表示
| Group | Slots |
|---|---|
| MyComponent | count (state) |
| Text | “Count: 0” |
在 groups 和 slots 数组中的具体表示:
groups数组:MyComponent:[key=1, parent=-1, firstChild=2, nextSibling=-1, dataStart=0, dataEnd=1]Text:[key=2, parent=1, firstChild=-1, nextSibling=-1, dataStart=1, dataEnd=2]
slots数组:[count, "Count: 0"]
3.3 Recomposition
Compose 的重组逻辑位于 androidx.compose.runtime 包中,主要实现包括:
Recomposer.kt:管理重组过程,标记和调度受影响的组件。Composer.kt:负责组件的创建、更新和销毁。
1. Recomposer 的作用
Recomposer 的主要职责包括:
- 调度 Composable 函数的执行:协调 Composable 函数的初始执行和重组。
- 管理状态变化:监听状态的变化,并触发受影响的 Composable 函数进行重组。
- 协调帧更新:确保 UI 更新与 Android 的帧率(如 60Hz)同步。
- 支持并发重组:在多个线程中高效地执行 Composable 函数。
2. Recomposer 的核心机制
Recomposer 的实现依赖于以下核心机制:
状态快照系统(State Snapshot System)
- Compose 使用状态快照来跟踪状态的变化。
- 当状态发生变化时,
Recomposer会查找所有依赖该状态的 Composable 函数,并触发它们重组。
Slot Table
Recomposer使用 Slot Table 来存储 Composable 函数的状态和组件结构。- Slot Table 是 Compose 实现高效重组的基础。
帧调度
Recomposer与 Android 的 Choreographer 集成,确保 UI 更新与屏幕刷新率同步。- 它会在每一帧开始前执行待处理的重组任务。
并发重组
Recomposer支持在多个线程中执行 Composable 函数,以提高性能。- 它使用线程池来管理并发任务。
3. Recomposer 的源码分析
以下是 Recomposer 的核心源码片段及其功能解析:
初始化
Recomposer 在初始化时会创建一个线程池,并注册到 Android 的 Choreographer。
class Recomposer {
private val choreographer = Choreographer.getInstance()
private val executor = Executors.newSingleThreadExecutor()
init {
choreographer.postFrameCallback(frameCallback)
}
}
帧回调
Recomposer 使用 Choreographer.FrameCallback 来协调帧更新。
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
// 执行待处理的重组任务
executePendingRecompositions()
// 注册下一帧的回调
choreographer.postFrameCallback(this)
}
}
状态监听
Recomposer 监听状态的变化,并标记受影响的 Composable 函数为“脏数据”。
fun onStateChanged(state: State<*>) {
// 查找依赖该状态的 Composable 函数
val affected = findAffectedComposables(state)
// 标记为需要重组
markDirty(affected)
}
重组调度
Recomposer 使用线程池执行重组任务。
private fun executePendingRecompositions() {
executor.execute {
// 获取待处理的重组任务
val tasks = getPendingRecompositionTasks()
// 执行任务
tasks.forEach { it.run() }
}
}
并发重组
Recomposer 支持在多个线程中执行 Composable 函数。
fun recompose(composable: @Composable () -> Unit) {
executor.execute {
// 在后台线程中执行 Composable 函数
composable()
}
}
4. Recomposer 的工作流程
- 初始化:
- 创建线程池,并注册到 Choreographer。
- 监听状态变化:
- 当状态发生变化时,标记受影响的 Composable 函数为“脏数据”。
- 调度重组:
- 在下一帧开始前,执行待处理的重组任务。
- 执行 Composable 函数:
- 在后台线程中执行 Composable 函数,并更新 Slot Table。
- 帧更新:
- 将更新后的 UI 提交给渲染系统。
3.4 Layout 和 Drawing
Compose 的布局和绘制逻辑位于 androidx.compose.ui 包中,主要实现包括:
Layout.kt:定义布局组件,如Column、Row等。Draw.kt:实现绘制逻辑,支持自定义绘制。
1. Layout 的作用
Layout 的主要功能包括:
- 自定义布局:允许开发者完全控制子组件的测量(Measure)和布局(Placement)过程。
- 支持复杂的布局逻辑:例如自定义排列、拖拽布局、流式布局等。
- 与 Compose 布局系统集成:与
MeasurePolicy和LayoutModifier等组件无缝协作。
2. Layout 的基本用法
Layout 的典型用法如下:
@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 测量子组件
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算布局尺寸
val layoutWidth = placeables.maxOf { it.width }
val layoutHeight = placeables.maxOf { it.height }
// 布局子组件
layout(layoutWidth, layoutHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = 0) // 自定义位置
}
}
}
}
3. Layout 的源码分析
Layout 是 Compose 中的一个 Composable 函数,其源码位于 androidx.compose.ui.Layout.kt 中。以下是其核心实现:
Layout 函数签名
@Composable
fun Layout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
measurePolicy: MeasurePolicy
)
modifier:用于调整布局的行为和外观。content:子组件的 Composable 内容。measurePolicy:定义测量和布局的逻辑。
MeasurePolicy
MeasurePolicy 是一个接口,定义了如何测量和布局子组件。它的核心方法包括:
interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int
fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int
fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int
fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int
}
最重要的方法是 measure,它负责测量子组件并返回布局结果。
Layout 的实现
Layout 的实现如下:
@Composable
fun Layout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(modifier, ComposeUiNode.SetModifier)
},
content = content
)
}
ReusableComposeNode:创建一个可重用的布局节点。measurePolicy:传递测量和布局的逻辑。modifier:传递布局的修饰符。
4. Layout 的工作流程
Layout 的工作流程如下:
- 测量子组件:
- 调用
measurePolicy.measure方法,为每个子组件分配空间。
- 调用
- 计算布局尺寸:
- 根据子组件的测量结果,确定布局的总尺寸。
- 布局子组件:
- 调用
layout方法,将子组件放置在指定的位置。
- 调用
5. 自定义布局的示例
以下是一个自定义布局的示例,将子组件垂直排列:
@Composable
fun VerticalLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 测量子组件
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算布局尺寸
val layoutWidth = placeables.maxOf { it.width }
val layoutHeight = placeables.sumOf { it.height }
// 布局子组件
layout(layoutWidth, layoutHeight) {
var y = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = y)
y += placeable.height
}
}
}
}
1. Draw 模块的核心类
1.1 Canvas
- 作用:
Canvas是 Compose 中的绘制画布,封装了 Android 的Canvas类。 - 源码位置:
androidx.compose.ui.graphics.Canvas - 关键方法:
drawRect:绘制矩形。drawCircle:绘制圆形。drawPath:绘制路径。drawImage:绘制图片。
1.2 Paint
- 作用:
Paint是 Compose 中的绘制工具,封装了 Android 的Paint类。 - 源码位置:
androidx.compose.ui.graphics.Paint - 关键属性:
color:设置绘制颜色。style:设置绘制样式(填充或描边)。strokeWidth:设置描边宽度。
1.3 DrawScope
- 作用:
DrawScope是 Compose 中用于定义绘制作用域的接口,提供了绘制的上下文信息(如尺寸、密度等)。 - 源码位置:
androidx.compose.ui.graphics.drawscope.DrawScope - 关键属性:
size:当前绘制区域的大小。density:当前设备的屏幕密度。
- 关键方法:
drawRect、drawCircle、drawPath等,与Canvas的方法类似。
1.4 DrawModifier
- 作用:
DrawModifier是一个Modifier,用于在 UI 组件上添加自定义绘制逻辑。 - 源码位置:
androidx.compose.ui.draw.DrawModifier - 关键实现:
DrawModifier通过draw方法将自定义绘制逻辑应用到组件上。
2. Draw 模块的工作原理
2.1 绘制流程
- 创建
Canvas:Compose 在渲染 UI 时,会为每个组件创建一个Canvas。 - 调用
DrawScope:通过DrawScope提供绘制上下文(如尺寸、密度等)。 - 执行绘制逻辑:在
DrawScope中调用drawRect、drawCircle等方法完成绘制。 - 应用
DrawModifier:如果组件使用了DrawModifier,会调用其draw方法应用自定义绘制逻辑。
2.2 重组与绘制
- 当组件的状态发生变化时,Compose 会触发重组。
- 在重组过程中,
DrawScope会重新调用绘制逻辑,更新 UI。
3. Draw 模块的关键源码解析
3.1 Canvas 的实现
Canvas 是对 Android Canvas 的封装,源码位于 androidx.compose.ui.graphics.Canvas。以下是关键方法:
fun drawRect(rect: Rect, paint: Paint) {
nativeCanvas.drawRect(rect.toAndroidRect(), paint.asFrameworkPaint())
}
nativeCanvas:底层的 AndroidCanvas。paint.asFrameworkPaint():将 Compose 的Paint转换为 Android 的Paint。
3.2 DrawScope 的实现
DrawScope 是绘制作用域的接口,源码位于 androidx.compose.ui.graphics.drawscope.DrawScope。以下是关键方法:
fun drawCircle(
color: Color,
radius: Float,
center: Offset,
style: DrawStyle = Fill,
alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
) {
// 调用 Canvas 的绘制方法
canvas.drawCircle(center, radius, paint)
}
canvas:当前的Canvas。paint:当前的Paint。
3.3 DrawModifier 的实现
DrawModifier 是用于添加自定义绘制逻辑的 Modifier,源码位于 androidx.compose.ui.draw.DrawModifier。以下是关键实现:
class DrawModifier(
private val onDraw: DrawScope.() -> Unit
) : Modifier.Element, DrawModifier {
override fun ContentDrawScope.draw() {
onDraw()
}
}
onDraw:自定义绘制逻辑。ContentDrawScope:继承自DrawScope,提供了绘制上下文。
4. Draw 模块的使用示例
4.1 自定义绘制
以下是一个简单的自定义绘制示例:
@Composable
fun CustomDrawComponent() {
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(color = Color.Red, topLeft = Offset(100f, 100f), size = Size(200f, 200f))
drawCircle(color = Color.Blue, radius = 100f, center = Offset(300f, 300f))
}
}
4.2 使用 DrawModifier
以下是一个使用 DrawModifier 的示例:
@Composable
fun CustomDrawModifier() {
Box(
modifier = Modifier
.size(200.dp)
.drawWithContent {
drawRect(color = Color.Red, size = size)
drawContent()
}
) {
Text("Hello, Compose!")
}
}
5. Draw 模块的优化
5.1 硬件加速
Compose 的绘制过程默认使用硬件加速,性能高效。
5.2 最小化绘制区域
Compose 会自动优化绘制区域,只绘制需要更新的部分。
5.3 缓存绘制结果
对于复杂的绘制逻辑,可以使用 RenderNode 或 Bitmap 缓存绘制结果,减少重复计算。
4. Compose 的优化技术
4.1 静态分析
Compose 编译器插件通过静态分析优化代码,减少不必要的重组。例如:
stable和unstable参数:标记参数的稳定性,避免不必要的重组。remember和key:通过缓存和唯一标识优化重组。
4.2 增量更新
Compose 通过比较新旧组件树,只更新变化的部分。例如:
DiffUtil:比较组件树的变化,生成最小更新集。LazyColumn和LazyRow:通过懒加载优化列表性能。
4.3 GPU 加速
Compose 的绘制过程使用 GPU 加速,提升渲染性能。例如:
Canvas:支持自定义绘制,性能高效。Modifier:通过链式调用优化 UI 组件的属性设置。
写在后面
总的看来,很像ArkTs的更新方式,有一种莫名的熟悉感哈哈~
If you like this article, it is written by Johnny Deng.
If not, I don’t know who wrote it.