学会说不!让你彻底学会Kotlin Flow的取消机制

本文译自「Cancellable Flows in Kotlin Coroutines: The Complete Guide to Flow Cancellation Techniques」,原文链接proandroiddev.com/cancellable...,由Sahil Thakar发布于2025年7月21日。

译者按: 本文并不是Flow的基础教程,而是专门讲解如何取消flow的,适合对Flow有一定基础的同学。如果对Flow还不够熟悉,可以先行阅读一下之前的文章:


大家好!👋

今天我们将深入探讨 Kotlin Flows 中最重要的一个方面------Flow取消。如果你一直在使用 Flows,你可能遇到过需要停止长时间运行的 Flow 操作的情况,这可能是因为用户离开了某个页面,或者网络请求耗时过长,又或者你只是想避免不必要的资源消耗。

Flow 取消不仅仅是调用 cancel() 并祈祷好运。它有多种复杂的技术,每种技术都有各自的用例和细微差别。那么,让我们揭开帷幕,探索可取消 Flows 的世界吧! 🚀

为何取消Flow如此重要

在深入探讨相关技术之前,我们先来了解一下取消机制为何如此重要:

  • 资源管理:防止内存泄漏和不必要的 CPU 占用
  • 用户体验:当用户离开时停止过时的操作
  • 网络效率:取消不再需要的待处理请求
  • 电池续航:减少移动设备上的后台处理

方法 1:Job取消 --- 基础

取消Flow最基本的方式是通过作业取消(Job Cancellation)。每个协程都有一个作业(Job),当你取消该作业时,该协程范围内运行的所有Flow也会被取消。

让我们来看看实际操作:

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.seconds

suspend fun main() {
    val job = CoroutineScope(Dispatchers.Default).launch {
        createNumberFlow()
            .collect { value ->
                println("Received: $value")
            }
    }
    delay(3.seconds)
    println("Cancelling job...")
    job.cancel()
    delay(1.seconds)
    println("Program finished")
}

fun createNumberFlow() = flow {
    repeat(10) { i ->
        println("Emitting: $i")
        emit(i)
        delay(1.seconds)
    }
}

Output:

apache 复制代码
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Cancelling job...
Program finished

这个过程里面到底发生了什么?

当你调用 job.cancel() 时,它会向协程发送一个取消信号。Flow构建器 (flow { }) 是取消协作式 的,这意味着它会在 delay()emit() 等挂起点检查取消情况。一旦取消,Flow就会停止发出新值,收集器也会停止接收它们。

但这里有一个有趣的问题------如果你的Flow没有挂起点怎么办?

kotlin 复制代码
fun nonCancellableFlow() = flow {
    repeat(1000000) { i ->
        emit(i) // 无挂起点!
        // 即使取消后仍将继续运行
    }
}

此Flow不会考虑取消操作,因为没有挂起点。要解决这个问题,你可以使用"ensureActive()":

kotlin 复制代码
fun cancellableFlow() = flow {
    repeat(1000000) { i ->
        ensureActive() // 会有对取消的检测
        emit(i)
    }
}

使用结构化并发实现高级作业取消

让我们探索一个使用结构化并发(Structured Concurrency)的更复杂的场景:

kotlin 复制代码
class DataRepository {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun fetchDataStream(): Flow<String> = flow {
        repeat(Int.MAX_VALUE) { i ->
            emit("Data item $i")
            delay(500.milliseconds)
        }
    }.flowOn(Dispatchers.IO)

    fun startFetching(): Job {
        return scope.launch {
            fetchDataStream()
                .catch { e -> println("Error: ${e.message}") }
                .collect { data ->
                    println("Processing: $data")
                }
        }
    }

    fun cleanup() {
        scope.cancel("Repository is being cleaned up")
    }
}

// Usage
suspend fun main() {
    val repository = DataRepository()
    val fetchJob = repository.startFetching()
    delay(3.seconds)
    println("Cleaning up repository...")
    repository.cleanup()
    delay(1.seconds)
    println("Done")
}

Output:

lasso 复制代码
Processing: Data item 0
Processing: Data item 1
Processing: Data item 2
Processing: Data item 3
Processing: Data item 4
Processing: Data item 5
Cleaning up repository...
Done

方法 2:withTimeout --- 基于时间的取消

有时,如果Flow操作耗时过长,你需要取消它。"withTimeout"非常适合这种情况。它会创建一个定时炸弹 ⏰ --- 如果操作未在指定时间内完成,则会抛出"TimeoutCancellationException"异常。

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.seconds

suspend fun main() {
    try {
        withTimeout(5.seconds) {
            slowDataFlow()
                .collect { value ->
                    println("Received: $value")
                }
        }
    } catch (e: TimeoutCancellationException) {
        println("Operation timed out: ${e.message}")
    }
}

fun slowDataFlow() = flow {
    repeat(10) { i ->
        println("Emitting: $i")
        emit(i)
        delay(1.seconds) // Each emission takes 1 second
    }
}

Output:

avrasm 复制代码
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Operation timed out: Timed out waiting for 5000 ms

withTimeoutOrNull --- 优雅的超时处理

如果你不想出现异常,请使用 withTimeoutOrNull

kotlin 复制代码
suspend fun main() {
    val result = withTimeoutOrNull(3.seconds) {
        fastDataFlow()
            .toList() // Collect all values into a list
    }
    when (result) {
        null -> println("Operation timed out")
        else -> println("Completed with results: $result")
    }
}

fun fastDataFlow() = flow {
    repeat(5) { i ->
        emit(i)
        delay(500.milliseconds)
    }
}

Output:

nix 复制代码
Completed with results: [0, 1, 2, 3, 4]

真实示例:带超时的网络请求

以下是使用超时进行网络请求的实际示例:

kotlin 复制代码
class ApiService {
    suspend fun fetchUserData(userId: String): Flow<UserData> = flow {
        // Simulate network delay
        delay(2.seconds)
        emit(UserData(userId, "John Doe", "john@example.com"))
        delay(1.seconds)
        emit(UserData(userId, "John Doe", "john.doe@example.com")) // Updated email
    }
}

data class UserData(val id: String, val name: String, val email: String)

suspend fun main() {
    val apiService = ApiService()
    try {
        withTimeout(4.seconds) {
            apiService.fetchUserData("123")
                .collect { userData ->
                    println("User data updated: $userData")
                }
        }
    } catch (e: TimeoutCancellationException) {
        println("Network request timed out. Please check your connection.")
    }
}

Output:

less 复制代码
User data updated: UserData(id=123, name=John Doe, email=john@example.com)
User data updated: UserData(id=123, name=John Doe, email=john.doe@example.com)

方法 3:takeWhile --- 条件取消

takeWhile 是一个强大的运算符,它根据条件 取消Flow。只要谓词返回 true,它就会继续发出值;一旦返回 false,它就会停止(取消)。

kotlin 复制代码
suspend fun main() {
    numberSequence()
        .takeWhile { it < 5 } // Stop when value >= 5
        .collect { value ->
            println("Received: $value")
        }

    println("Flow completed")
}

fun numberSequence() = flow {
    repeat(10) { i ->
        println("Emitting: $i")
        emit(i)
        delay(500.milliseconds)
    }
}

Output:

apache 复制代码
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5
Flow completed

注意,该Flow发送了 5 个值,但却没有收到,因为 takeWhile 在条件变为 false 时停止了收集。

takeWhile 与 filter --- 理解区别

许多开发人员会混淆 takeWhilefilter。让我们看看它们的区别:

kotlin 复制代码
suspend fun main() {
    println("=== Using filter ===")
    numberSequence()
        .filter { it < 5 } // 过滤值但不停止flow
        .collect { value ->
            println("Received: $value")
        }

    println("\n=== Using takeWhile ===")
    numberSequence()
        .takeWhile { it < 5 } // 完全停止flow
        .collect { value ->
            println("Received: $value")
        }
}

Output:

vbnet 复制代码
=== Using filter ===
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5
Emitting: 6
Emitting: 7
Emitting: 8
Emitting: 9

=== Using takeWhile ===
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5

关键区别filter 会继续执行Flow,但会跳过不需要的值,而 takeWhile 则会完全终止Flow。

实际 takeWhile 示例

示例 1:电池电量监控

kotlin 复制代码
fun batteryLevelFlow() = flow {
    var batteryLevel = 100
    while (true) {
        emit(batteryLevel)
        batteryLevel = (batteryLevel - (1..5).random()).coerceAtLeast(0)
        delay(1.seconds)
    }
}

suspend fun main() {
    batteryLevelFlow()
        .takeWhile { it > 20 } // Stop monitoring when battery is low
        .collect { level ->
            println("Battery level: $level%")
            if (level <= 30) {
                println("⚠️ Low battery warning!")
            }
        }
     println("🔋 Battery critically low! Stopping monitoring.")
}

示例 2:股票价格监控

kotlin 复制代码
data class StockPrice(val symbol: String, val price: Double)

fun stockPriceFlow(symbol: String) = flow {
    var price = 100.0
    while (true) {
        price += (-5.0..5.0).random()
        emit(StockPrice(symbol, price))
        delay(1.seconds)
    }
}

suspend fun main() {
    val stopLossPrice = 90.0
    stockPriceFlow("AAPL")
        .takeWhile { it.price > stopLossPrice } // Stop loss triggered
        .collect { stock ->
            println("${stock.symbol}: $${String.format("%.2f", stock.price)}")
        }
    println("🛑 Stop loss triggered! Stopping price monitoring.")
}

方法 4:take --- 基于数量的取消

take 在发出特定数量的项目后取消Flow:

kotlin 复制代码
suspend fun main() {
    infiniteFlow()
        .take(5) // 仅取前5个元素
        .collect { value ->
            println("Received: $value")
        }
    println("Collected exactly 5 items")
}

fun infiniteFlow() = flow {
    var counter = 0
    while (true) {
        emit(counter++)
        delay(300.milliseconds)
    }
}

方法 5:cancellable() --- 使Flow具备取消感知能力

有时,你的Flow本身并不支持取消操作。cancellable() 运算符可以使它们对取消操作做出响应:

kotlin 复制代码
suspend fun main() {
    val job = CoroutineScope(Dispatchers.Default).launch {
        heavyComputationFlow()
            .cancellable() // 使flow感知取消
            .collect { value ->
                println("Processed: $value")
            }
    }        delay(2.seconds)
    println("Cancelling...")
    job.cancel()
    delay(500.milliseconds)
    println("Done")
}

fun heavyComputationFlow() = flow {
    repeat(1000) { i ->
        // 模拟无挂起点的繁重计算
        val result = (1..1000).map { it * it }.sum()
        emit("Result $i: $result")
    }
}

方法 6:first() 和条件终止运算符

你关于 first { condition } 的说法完全正确!这是一种强大的取消技术,Flow会一直收集,直到找到第一个符合条件的元素,然后取消Flow并返回该元素。

kotlin 复制代码
suspend fun main() {
    val result = numberFlow()
        .first { it > 5 } // Cancels as soon as it finds first value > 5
    println("First value > 5: $result")
}

fun numberFlow() = flow {
    repeat(20) { i ->
        println("Emitting: $i")
        emit(i)
        delay(200.milliseconds)
    }
}

Output:

apache 复制代码
Emitting: 0
Emitting: 1
Emitting: 2
Emitting: 3
Emitting: 4
Emitting: 5
Emitting: 6
First value > 5: 6

注意,在找到第一个大于 5 的值后,Flow是如何停止发射 的。这与 filter 不同,后者会继续整个Flow。

对比 first() 和 firstOrNull()

kotlin 复制代码
suspend fun main() {
    // first() - 如果未找到则抛出异常
    try {
        val result1 = shortFlow().first { it > 10 }
        println("Found: $result1")
    } catch (e: NoSuchElementException) {
        println("No element found matching condition")
    }
    // firstOrNull() - 如果未找到则返回 null
    val result2 = shortFlow().firstOrNull { it > 10 }
    println("Result: $result2")
}

fun shortFlow() = flowOf(1, 2, 3, 4, 5)

Output:

sql 复制代码
No element found matching condition
Result: null

真实示例:查找可用服务器

kotlin 复制代码
data class Server(val name: String, val responseTime: Int)

fun checkServers() = flow {
    val servers = listOf("server1", "server2", "server3", "server4")
    servers.forEach { serverName ->
        println("Checking $serverName...")
        delay(500.milliseconds) // 模拟网络检查
        val responseTime = (100..800).random()
        emit(Server(serverName, responseTime))
    }
}

suspend fun main() {
    val fastServer = checkServers()
        .first { it.responseTime < 300 } // 找到第一个快速服务器并停止
    println("Using fast server: ${fastServer.name} (${fastServer.responseTime}ms)")
}

方法 7:single() --- 期望恰好一个元素

single()first() 类似,但它期望恰好一个元素。找到第一个元素后会取消,但如果有多个元素,则会引发异常。

kotlin 复制代码
suspend fun main() {
    // 这能行 - 恰好有一个元素匹配
    val result1 = flowOf(1, 2, 3, 4, 5)
        .single { it == 3 }
    println("Single result: $result1")
    // 这会抛异常 - 有多个元素匹配
    try {
        val result2 = flowOf(1, 2, 3, 4, 5)
            .single { it > 2 } // 3, 4, 5 all match!
        println("Result: $result2")
    } catch (e: IllegalArgumentException) {
        println("Error: Multiple elements found - ${e.message}")
    }
}

Output:

subunit 复制代码
Single result: 3
Error: Multiple elements found - Flow has more than one element matching the predicate.

方法 8:any()、all()、none() --- 布尔条件取消

这些运算符可根据布尔条件提供提前取消功能:

any() --- 首次匹配时取消

kotlin 复制代码
suspend fun main() {
    val hasLargeNumber = numberFlow()
        .any { it > 15 } // 一旦发现第一个值 > 15 则取消
    println("Has number > 15: $hasLargeNumber")
}

fun numberFlow() = flow {
    repeat(30) { i ->
        println("Checking: $i")
        emit(i)
        delay(100.milliseconds)
    }
}

Output:

yaml 复制代码
Checking: 0
Checking: 1
...
Checking: 15
Checking: 16
Has number > 15: true

all() --- 第一次不匹配时取消

kotlin 复制代码
suspend fun main() {
    val allSmall = flowOf(1, 2, 3, 4, 5, 15, 6, 7)
        .all { it < 10 } // 一旦发现第一个值 >= 10 就取消
    println("All numbers < 10: $allSmall")
}

none() --- 第一次匹配时取消

kotlin 复制代码
suspend fun main() {
    val noLargeNumbers = flowOf(1, 2, 3, 4, 5, 15, 6, 7)
        .none { it > 10 } // 一旦发现第一个值 > 10 则取消
    println("No numbers > 10: $noLargeNumbers")
}

方法 9:transformWhile --- 高级条件转换

transformWhiletakeWhile 的更强大版本,它允许你转换元素,并且具有更灵活的发射行为:

kotlin 复制代码
suspend fun main() {
    numberFlow()
        .transformWhile { value ->
            if (value < 5) {
                emit("Value: $value")
                emit("Double: ${value * 2}") // Can emit multiple times
                true // Continue
            } else {
                emit("Final: $value") // Can emit the "stopping" element
                false // Stop here
            }
        }
        .collect { println(it) }
}

Output:

apache 复制代码
Emitting: 0
Value: 0
Double: 0
Emitting: 1
Value: 1
Double: 2
Emitting: 2
Value: 2
Double: 4
Emitting: 3
Value: 3
Double: 6
Emitting: 4
Value: 4
Double: 8
Emitting: 5
Final: 5

对比transformWhile和takeWhile

kotlin 复制代码
suspend fun main() {
    println("=== takeWhile ===")
    flowOf(1, 2, 3, 4, 5, 6)
        .takeWhile { it < 4 }
        .collect { println("Received: $it") }
    println("\n=== transformWhile ===")
    flowOf(1, 2, 3, 4, 5, 6)
        .transformWhile { value ->
            if (value < 4) {
                emit("Valid: $value")
                true
            } else {
                emit("Stopping at: $value") // 仍可以发出正被停止的元素!
                false
            }
        }
        .collect { println(it) }
}

Output:

asciidoc 复制代码
=== takeWhile ===
Received: 1
Received: 2
Received: 3

=== transformWhile ===
Valid: 1
Valid: 2
Valid: 3
Stopping at: 4

方法 10:collectLatest --- 取消上一次终端处理

每当发出新值时,collectLatest 都会取消上一次收集操作,即上一次的终端处理:

kotlin 复制代码
suspend fun main() {
    fastEmittingFlow()
        .collectLatest { value ->
            println("Processing $value...")
            delay(1.seconds) // Slow processing
            println("Finished processing $value")
        }
}

fun fastEmittingFlow() = flow {
    repeat(5) { i ->
        emit(i)
        delay(300.milliseconds) // Fast emission
    }
}

Output:

gams 复制代码
Processing 0...
Processing 1...
Processing 2...
Processing 3...
Processing 4...
Finished processing 4

只有最后一个值会被完全处理,因为每次新的发送都会取消之前的处理。

collectLatest 实用示例:即时搜索的实现

kotlin 复制代码
class SearchManager {
    private val _searchQuery = MutableStateFlow("")
    suspend fun startSearching() {
        _searchQuery
            .filter { it.isNotBlank() }
            .collectLatest { query ->
                println("Searching for: $query")
                delay(2.seconds) // Simulate API call
                println("Results for: $query")
            }
    }

    fun updateQuery(query: String) {
        _searchQuery.value = query
    }
}

suspend fun main() {
    val searchManager = SearchManager()
    val job = CoroutineScope(Dispatchers.Default).launch {
        searchManager.startSearching()
    }
    // Simulate user typing
    searchManager.updateQuery("k")
    delay(500.milliseconds)
    searchManager.updateQuery("ko")
    delay(500.milliseconds)
    searchManager.updateQuery("kot")
    delay(500.milliseconds)
    searchManager.updateQuery("kotlin")
    delay(3.seconds)
    job.cancel()
}

Output:

nestedtext 复制代码
Searching for: k
Searching for: ko
Searching for: kot
Searching for: kotlin
Results for: kotlin

只有"kotlin"会获得完整的搜索结果,因为之前的搜索已被新查询取消。

方法 11:使用 SharedFlow 和 StateFlow 自定义取消

对于更复杂的场景,你可能需要自定义取消逻辑:

kotlin 复制代码
class DataManager {
    private val _dataFlow = MutableSharedFlow<String>()
    val dataFlow = _dataFlow.asSharedFlow()
    private var isActive = true
    suspend fun startEmitting() {
        while (isActive) {
            _dataFlow.emit("Data at ${System.currentTimeMillis()}")
            delay(1.seconds)
        }
    }

    fun stop() {
        isActive = false
    }
}

suspend fun main() {
    val dataManager = DataManager()    
    val job = CoroutineScope(Dispatchers.Default).launch {
        dataManager.startEmitting()
    }    
    val collectorJob = CoroutineScope(Dispatchers.Default).launch {
        dataManager.dataFlow.collect { data ->
            println("Received: $data")
        }
    }   
    delay(3.seconds)
    println("Stopping data manager...")
    dataManager.stop()   
    delay(1.seconds)
    job.cancel()
    collectorJob.cancel()
    println("All jobs cancelled")
}

最佳实践及各技术的使用时机

1. Job取消

  • 使用时机:你需要取消整个协程范围
  • 最佳用途:Activity/Fragment 生命周期管理、代码库清理
  • 记住:始终取消父作业以避免内存泄漏

2. withTimeout/withTimeoutOrNull

  • 使用时机:操作有时间限制
  • 最佳用途:网络请求、文件操作、用户输入等待
  • 记住 :考虑使用 withTimeoutOrNull 进行优雅处理

3. takeWhile

  • 使用时机:你有基于条件的停止条件
  • 最佳用途:监控系统、用户输入验证、基于阈值的操作
  • 记住:一旦条件变为 false,Flow就会停止

4. take

  • 使用时机:你需要特定数量的项目
  • 最佳用途:分页、采样、测试场景
  • 记住:简单且可预测的行为

5. cancellable()

  • 使用场景:处理 CPU 密集型且无挂起点的Flow
  • 最适合:数学计算、数据处理
  • 记住:会增加开销,因此仅在必要时使用

性能相关注意事项

不同的取消技术对性能的影响不尽相同:

kotlin 复制代码
suspend fun performanceComparison() {
    println("=== Performance Test ===")        // 测试 1:作业取消(最快)
    val time1 = measureTimeMillis {
        val job = CoroutineScope(Dispatchers.Default).launch {
            repeat(1000000) {
                // Heavy work
            }
        }
        job.cancel()
    }
    println("Job cancellation: ${time1}ms")        // 测试 2:takeWhile(条件开销)
    val time2 = measureTimeMillis {
        flow {
            repeat(1000) { emit(it) }
        }.takeWhile { it < 500 }
         .collect { }
    }
    println("takeWhile: ${time2}ms")        // 测试 3:withTimeout(异常开销)
    val time3 = measureTimeMillis {
        try {
            withTimeout(1.milliseconds) {
                repeat(1000000) {
                    // Some work
                }
            }
        } catch (e: TimeoutCancellationException) {
            // Expected
        }
    }
    println("withTimeout: ${time3}ms")
}

常见陷阱及避免方法

陷阱 1:忘记检查 CPU 密集型操作中的取消操作

kotlin 复制代码
// ❌ 错误------没考虑取消
fun badFlow() = flow {
    repeat(1000000) { i ->
        heavyComputation()
        emit(i)
    }
}

// ✅ 正确 - 检查取消
fun goodFlow() = flow {
    repeat(1000000) { i ->
        ensureActive() // or yield()
        heavyComputation()
        emit(i)
    }
}

陷阱 2:不处理 TimeoutCancellationException

kotlin 复制代码
// ❌ 错误 - 异常会导致应用程序崩溃
suspend fun badTimeout() {
    withTimeout(1.seconds) {
        longRunningOperation()
    }
}

// ✅ 正确 - 正确的异常处理
suspend fun goodTimeout() {
    try {
        withTimeout(1.seconds) {
            longRunningOperation()
        }
    } catch (e: TimeoutCancellationException) {
        println("Operation timed out, handling gracefully")
    }
}

陷阱 3:混淆 takeWhile 和 filter

kotlin 复制代码
// 记住:takeWhile 会停止Flow,filter 只会跳过值
flow { emit(1); emit(2); emit(3) }
    .takeWhile { it < 2 }  // 发射:1(然后停止)
    .filter { it < 2 }     // 这甚至不会运行,因为 takeWhile 停止了flow

结论

Flow取消是一项强大的功能,如果使用得当,可以显著提升应用的性能和用户体验。以下是简要回顾:

  • 作业取消(Job cancellation) 用于生命周期管理
  • withTimeout 用于时间限制操作
  • takeWhile 用于基于条件的停止
  • take 用于基于计数的限制
  • cancellable() 用于使非合作性Flow具有响应性

请记住,有效地取消Flow的关键在于理解你的用例并选择正确的技术。不要想太多------从最简单的解决问题的方法入手,然后再进行优化。

祝你使用愉快!🌊

你最喜欢的Flow取消技术是什么?你遇到过什么有趣的边缘情况吗?请在下方留言,让我们一起讨论!💬


欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
雨白44 分钟前
Android 自定义 View:精通文字的测量与高级排版
android
Jasonakeke1 小时前
【重学MySQL】八十八、8.0版本核心新特性全解析
android·数据库·mysql
一条上岸小咸鱼3 小时前
Kotlin 类型检查与转换
android·kotlin
闲暇部落4 小时前
android studio配置 build
android·android studio·build
_祝你今天愉快5 小时前
Android FrameWork - Zygote 启动流程分析
android
龙之叶6 小时前
Android系统模块编译调试与Ninja使用指南
android
源码哥_博纳软云7 小时前
JAVA国际版多商户运营版商城系统源码多商户社交电商系统源码支持Android+IOS+H5
android·java·ios·微信·微信小程序·小程序·uni-app
用户2018792831677 小时前
bindService是如何完成binder传递的?
android
洞见不一样的自己7 小时前
Android 小知识点
android
tangweiguo030519879 小时前
Flutter性能优化完全指南:构建流畅应用的实用策略
android·flutter