一、为什么Android生产环境里协程异常处理是刚需

1.1 异步崩了的隐形危害

做Android开发的都懂,异步操作是日常刚需——刷列表加载网络数据、上传图片处理后台任务,这些要是卡了主界面,用户分分钟划走。现在大多用Kotlin协程做异步,比线程、AsyncTask清爽太多,但很多人只盯着“让异步跑起来”,完全忽略“异步跑崩了怎么办”,这在生产环境绝对是定时炸弹。举个常见错误例子:

// 技术栈:Kotlin 1.8,Android Gradle 7.4,Coroutines 1.6.4
import kotlinx.coroutines.*
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class NewsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_news)
        
        // 错误示例:未捕获协程内部异常,直接导致APP闪退
        CoroutineScope(Dispatchers.Main).launch {
            // 模拟请求接口返回空数据,强制调用toString触发空指针
            val articleId = withContext(Dispatchers.IO) { getLatestArticleId() } // 假设返回null
            val articleContent = "文章ID:${articleId.toString()}" // 这里会抛空指针
            println("加载完成:$articleContent")
        }
    }
    
    // 模拟后端接口返回null
    private suspend fun getLatestArticleId(): String? = null
}

这个代码在测试阶段可能没触发空指针,但线上网络波动、数据异常时就会炸——系统直接弹“APP停止运行”,用户刚打开新闻页就闪退,完全不知道哪里出错,大概率直接卸载。

1.2 协程异常的特殊逻辑

很多人知道try-catch,但协程的异常和普通代码不一样:普通try-catch包协程代码,有时候抓不到异常,因为协程的异常是“挂起态”,和线程异常机制不同。比如刚才的空指针,直接包try-catch,日志里根本不会有捕获记录,APP还是崩。 所以必须用协程专属的异常处理方式,目前常用的有两种:小范围单个任务用runCatching,全局Scope用CoroutineExceptionHandler。比如修改后的正确示例:

// 技术栈:Kotlin 1.8,Android Gradle 7.4,Coroutines 1.6.4
import kotlinx.coroutines.*
import android.os.Bundle
import android.widget.Toast

class NewsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_news)
        
        // 正确示例1:用runCatching包裹单个协程任务,安全捕获异常
        CoroutineScope(Dispatchers.Main).launch {
            val result = runCatching {
                withContext(Dispatchers.IO) { getLatestArticleId() }
            }.onSuccess { articleId ->
                // 异常不发生时的逻辑,比如渲染列表
                renderArticle(articleId)
            }.onFailure { error ->
                // 异常发生时的逻辑,给用户友好提示,不崩溃
                println("加载文章失败:${error.message}")
                Toast.makeText(this@NewsActivity, "网络不佳,请稍后重试", Toast.LENGTH_SHORT).show()
            }.getOrNull()
        }
    }
    
    private suspend fun getLatestArticleId(): String? = null
    private fun renderArticle(articleId: String?) { /* 渲染列表逻辑 */ }
}

二、生产环境里协程异常处理的核心应用场景

2.1 网络请求场景

网络请求是Android异常高发区:断网、接口超时、数据格式错误,这些绝对不能让APP崩。比如做电商APP的商品列表,用户下滑加载更多时,接口超时了——要是没处理,APP直接闪退,用户刚要挑商品就崩;处理了的话,弹个“加载失败,是否重试”,给用户选择权,还能留得住用户。

2.2 本地数据操作场景

用Room做数据库时,插入、查询的异常也很坑:比如用户输入超长笔记标题,触发数据库长度限制;或者同步数据时,旧版本App存储的字段格式不对,导致查询崩溃。比如笔记APP里,异常处理后可以提示“标题过长,请缩短至20字以内”,而不是让用户的笔记直接没了,APP崩了还要重新打开。

2.3 UI交互场景

比如图片上传功能,用户选了高清图点上传,协程处理压缩、上传逻辑,要是中途网络断了——没处理的话,APP可能在后台崩溃,用户以为上传成了,重刷后发现图片没传,白等半天;处理了的话,弹个“上传失败,已保存到本地草稿”,用户下次打开还能继续,体验完全不同。

三、协程异常处理的技术优缺点

3.1 优点

协程的异常处理是结构化的,就是异常范围很明确:给哪个协程加处理器,异常就只在那里生效,不会像全局UncaughtExceptionHandler那样吞掉所有异常,排查起来根本找不到问题。比如只给加载列表的协程加异常处理,其他业务逻辑的异常还是会正常抛出,代码逻辑清晰,排查效率高。

3.2 缺点

处理不当反而会坑:比如有人图省事,给所有协程加全局异常处理器,而且只弹“出错了”,不上报具体错误,线上根本不知道哪里崩;还有人过度捕获,不管什么错误都吞掉,甚至连逻辑错误(比如1/0)都不抛,APP看起来没崩,但功能全坏了,用户用着发现“列表一直加载”,你查都查不到问题。

四、生产环境的注意事项

4.1 不盲目吞异常,分场景处理

不是所有异常都要抓:程序逻辑错误(比如写了个死循环)必须暴露,方便开发排查;用户可纠正的异常(比如断网、输入错误)要优雅处理——断网提示“检查网络”,格式不对提示正确格式,服务器错误提示“稍后再试”,不要都用“出错了”打发。

4.2 正确配置CoroutineExceptionHandler

很多人踩坑:CoroutineExceptionHandler只作用于顶层协程,子协程的异常会向上传递到父协程,给子协程加处理器没用。比如你在launch里又嵌套launch,给内层子协程加处理器,异常还是会跑到外层,所以要把处理器加在顶层Scope,或者用runCatching包裹子协程代码。示例:

// 技术栈:Kotlin 1.8,Android Gradle 7.4,Coroutines 1.6.4
// 全局协程异常处理器,用于整个Scope的异常处理
val globalExceptionHandler = CoroutineExceptionHandler { _, throwable ->
    // 上报异常到后台,方便排查(比如用腾讯Bugly)
    reportCrashToServer(throwable)
    // 给用户提示
    Toast.makeText(context, "加载失败,请重试", Toast.LENGTH_SHORT).show()
}

// 使用时,把处理器加在顶层Scope上
CoroutineScope(Dispatchers.Main + globalExceptionHandler).launch {
    // 这里的任何异常,都会被globalExceptionHandler处理,不会导致APP崩
    val result = withContext(Dispatchers.IO) { fetchData() }
}

4.3 结合日志上报,埋点排查

生产环境看不到日志等于瞎子走路,异常处理时必须上报详细信息:异常类型、堆栈、用户操作步骤、手机型号、版本号等。比如上报“用户在第3次下滑加载文章时出错,手机是小米12,版本号V2.5.0”,下次再出问题,直接定位是分页加载的逻辑错误,不用乱猜。

五、总结

协程让Android异步逻辑更简单,但异常处理是APP稳定性的“安全气囊”——不管异步逻辑跑得多顺,撞到“异常”时,安全气囊会弹出来,保护APP不崩,给用户友好提示,还能帮开发者快速排查问题。 在生产环境里,协程异常处理不是“加分项”,是“必选项”,它直接关系到APP的口碑、用户留存——用户用你APP从来没闪退,遇到问题有提示,才会愿意留下;要是一有异步操作就崩,用户要么再也不用,要么直接卸载,最后损失的还是开发者自己。