Android Kotlin 协程中的异常处理与任务取消

举报
yd_223002268 发表于 2026/09/08 08:43:09 2026/09/08
【摘要】 Android Kotlin 协程中的异常处理与任务取消协程让异步代码更接近顺序写法,但“代码更短”不代表异常会自动得到正确处理。实际项目中,最常见的问题包括子任务失败导致整个作用域取消、取消异常被误当作普通错误、界面离开后任务仍在执行,以及并发请求之间缺少明确关系。 一、先理解结构化并发结构化并发要求协程存在清晰的父子关系。父协程结束时,子协程也应结束;普通子协程失败时,异常会向上传递并...

Android Kotlin 协程中的异常处理与任务取消

协程让异步代码更接近顺序写法,但“代码更短”不代表异常会自动得到正确处理。实际项目中,最常见的问题包括子任务失败导致整个作用域取消、取消异常被误当作普通错误、界面离开后任务仍在执行,以及并发请求之间缺少明确关系。

一、先理解结构化并发

结构化并发要求协程存在清晰的父子关系。父协程结束时,子协程也应结束;普通子协程失败时,异常会向上传递并取消同级任务。

viewModelScope.launch {
    val profile = async { repository.loadProfile() }
    val messages = async { repository.loadMessages() }

    val uiModel = HomeUiModel(
        profile = profile.await(),
        messages = messages.await()
    )
    _uiState.value = HomeUiState.Content(uiModel)
}

这段代码表达了明确语义:两个结果都成功,首页数据才完整。如果任何一个请求失败,另一个任务也会被取消。对于必须成组成功的业务,这是合理行为。

二、区分业务失败与协程取消

协程通过 CancellationException 传播取消信号。捕获所有异常后不再抛出取消异常,会破坏取消机制,使已经离开页面的任务继续运行。

suspend fun loadProfileSafely(): Profile {
    return try {
        repository.loadProfile()
    } catch (exception: CancellationException) {
        throw exception
    } catch (exception: IOException) {
        throw ProfileLoadException(exception)
    }
}

如果使用 runCatching 包裹挂起调用,也要确认当前 Kotlin 版本和项目封装是否会吞掉取消信号。更容易审查的做法,是只捕获确实能够处理的异常类型。

三、在正确层级转换错误

数据层负责把底层异常转换为稳定的业务错误,ViewModel 决定错误如何成为页面状态,界面层只负责展示。

sealed interface LoadResult<out T> {
    data class Success<T>(val data: T) : LoadResult<T>
    data object NetworkUnavailable : LoadResult<Nothing>
    data object Unauthorized : LoadResult<Nothing>
    data class ServerRejected(val reason: String) : LoadResult<Nothing>
}

不要把底层异常文本原样显示给用户。异常信息可能不稳定,也可能包含用户无法理解的技术细节。业务层应根据协议中已经约定的错误类型进行映射,无需为不存在的字段设计大量猜测性分支。

四、按业务关系选择 supervisorScope

有些页面允许局部失败。例如首页包含账户、公告和推荐模块,公告加载失败不应阻止账户信息显示。这时可以使用 supervisorScope 隔离子任务失败,但仍需分别处理每个结果。

suspend fun loadDashboard(): DashboardData = supervisorScope {
    val account = async { repository.loadAccount() }
    val noticeResult = async {
        try {
            NoticeResult.Success(repository.loadNotices())
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: IOException) {
            NoticeResult.Failed
        }
    }

    DashboardData(
        account = account.await(),
        noticeResult = noticeResult.await()
    )
}

supervisorScope 不是通用保险开关。只有子任务确实允许独立失败时才应使用,否则会掩盖原本需要整体失败的业务关系。

五、避免重复任务

搜索联想、输入校验等场景中,新请求到来时旧结果已经没有价值,应主动取消前一个任务。

private var searchJob: Job? = null

fun search(keyword: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(300)
        val result = repository.search(keyword)
        _searchState.value = SearchState.Content(result)
    }
}

对于连续数据流,也可以使用 mapLatestflatMapLatest 表达“只关心最新输入”。选择哪种方式,应与项目现有架构保持一致。

六、确保耗时任务可取消

挂起函数通常会在挂起点检查取消,但纯计算循环可能长时间没有挂起点。可以周期性调用 ensureActive

suspend fun calculate(items: List<RawItem>): List<ResultItem> {
    return withContext(Dispatchers.Default) {
        items.mapIndexed { index, item ->
            if (index % 100 == 0) ensureActive()
            transform(item)
        }
    }
}

网络、数据库和文件组件也应支持取消。若底层调用不可取消,上层取消只能停止等待,无法真正终止资源消耗。

七、不要随意创建全局作用域

业务任务如果脱离页面和应用组件的生命周期,就很难确定何时结束。界面相关任务使用 lifecycleScope,页面业务状态使用 viewModelScope,应用级任务则交给项目已有的后台任务体系。

只有确实需要跨页面持续执行的任务,才应该提升作用域,而且需要明确负责人、停止条件和失败处理策略。

八、测试异常与取消路径

协程测试不能只覆盖成功结果,还应包含:

  • 第一个并发任务失败时,其他任务是否按预期取消。
  • 页面销毁或新搜索到来时,旧任务是否停止。
  • 取消是否被错误地转换为失败提示。
  • 局部模块失败时,其他模块是否仍能展示。
  • 重试过程中是否会产生两个同时运行的请求。

通过可控制的测试调度器和伪数据源,可以稳定复现这些时序,不必依赖真实等待。

总结

协程异常处理的关键不是到处添加 try-catch,而是明确任务之间的关系。需要共同成功时使用普通父子作用域,允许局部失败时谨慎使用监督作用域;始终保留取消语义,并把底层异常转换为稳定业务状态。只有作用域、异常和取消三者边界都清晰,异步代码才真正可靠。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。