引言
在Android开发中,ANR(Application Not Responding)是用户体验的致命杀手。当主线程被耗时操作阻塞超过阈值(5秒前台/10秒后台),系统会直接弹窗提示应用无响应。本文将深入剖析如何通过Kotlin协程将耗时操作移出主线程,并结合完整代码示例,覆盖网络请求、数据库操作、文件读写等高频场景,助你彻底解决ANR问题。
一、ANR的核心原因与协程优势
1.1 ANR触发场景
场景类型 | 具体操作示例 | 风险等级 |
---|---|---|
网络请求 | HttpURLConnection 同步调用 | ⚠️ 高危 |
数据库操作 | 大量Room查询未异步执行 | ⚠️ 高危 |
文件读写 | 大文件拷贝/解析 | ⚠️ 高危 |
复杂计算 | 大数据排序/图像渲染 | ⚠️ 中危 |
1.2 协程的核心优势
- 轻量级线程:协程切换成本远低于线程
- 结构化并发:自动生命周期管理(如
lifecycleScope
) - 非阻塞挂起:用同步代码风格写异步逻辑
二、完整代码示例与场景解析
2.1 基础配置:添加依赖与协程作用域
build.gradle:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" // lifecycleScope
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" // viewModelScope
}
2.2 场景1:网络请求优化
错误实现(直接阻塞主线程):
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ❌ 直接在主线程执行网络请求
val data = fetchDataBlocking() // 阻塞主线程!
updateUI(data)
}
private fun fetchDataBlocking(): String {
val url = URL("https://api.example.com/data")
val connection = url.openConnection() as HttpURLConnection
return connection.inputStream.bufferedReader().use { it.readText() }
}
}
协程优化实现:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ✅ 使用lifecycleScope启动协程
lifecycleScope.launch {
try {
val data = fetchDataSuspend()
updateUI(data)
} catch (e: Exception) {
showError(e)
}
}
}
// ⭐️ 挂起函数封装网络请求
private suspend fun fetchDataSuspend(): String = withContext(Dispatchers.IO) {
val url = URL("https://api.example.com/data")
val connection = url.openConnection() as HttpURLConnection
try {
connection.inputStream.bufferedReader().use { it.readText() }
} finally {
connection.disconnect()
}
}
}
2.3 场景2:数据库批量操作优化
错误实现(主线程执行批量插入):
class UserRepository(private val userDao: UserDao) {
// ❌ 在主线程插入1000条数据
fun saveUsers(users: List<User>) {
users.forEach { userDao.insert(it) } // 阻塞主线程!
}
}
协程优化实现:
class UserRepository(private val userDao: UserDao) {
// ✅ 使用协程+事务批量处理
suspend fun saveUsers(users: List<User>) = withContext(Dispatchers.IO) {
userDao.transaction {
users.forEach { userDao.insert(it) }
}
}
}
// 在ViewModel中调用
class UserViewModel : ViewModel() {
fun saveData(users: List<User>) {
viewModelScope.launch {
userRepository.saveUsers(users)
// 可选:通知UI完成
}
}
}
2.4 场景3:大文件读写优化
错误实现(主线程解析10MB JSON文件):
fun parseLargeJsonFile(context: Context) {
val json = context.assets.open("large_data.json")
.bufferedReader().use { it.readText() } // ❌ 主线程读取大文件
val data = Gson().fromJson(json, DataModel::class.java)
// ...
}
协程优化实现:
suspend fun parseLargeJsonFile(context: Context): DataModel =
withContext(Dispatchers.IO) {
val json = context.assets.open("large_data.json")
.bufferedReader().use { it.readText() }
return@withContext withContext(Dispatchers.Default) { // 切换至计算线程解析
Gson().fromJson(json, DataModel::class.java)
}
}
// 调用示例
lifecycleScope.launch {
val data = parseLargeJsonFile(requireContext())
updateUI(data)
}
2.5 场景4:复杂计算任务优化
错误实现(主线程排序10万条数据):
fun sortData(list: List<Int>): List<Int> {
return list.sorted() // ❌ 大数据排序阻塞主线程
}
协程优化实现:
suspend fun sortLargeList(list: List<Int>): List<Int> =
withContext(Dispatchers.Default) { // 使用默认计算线程池
list.sorted()
}
// 在ViewModel中并行处理多个计算
fun processMultipleLists(list1: List<Int>, list2: List<Int>) {
viewModelScope.launch {
val deferred1 = async { sortLargeList(list1) }
val deferred2 = async { sortLargeList(list2) }
val result1 = deferred1.await()
val result2 = deferred2.await()
mergeResults(result1, result2)
}
}
三、高级优化技巧
3.1 结构化并发的最佳实践
class MyViewModel : ViewModel() {
// ✅ 在ViewModel中使用viewModelScope
fun fetchDataAndSave() {
viewModelScope.launch {
// 并行执行网络请求和本地查询
val remoteData = async { fetchRemoteData() }
val localData = async { loadLocalData() }
// 合并结果并处理
val combined = combineData(remoteData.await(), localData.await())
saveCombinedData(combined)
}
}
}
3.2 异常处理方案对比
方式 | 适用场景 | 代码示例 |
---|---|---|
try-catch块 | 单个协程内的局部异常处理 | try { ... } catch (e: IOException) { ... } |
CoroutineExceptionHandler | 全局或作用域级别的异常捕获 | val handler = CoroutineExceptionHandler { _, e -> logError(e) } |
SupervisorJob | 子协程失败不影响父协程 | val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) |
推荐方案:
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Toast.makeText(context, "Error: ${throwable.message}", Toast.LENGTH_SHORT).show()
}
lifecycleScope.launch(exceptionHandler) {
val data = withContext(Dispatchers.IO) { riskyOperation() }
updateUI(data)
}
四、性能监控与调试
4.1 使用StrictMode检测主线程违规
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测磁盘读
.detectDiskWrites() // 检测磁盘写
.detectNetwork() // 检测网络操作
.penaltyLog() // 违规时打印日志
.build()
)
}
}
4.2 Android Profiler关键指标
- CPU Profiler:检查主线程是否存在长时间占用
- Memory Profiler:排查内存泄漏导致GC冻结主线程
- Network Profiler:监控网络请求耗时
五、完整示例项目结构
app/
├── src/
│ ├── main/
│ │ ├── java/com/example/
│ │ │ ├── data/
│ │ │ │ ├── UserRepository.kt # 数据仓库(协程封装)
│ │ │ │ └── ApiService.kt # Retrofit接口定义
│ │ │ ├── ui/
│ │ │ │ ├── MainActivity.kt # 使用lifecycleScope
│ │ │ │ └── UserViewModel.kt # 使用viewModelScope
│ │ │ └── utils/
│ │ │ └── FileUtils.kt # 文件操作协程扩展
六、总结与最佳实践
- 黄金法则:主线程只做UI更新和轻量操作
- 调度器选择:
Dispatchers.Main
:UI更新、LiveData观察Dispatchers.IO
:文件、网络、数据库Dispatchers.Default
:复杂计算
- 生命周期管理:
- Activity/Fragment使用
lifecycleScope
- ViewModel使用
viewModelScope
- Activity/Fragment使用
- 异常处理:结合try-catch与CoroutineExceptionHandler
- 性能监控:定期使用Profiler工具分析主线程负载
通过合理应用协程,不仅能有效避免ANR,还能让异步代码保持简洁优雅。立即重构您的阻塞代码,为用户提供丝滑流畅的体验!