29、性能优化与KTX:使用KTX减少ANR与内存泄漏,最佳实践
性能优化这件事,说白了就是跟系统抢时间、跟内存斗智斗勇。我做了这么多年Android开发,最怕的就是用户反馈说「App卡死了」或者「闪退了」。ANR和内存泄漏,这两个问题就像悬在头顶的达摩克利斯之剑。不过,KTX库的出现,确实帮我们挡了不少子弹。
今天我们就聊聊,怎么用KTX来减少ANR和内存泄漏。嗯,这里有很多实战经验,都是我踩过坑之后总结出来的。
ANR的根源与KTX的应对策略
ANR(Application Not Responding)的本质是什么?说白了就是主线程被阻塞了,超过5秒没处理完输入事件,或者广播接收器超过10秒没返回。我遇到过最夸张的一次,是因为在onCreate里做了大量SharedPreferences的读写操作,直接导致启动ANR。
KTX在这方面提供了几个很实用的扩展:
- lifecycleScope:自动绑定生命周期,避免在后台线程做耗时操作时忘记取消
- viewModelScope:ViewModel销毁时自动取消协程,防止泄漏
- withContext(Dispatchers.IO):轻松切换线程,把耗时操作扔到后台
核心原则:主线程只做UI操作,其他一切交给协程+KTX。
举个例子,我以前写网络请求是这样的:
// 没有KTX的写法——容易ANR
fun loadData() {
Thread {
val result = api.fetchData() // 网络请求
runOnUiThread {
textView.text = result
}
}.start()
}
用了KTX之后,代码清爽多了:
// 使用KTX的lifecycleScope
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
api.fetchData() // 自动切到IO线程
}
textView.text = result // 自动回到主线程
}
你想想看,少了多少模板代码?而且lifecycleScope会在Activity销毁时自动取消协程,不会出现「请求还没回来,Activity已经没了」的尴尬情况。
内存泄漏的常见场景与KTX解法
内存泄漏,我愿称之为Android开发的「慢性病」。它不会立刻让App崩溃,但会慢慢吃掉你的内存,最终导致OOM。我见过最典型的场景就是:
- 匿名内部类持有Activity引用
- Handler发送延迟消息,Activity却已经销毁
- 协程没有取消,继续持有ViewModel引用
KTX怎么解决这些问题?我们一个个来看。
1. 协程泄漏——用viewModelScope兜底
我曾经在一个项目里,直接在ViewModel里用GlobalScope.launch启动协程。结果呢?用户退出页面后,协程还在跑,还在更新UI——直接崩溃。后来我全部改成了viewModelScope:
class MyViewModel : ViewModel() {
fun fetchData() {
// viewModelScope会在ViewModel清除时自动取消
viewModelScope.launch {
val data = repository.getData()
_uiState.value = data
}
}
}
这里有个细节:viewModelScope使用的是Dispatchers.Main作为默认调度器。如果你要做IO操作,记得用withContext切换一下。
个人习惯:我一般会在ViewModel里定义一个统一的ioScope,专门处理IO操作,这样代码更清晰。
2. LiveData泄漏——用observe的LifecycleOwner
很多人写LiveData观察者时,忘了传LifecycleOwner,或者传了错误的Owner。结果Activity销毁了,观察者还在,导致内存泄漏。KTX的observe方法强制要求传入LifecycleOwner,从设计上就堵住了这个漏洞:
// 正确的写法——KTX强制要求
viewModel.userData.observe(this) { user ->
// 当LifecycleOwner(Activity)销毁时,自动移除观察者
textView.text = user.name
}
嗯,这里要注意:如果你传的是viewLifecycleOwner(Fragment里),那生命周期绑定的是Fragment的视图,而不是Fragment本身。这个区别我曾经搞混过,导致Fragment已经detach了,观察者还在跑。
3. SharedPreferences监听器泄漏
SharedPreferences的OnSharedPreferenceChangeListener是个经典的泄漏点。你注册了监听器,但忘了在onDestroy里注销。KTX提供了edit扩展函数,但它并没有直接解决监听器泄漏的问题。我的做法是:
// 结合lifecycleScope来管理监听器
lifecycleScope.launch {
// 使用Flow来监听变化
prefs.dataFlow().collect { value ->
// 处理变化
}
}
// lifecycleScope取消时,collect也会自动取消
不过说实话,SharedPreferences本身就不适合存大量数据。我建议用DataStore替代,DataStore天然支持Flow,配合KTX用起来更顺手。
最佳实践:一个完整的性能优化示例
我们来看一个综合案例。假设你要做一个「用户列表」页面,需要从网络加载数据,然后展示在RecyclerView里。用KTX怎么写出高性能、无泄漏的代码?
class UserViewModel : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
fun loadUsers() {
viewModelScope.launch {
// 1. 切换到IO线程做网络请求
val result = withContext(Dispatchers.IO) {
api.getUsers()
}
// 2. 回到主线程更新UI
_users.value = result
}
}
}
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 3. 使用viewLifecycleOwner,确保生命周期绑定正确
viewModel.users.observe(viewLifecycleOwner) { users ->
adapter.submitList(users)
}
// 4. 使用lifecycleScope处理UI相关的异步操作
lifecycleScope.launch {
// 比如:延迟加载、动画等
delay(300)
showLoadingAnimation()
}
}
}
这个例子涵盖了三个关键点:
- viewModelScope:保证网络请求不会泄漏
- viewLifecycleOwner:保证LiveData观察者不会泄漏
- lifecycleScope:保证UI相关的协程在视图销毁时取消
避坑指南:我曾经在Fragment里用了this作为LifecycleOwner,结果Fragment被替换后,旧的观察者还在运行。后来全部改成viewLifecycleOwner,问题就解决了。记住:Fragment的this是Fragment本身的生命周期,而viewLifecycleOwner是Fragment视图的生命周期。
知识体系总览
下面这张图总结了KTX在性能优化中的核心作用:
总结一下
KTX在性能优化方面的价值,其实就三个字:省心。它帮我们处理了生命周期绑定、线程切换、自动取消这些容易出错的细节。你想想看,如果没有KTX,你得手动管理协程的取消、手动切换线程、手动注销观察者——任何一个环节漏了,就是ANR或内存泄漏。
我个人建议,新项目直接全面拥抱KTX。老项目迁移时,可以先把lifecycleScope和viewModelScope用起来,这两个是性价比最高的优化点。至于SharedPreferences的监听器泄漏,如果你不想大改,至少确保在onDestroy里调用unregisterOnSharedPreferenceChangeListener。
最后说一句:性能优化不是一蹴而就的事。KTX给了我们好工具,但真正用好它,还是得理解背后的原理。嗯,今天就聊到这里,希望这些经验对你有帮助。