Compose - 底层原理(四) - Compose中的Effect

192 阅读8分钟

Compose的基本使用

1. LaunchedEffect

就像是派一个助手去做某件事:

@Composable
fun TimerExample() {
    var time by remember { mutableStateOf(0) }
    
    // 就像安排一个助手每秒更新时间
    LaunchedEffect(Unit) {
        while(true) {
            delay(1000)
            time++
        }
    }
    
    Text("Time: $time")
}

生活例子:

  • 就像你请助手每隔一秒看一下时钟,然后告诉你时间
  • 当你不需要这个服务时(组件销毁),助手就会停止工作

2. DisposableEffect

像是租借设备,用完要归还:

@Composable
fun SensorExample() {
    DisposableEffect(Unit) {
        // 借用传感器
        val sensor = getSensor()
        sensor.start()
        
        // 用完归还
        onDispose {
            sensor.stop()
        }
    }
}

生活例子:

  • 就像你租了一台相机
  • 用完后要记得归还
  • 确保不会浪费资源

3. SideEffect

像是记日记,记录发生的事:

@Composable
fun LoggingExample(count: Int) {
    SideEffect {
        // 记录每次数值变化
        println("Count changed to: $count")
    }
}

生活例子:

  • 就像每次你做完一件事
  • 都在日记本上记一笔
  • 用来追踪发生了什么

4. rememberCoroutineScope

像是雇一个随时待命的助手:

@Composable
fun HelperExample() {
    val helper = rememberCoroutineScope()
    
    Button(onClick = {
        helper.launch {
            // 执行一些耗时任务
            delay(1000)
            showMessage("Done!")
        }
    }) {
        Text("Start Task")
    }
}

生活例子:

  • 就像有个助手随时待命
  • 当你需要做某事时
  • 可以立即安排助手去做

流程图

graph TD
    A[组件创建] --> B{需要什么类型的Effect?}
    B -->|异步任务| C[LaunchedEffect]
    B -->|资源管理| D[DisposableEffect]
    B -->|记录操作| E[SideEffect]
    B -->|待命助手| F[rememberCoroutineScope]
    
    C --> G[执行异步操作]
    G --> H[完成或取消]
    
    D --> I[使用资源]
    I --> J[清理资源]
    
    E --> K[执行同步操作]
    
    F --> L[创建协程作用域]
    L --> M[按需执行任务]

实际应用示例

  1. 定时刷新示例
@Composable
fun RefreshExample() {
    var data by remember { mutableStateOf("") }
    
    // 像是定时提醒
    LaunchedEffect(Unit) {
        while(true) {
            data = fetchNewData()
            delay(5000) // 每5秒刷新一次
        }
    }
    
    Text("Data: $data")
}
  1. 监听器示例
@Composable
fun ListenerExample() {
    // 像是安装和卸载监听器
    DisposableEffect(Unit) {
        val listener = EventListener()
        addEventListener(listener)
        
        onDispose {
            removeEventListener(listener)
        }
    }
}
  1. 组合使用示例
@Composable
fun CombinedExample() {
    val scope = rememberCoroutineScope() // 待命助手
    var data by remember { mutableStateOf("") }
    
    // 定时任务
    LaunchedEffect(Unit) {
        loadInitialData()
    }
    
    // 资源管理
    DisposableEffect(Unit) {
        startService()
        onDispose { stopService() }
    }
    
    // 记录操作
    SideEffect {
        logDataChange(data)
    }
    
    // 用户操作
    Button(onClick = {
        scope.launch { 
            refreshData()
        }
    }) {
        Text("Refresh")
    }
}

使用建议

  1. 选择合适的Effect
  • 异步操作 → LaunchedEffect
  • 资源管理 → DisposableEffect
  • 操作记录 → SideEffect
  • 随时任务 → rememberCoroutineScope
  1. 注意事项
  • Effect要在Composable函数中使用
  • 正确处理清理工作
  • 避免过度使用Effect

这样的设计让我们能够:

  • 有序地管理副作用
  • 正确处理资源
  • 保持代码清晰
  • 提高应用性能

Effect 和compose runtime的交互

Compose Runtime 和 Effect 的交互机制:

1. Compose Runtime 和 Effect 的基本关系

graph TD
    A[Compose Runtime] -->|创建| B[Effect Registry]
    B -->|管理| C[Effect 实例]
    C -->|生命周期| D[Effect 执行]
    D -->|状态变化| A

2. Effect 的注册和执行过程

// 简化的 Runtime 实现
class ComposeRuntime {
    // Effect 注册表
    private val effectRegistry = mutableMapOf<Any, EffectInfo>()
    
    // 执行组合
    fun compose() {
        // 1. 创建组合上下文
        val context = CompositionContext()
        
        // 2. 执行组合
        try {
            // 进入组合阶段
            context.startComposition()
            
            // 执行可组合函数
            content()
            
        } finally {
            // 完成组合
            context.endComposition()
        }
    }
}

// Effect 信息
data class EffectInfo(
    val key: Any,
    val effect: suspend () -> Unit,
    val cleanup: () -> Unit
)

3. LaunchedEffect 的实现原理

@Composable
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val currentComposer = currentComposer
    
    DisposableEffect(key1) {
        // 1. 创建新的协程
        val job = currentComposer.scopeCoroutine.launch {
            block()
        }
        
        // 2. 清理时取消协程
        onDispose {
            job.cancel()
        }
    }
}

生活例子:

  • Compose Runtime 就像一个餐厅经理
  • Effect 就像是不同的服务员
  • 每个服务员负责特定的任务(Effect)
  • 经理负责协调所有服务员的工作

4. 实际工作流程

@Composable
fun EffectExample() {
    // 1. 创建状态
    var count by remember { mutableStateOf(0) }
    
    // 2. 注册 Effect
    LaunchedEffect(count) {
        // 3. Effect 执行
        println("Count changed to: $count")
    }
}

工作流程图:

sequenceDiagram
    participant Composable
    participant Runtime
    participant EffectManager
    participant Effect
    
    Composable->>Runtime: 创建/更新组件
    Runtime->>EffectManager: 检查 Effect 注册
    
    alt 首次执行
        EffectManager->>Effect: 创建新 Effect
    else 键值变化
        EffectManager->>Effect: 重新执行 Effect
    else 键值未变
        EffectManager->>Effect: 保持现有 Effect
    end
    
    Effect-->>Runtime: Effect 执行结果
    Runtime-->>Composable: 更新UI

5. Effect 生命周期管理

class EffectManager {
    private val activeEffects = mutableMapOf<Any, EffectHandle>()
    
    fun manageEffect(key: Any, effect: Effect) {
        // 1. 检查是否存在相同 key 的 Effect
        val existing = activeEffects[key]
        
        if (existing != null) {
            if (existing.shouldUpdate(effect)) {
                // 2. 需要更新:清理旧的,启动新的
                existing.dispose()
                activeEffects[key] = effect.start()
            }
            // 否则保持现有 Effect
        } else {
            // 3. 新建 Effect
            activeEffects[key] = effect.start()
        }
    }
}

6. 状态追踪和重组

class EffectScope {
    private var currentEffect: Effect? = null
    
    fun trackEffect(effect: Effect) {
        // 1. 记录当前正在执行的 Effect
        currentEffect = effect
        
        try {
            // 2. 执行 Effect
            effect.execute()
        } finally {
            // 3. 清理追踪
            currentEffect = null
        }
    }
}

7. 优化机制

class EffectOptimizer {
    fun shouldRecompose(oldKey: Any?, newKey: Any?): Boolean {
        return when {
            // 1. 键值相同,不需要重组
            oldKey === newKey -> false
            
            // 2. 结构相等性检查
            oldKey != null && newKey != null && 
            oldKey == newKey -> false
            
            // 3. 其他情况需要重组
            else -> true
        }
    }
}

关键点总结:

  1. 注册机制

    • Runtime 管理所有 Effect 的注册
    • 每个 Effect 都有唯一的标识
  2. 生命周期管理

    • 创建:首次执行时
    • 更新:键值变化时
    • 销毁:组件销毁时
  3. 状态追踪

    • 自动追踪 Effect 依赖的状态
    • 只在必要时重新执行
  4. 优化策略

    • 避免不必要的重新执行
    • 智能的清理机制
    • 协程作用域管理

这种机制确保了:

  • Effect 的正确执行
  • 资源的有效管理
  • 性能的优化
  • 状态的一致性

开发中的Effect优化

让我从实际开发角度,总结 Effect 的最佳实践和优化建议:

1. 正确使用 Effect 的 key

// ❌ 错误示例:不必要的重组
@Composable
fun BadExample() {
    LaunchedEffect(System.currentTimeMillis()) { // 每次重组都会创建新的 Effect
        doSomething()
    }
}

// ✅ 正确示例:使用稳定的 key
@Composable
fun GoodExample(id: String) {
    LaunchedEffect(id) { // 只在 id 变化时重新执行
        doSomething()
    }
}

2. 避免重复创建 Effect

// ❌ 错误示例:在循环中创建 Effect
@Composable
fun BadListExample(items: List<Item>) {
    items.forEach { item ->
        LaunchedEffect(Unit) {  // 每个 item 都创建一个 Effect
            processItem(item)
        }
    }
}

// ✅ 正确示例:合并 Effect
@Composable
fun GoodListExample(items: List<Item>) {
    LaunchedEffect(items) {  // 一个 Effect 处理所有 items
        items.forEach { item ->
            processItem(item)
        }
    }
}

3. 合理管理资源

// ❌ 错误示例:资源泄露
@Composable
fun BadResourceExample() {
    val context = LocalContext.current
    val locationManager = context.getSystemService(LOCATION_SERVICE) as LocationManager
    
    LaunchedEffect(Unit) {
        locationManager.requestLocationUpdates(/*...*/)
        // 没有清理监听器
    }
}

// ✅ 正确示例:使用 DisposableEffect
@Composable
fun GoodResourceExample() {
    val context = LocalContext.current
    
    DisposableEffect(Unit) {
        val locationManager = context.getSystemService(LOCATION_SERVICE) as LocationManager
        locationManager.requestLocationUpdates(/*...*/)
        
        onDispose {
            locationManager.removeUpdates(/*...*/)
        }
    }
}

想象你租了一个健身房的储物柜:

错误的方式(BadResourceExample):
// 相当于:
fun 使用储物柜() {
    // 租了储物柜
    val 储物柜 = 健身房.租储物柜()
    
    // 开始使用储物柜
    储物柜.存放物品()
    
    // ❌ 走的时候忘记还钥匙了!
    // 下次别人就不能用这个储物柜了
}

这种情况会造成:

  • 储物柜一直被占用
  • 其他人不能使用
  • 浪费资源
正确的方式(GoodResourceExample):
// 相当于:
fun 使用储物柜() {
    DisposableEffect(Unit) {
        // 租储物柜
        val 储物柜 = 健身房.租储物柜()
        储物柜.存放物品()
        
        // ✅ 离开时一定会执行清理工作
        onDispose {
            // 记得归还钥匙
            储物柜.取回物品()
            储物柜.归还()
        }
    }
}

在实际代码中:

  • locationManager.requestLocationUpdates() 就像租用储物柜
  • removeUpdates() 就像归还钥匙
  • DisposableEffect 确保我们不会忘记做清理工作

这样可以:

  • 防止资源泄露
  • 让系统资源能被其他人使用
  • 保持程序运行效率

简单来说:

  • LaunchedEffect 适合做一次性的事情
  • DisposableEffect 适合处理需要"借用-归还"的资源

4. 优化状态更新

// ❌ 错误示例:频繁更新
@Composable
fun BadStateExample(searchQuery: String) {
    LaunchedEffect(searchQuery) {
        performSearch(searchQuery)  // 每次输入都触发搜索
    }
}

// ✅ 正确示例:使用防抖
@Composable
fun GoodStateExample(searchQuery: String) {
    LaunchedEffect(Unit) {
        snapshotFlow { searchQuery }
            .debounce(300L)
            .collect { query ->
                performSearch(query)
            }
    }
}
错误的方式

想象你在餐厅点餐:

// 服务员的做法
fun 错误的点餐方式() {
    // 客人每说一个字,就跑去厨房一次
    LaunchedEffect(客人的话) {
        // 客人:"我要一份..."  跑厨房一次
        // 客人:"我要一份炒..."  又跑厨房一次
        // 客人:"我要一份炒饭..."  再跑厨房一次
        通知厨房(客人的话)
    }
}

这样做的问题:

  • 太频繁了
  • 浪费资源
  • 效率低下
正确的方式

聪明的服务员会这样做:

fun 正确的点餐方式() {
    LaunchedEffect(Unit) {
        // 等客人说完一句话才去厨房
        snapshotFlow { 客人的话 }
            .debounce(300L)  // 等待300毫秒,确保客人说完
            .collect { 完整的订单 ->
                // 客人说完"我要一份炒饭"后
                // 才一次性去通知厨房
                通知厨房(完整的订单)
            }
    }
}

在搜索场景中:

  • 错误方式:用户每输入一个字母都发起搜索请求
  • 正确方式:等用户输入暂停300毫秒后,才发起搜索请求

这样可以:

  • 减少不必要的请求
  • 提高性能
  • 提供更好的用户体验

就像等客人把话说完再行动,而不是听到一个字就跑一次。

防抖动的解释

让我用生活例子解释"防抖"(debounce):

1. 不使用防抖的情况

想象你在电梯里:

// ❌ 没有防抖:每次按键都立即响应
fun 电梯按键() {
    当按下时 {
        立即发送信号()  // 每次按都会触发
    }
}
  • 你快速按了5次"关门"按钮
  • 电梯会响应5次
  • 很浪费,其实一次就够了
2. 使用防抖后
// ✅ 使用防抖:等待一小段时间,确认没有新的按键才响应
fun 智能电梯按键() {
    当按下时 {
        等待300毫秒()  // 如果300毫秒内又有人按,就重新计时
        如果没人再按了 {
            发送信号()  // 只响应最后一次按键
        }
    }
}
  • 你快速按了5次"关门"按钮
  • 电梯会等你按完
  • 只响应最后一次按键
  • 更智能,更节省资源
在搜索场景中:
// 使用防抖的搜索框
LaunchedEffect(Unit) {
    snapshotFlow { searchQuery }
        .debounce(300L)      // 等待300毫秒
        .collect { query ->  // 如果300毫秒内没有新输入,才搜索
            performSearch(query)
        }
}

比如用户输入"android":

  1. 输入"a" -> 等待
  2. 输入"an" -> 重新等待
  3. 输入"and" -> 重新等待
  4. 输入"andr" -> 重新等待
  5. 输入"andro" -> 重新等待
  6. 输入"androi" -> 重新等待
  7. 输入"android" -> 等待300ms后,执行搜索

这样可以:

  • 减少不必要的搜索请求
  • 提高应用性能
  • 提供更好的用户体验

就像智能电梯一样,等你真正按完了才动作,而不是每按一次就反应一次。

5. 协程作用域管理

// ❌ 错误示例:作用域管理不当
@Composable
fun BadScopeExample() {
    val scope = CoroutineScope(Dispatchers.Main)  // 错误:生命周期不跟随组件
    
    Button(onClick = {
        scope.launch {
            doSomething()
        }
    }) { /*...*/ }
}

// ✅ 正确示例:使用 rememberCoroutineScope
@Composable
fun GoodScopeExample() {
    val scope = rememberCoroutineScope()  // 正确:生命周期跟随组件
    
    Button(onClick = {
        scope.launch {
            doSomething()
        }
    }) { /*...*/ }
}
错误的方式

想象你请了一个临时工:

// ❌ 错误方式
fun 临时工例子() {
    val 临时工 = 随便找个人()  // 这个人跟你的店没关系
    
    当顾客来时 {
        让临时工去干活 {
            处理顾客需求()
        }
    }
}

问题在于:

  • 这个临时工不属于你的店
  • 就算店关门了,他还在那干活
  • 浪费资源,可能造成混乱
正确的方式

雇佣正式员工:

// ✅ 正确方式
fun 正式员工例子() {
    val 正式员工 = 雇佣店员()  // 这个人跟随店铺的营业时间
    
    当顾客来时 {
        让正式员工去干活 {
            处理顾客需求()
        }
    }
}

好处是:

  • 正式员工跟随店铺营业时间
  • 店关门时,员工也下班了
  • 资源管理更合理

在代码中:

  • CoroutineScope(Dispatchers.Main) 就像临时工,不会跟随组件生命周期
  • rememberCoroutineScope() 就像正式员工,会跟随组件的创建和销毁

简单说:

  • 使用 rememberCoroutineScope 能确保:
    • 组件在用时,协程在运行
    • 组件销毁时,协程也会停止
    • 不会造成资源浪费和内存泄露

就像雇佣正式员工比随便找临时工更可靠一样。

6. 组合多个 Effect

// ❌ 错误示例:Effect 分散
@Composable
fun BadMultipleEffects(id: String) {
    LaunchedEffect(id) { loadData() }
    LaunchedEffect(id) { trackAnalytics() }
    LaunchedEffect(id) { updateUI() }
}

// ✅ 正确示例:合并相关 Effect
@Composable
fun GoodMultipleEffects(id: String) {
    LaunchedEffect(id) {
        // 并行执行多个任务
        coroutineScope {
            launch { loadData() }
            launch { trackAnalytics() }
            launch { updateUI() }
        }
    }
}

7. 错误处理

// ✅ 推荐示例:添加错误处理
@Composable
fun ErrorHandlingExample() {
    LaunchedEffect(Unit) {
        try {
            // 执行可能失败的操作
            riskyOperation()
        } catch (e: Exception) {
            // 错误处理
            handleError(e)
        } finally {
            // 清理工作
            cleanup()
        }
    }
}

8. 性能优化建议

flowchart TD
    A[Effect 优化] --> B[减少重组]
    A --> C[资源管理]
    A --> D[状态更新]
    
    B --> B1[使用稳定的 key]
    B --> B2[合并 Effect]
    
    C --> C1[及时清理]
    C --> C2[使用 DisposableEffect]
    
    D --> D1[防抖/节流]
    D --> D2[批量更新]

9. 实际应用示例

@Composable
fun OptimizedFeature() {
    var data by remember { mutableStateOf<Data?>(null) }
    val scope = rememberCoroutineScope()
    
    // 1. 数据加载
    LaunchedEffect(Unit) {
        try {
            data = loadData()
        } catch (e: Exception) {
            handleError(e)
        }
    }
    
    // 2. 资源管理
    DisposableEffect(Unit) {
        val listener = createListener()
        onDispose { listener.release() }
    }
    
    // 3. 状态更新
    val filteredData by remember(data) {
        derivedStateOf {
            data?.filter { it.isValid }
        }
    }
    
    // 4. 用户操作
    Button(
        onClick = {
            scope.launch {
                updateData()
            }
        }
    ) {
        Text("Update")
    }
}

关键优化点总结:

  1. 使用稳定的 key 避免不必要的重组
  2. 合理合并和拆分 Effect
  3. 正确管理资源和清理工作
  4. 优化状态更新频率
  5. 使用正确的协程作用域
  6. 添加适当的错误处理
  7. 注意性能影响

这些优化可以帮助:

  • 提高应用性能
  • 减少资源消耗
  • 提升代码可维护性
  • 避免常见问题