学会说不!让你彻底学会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取消技术是什么?你遇到过什么有趣的边缘情况吗?请在下方留言,让我们一起讨论!💬


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

保护原创,请勿转载!

相关推荐
雨白1 小时前
开发 SunnyWeather:Android 天气预报 App(下)
android
_extraordinary_2 小时前
Java 字符串常量池 +反射,枚举和lambda表达式
android·java·开发语言
来来走走3 小时前
Flutter dart运算符
android·前端·flutter
青小莫3 小时前
IDM下载失败常见原因
android
阿华的代码王国3 小时前
【Android】日期选择器
android·xml·java·前端·后端
小墙程序员5 小时前
Android 性能优化(五)Heap Dump 的使用
android·性能优化
阿华的代码王国5 小时前
【Android】RecyclerView实现新闻列表布局(1)适配器使用相关问题
android·xml·java·前端·后端
EngZegNgi5 小时前
Unity —— Android 应用构建与发布
android·unity·自动化·游戏引擎·构建
fatiaozhang95276 小时前
烽火HG680-KX-海思MV320芯片-2+8G-安卓9.0-强刷卡刷固件包
android·电视盒子·刷机固件·机顶盒刷机