Android Jetpack Compose介绍
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) { // 缓存计算结果,依赖 inputperformExpensiveCalculation(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.xoffsetY += 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): MeasureResultfun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Intfun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Intfun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Intfun 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.currentval layoutDirection = LocalLayoutDirection.currentval viewConfiguration = LocalViewConfiguration.currentReusableComposeNode<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 = 0placeables.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.
相关文章:
Android Jetpack Compose介绍
Android Jetpack Compose Android Jetpack Compose 是 Google 推出的现代 UI 工具包,用于以声明式的方式构建 Android 应用的 UI。它摒弃了传统的 XML 布局方式,完全基于 Kotlin 编写,提供了更简洁、更强大的 UI 开发体验。以下是 Compose 的…...
tcping 命令的使用,ping IP 和端口
1. Windows系统安装 下载tcping工具:根据系统位数(32位或64位)下载对应的tcping.exe文件。安装步骤: 将下载的tcping.exe文件复制到C:\Windows\System32目录下。如果下载的是64位版本,需将文件名改为tcpi…...
天地图InfoWindow插入React自定义组件
截至2025年03月21日天地图的Marker不支持添加Label; 同时Label和Icon是不支持自定义HTMLElement只支持String;目前只有InfoWindow支持自定义HTMLElement; 效果图 React核心api import ReactDOM from react-dom/client const content document.createElement(div);…...
003-掌控命令行-CLI11-C++开源库108杰
首选的现代C风格命令行参数解析器! (本课程包含两段教学视频。) 以文件对象监控程序为实例,五分钟实现从命令行读入多个监控目标路径;区分两大时机,学习 CLI11 构建与解析参数两大场景下的异常处理;区分三…...
理解 Node.js 中的 process`对象与常用操作
理解 Node.js 中的 process 对象与常用操作 在 Node.js 中,process 是一个全局对象,提供了与当前 Node.js 进程相关的信息和操作。无论是获取进程信息、处理信号、访问环境变量,还是控制进程行为,process 都是不可或缺的工具。 看…...
鸿蒙HarmonyOS NEXT应用崩溃分析及修复
鸿蒙HarmonyOS NEXT应用崩溃分析及修复 如何保证应用的健壮性,其中一个指标就是看崩溃率,如何降低崩溃率,就需要知道存在哪些崩溃,然后对症下药,解决崩溃。那么鸿蒙应用中存在哪些崩溃类型呢?又改如何解决…...
【conda activate无效】 conda: error: argument COMMAND: invalid choice: ‘activate‘
conda activate失效了 在使用conda activate时出现报错: usage: conda [-h] [-v] [--no-plugins] [-V] COMMAND ... conda: error: argument COMMAND: invalid choice: activate (choose from clean, compare, config, create, info, init, install, list, notice…...
Redis + 布隆过滤器解决缓存穿透问题
Redis 布隆过滤器解决缓存穿透问题 1. Redis 布隆过滤器解决缓存穿透问题 📌 什么是缓存穿透? 缓存穿透指的是查询的数据既不在缓存,也不在数据库,导致每次查询都直接访问数据库,增加数据库压力。 例如࿱…...
机器学习——分类、回归、聚类、LASSO回归、Ridge回归(自用)
纠正自己的误区:机器学习是一个大范围,并不是一个小的方向,比如:线性回归预测、卷积神经网络和强化学都是机器学习算法在不同场景的应用。 机器学习最为关键的是要有数据,也就是数据集 名词解释:数据集中的…...
HarmonyOS鸿蒙开发 BuilderParam在父组件的Builder的点击事件报错:Error message:is not callable
HarmonyOS鸿蒙开发 BuilderParam在父组件的Builder的点击事件报错:Error message:is not callable 最近在鸿蒙开发过程中,UI做好了,根据列表item进行点击跳转,报错了 报错信息如下 Error message:is not callable Stacktrace:at…...
【canvas】一键自动布局:如何让流程图节点自动找到最佳位置
一键自动布局:如何让流程图节点自动找到最佳位置 引言 在流程图、拓扑图和系统架构图设计中,节点布局往往是最令人头疼的问题。如果手动调整每个节点位置,不仅耗时费力,还难以保证美观性和一致性。本文将深入解析如何实现自动布…...
[每周一更]-(第137期):Go + Gin 实战:Docker Compose + Apache 反向代理全流程
文章目录 **1. Go 代码示例(main.go)****2. Dockerfile 多段构建**3.构建 Docker 镜像**4. docker-compose.yml 直接拉取镜像****5. 运行容器****6. 测试 API**7、配置域名访问**DNS解析:将域名转换为IP地址****DNS寻址示例** 8.错误记录 访问…...
HTTPS 加密过程详解
HTTPS 详解及其加密过程流程框架 HTTPS(Hypertext Transfer Protocol Secure)是一种基于 HTTP 协议的安全通信协议,通过 SSL/TLS 协议对传输数据进行加密和身份验证,解决了 HTTP 明文传输的安全隐患。以下是其核心原理和加密流程的…...
SpringCache小记
Spring Cache 小记 官方文档:https://springdoc.cn/spring-cache-tutorial/ 基础知识 常用注解 EnableCaching:开启缓存功能,一般放在启动类上。 Cacheable:表示该方法支持缓存。当调用被注解的方法时,如果对应的键已…...
Web-Machine-N7靶机通关攻略
获取靶机ip arp-scan -l 端口扫描 nmap xxxx 访问80端口发现没用 扫描目录 gobuster dir -u http:/192.168.117.160 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium,txt -x php,html,txt ,zip 打开exploit.html 点击F12,修改localhost为靶机ip&#…...
笔记本运行边缘计算
笔记本电脑可以用来运行PCDN(Peer-to-Peer Content Delivery Network)服务。实际上,如果你有闲置的笔记本电脑,并且它具备一定的硬件条件和网络环境,那么它可以成为一个不错的PCDN节点。 运行PCDN的基本要求 硬件需求…...
self Attention为何除以根号dk?(全新角度)
全网最独特解析:self Attention为何除根号dk? 一、假设条件:查询向量和键向量服从正态分布 假设查询向量 q i q_i qi和键向量 k j k_j kj的每个分量均为独立同分布的随机变量,且服从标准正态分布,即:…...
第十五次CCF-CSP认证(含C++源码)
第十五次CCF-CSP认证 小明上学满分思路 数据中心满分思路 小明放学满分题解 小明上学 题目链接 满分思路 其实题目看着长,但是做起来是非常好写的,其实主要原因在于,他的红绿灯的变化规律是一定的,而且小明路上的每次红绿灯情况…...
基于Azure Delta Lake与Databricks的医疗数据变更管理
设计Azure云架构方案实现Azure Delta Lake和Azure Databricks,在医疗场景下记录所有数据变更,满足合规性要求(如 GDPR),并具备回滚能力,能快速恢复误删数据(如 RESTORE TABLE table VERSION AS …...
简述Mybatis的插件运行原理,以及如何编写一个插件?
MyBatis 插件运行原理 MyBatis 插件的核心原理基于 Java 的动态代理和责任链模式。下面详细阐述其工作机制: 动态代理 MyBatis 允许你在四大核心对象(Executor、StatementHandler、ParameterHandler 和 ResultSetHandler)的方法执行前后进…...
Java-servlet(七)详细讲解Servlet注解
Java-servlet(七)详细讲解Servlet注解 前言一、注解的基本概念二、Override 注解2.1 作用与优势2.2 示例代码 三、Target 注解3.1 定义与用途3.2 示例代码 四、WebServlet 注解4.1 作用4.2 示例代码 五、反射与注解5.1 反射的概念5.2 注解与反射的结合使…...
SQLark 实战 | 如何通过对象名和 DDL 快速搜索数据库对象
在数据库运维管理、应用开发和问题定位时,常常需要搜索相关的数据库对象。本文将为你介绍如何使用 SQLark 的搜索功能,实现对数据库对象的快速查找与定位。 👉 前往 SQLark 官网:www.sqlark.com 下载全功能免费版。 通过对象名称搜…...
C/S模型-TCP
下图是基于TCP协议的客户端/服务器程序的一般流程: TCP协议通讯流程 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SY…...
51c自动驾驶~合集24
我自己的原文哦~ https://blog.51cto.com/whaosoft/11926510 #DriveArena 上海AI Lab又放大招:首个高保真闭环生成仿真平台 仓库链接:https://github.com/PJLab-ADG/DriveArena 项目链接:https://pjlab-adg.github.io/DriveArena/ D…...
19.哈希表的实现
1.哈希的概念 哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找。 1.2.直接定址法…...
【PCB工艺】晶体管的发展历史
晶体管被认为是20世纪最伟大的发明之一,因为没有晶体管就不会有现代电脑、手机或平板,你也无法阅读到这里的内容,因为不存在网络。 ——本文纯粹出于对过往奋斗在这个领域中科学家的缅怀。科学家有太多宝贵的思想和经验值得我们认真总结和…...
通向AGI的未来之路!首篇2D/视频/3D/4D统一生成框架全景综述(港科大中山等)
文章链接: https://arxiv.org/pdf/2503.04641 摘要 理解并复现现实世界是人工通用智能(AGI)研究中的一个关键挑战。为实现这一目标,许多现有方法(例如世界模型)旨在捕捉支配物理世界的基本原理࿰…...
C# BindingFlags 使用详解
总目录 前言 在 C# 编程的世界里,反射(Reflection)是一个强大且灵活的特性,它允许我们在运行时动态地获取和操作类型的信息。而 BindingFlags 枚举类型,作为反射中的核心概念之一,为我们提供了精确控制类型…...
【亚马逊云科技】大模型选型实战(挑选和测评对比最适合业务的大模型)
文章目录 前言1、实验内容2、手册内容 一、环境准备二、Prompt 实战与模型配置2.1 基于 Amazon Bedrock 对比测试不同模型的逻辑推理效果2.2 基于 Amazon Bedrock 对比测试不同模型知识问答能力2.3 Prompt 实战结果分析 三、基于 Amazon Bedrock Evaluations 进行模型评测与自动…...
调用feapder作为子程序时setting.py文件不起作用
feaper 官方文档地址: 简介及安装 - feapder官方文档|feapder-document 问题: 在最近的开发中需要调用feapder作为主程序调用的子程序时发现自动入库时无法入库,通过查看日志信息发现连接数据库时被拒绝连接了,但是我的setting.p…...
