聊聊 Activity 持有 ViewModel 的清理
在"单 Activity"架构的 App 中,页面功能常常由 Fragment 承载,而 Fragment 之间的通信方式往往有两种:通过 Fragment Result API 或 ViewModel;通过 Fragment Result API 的格式如下:
class FragmentA : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 此函数在 fragment-ktx 中
setFragmentResultListener("requestKey") { requestKey, bundle ->
val result = bundle.getString("bundleKey")
}
}
}
class FragmentB : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
val result = "result"
// 此函数在 fragment-ktx 中
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
}
FragmentA
只有处于STARTED
状态下才会接收到结果。此种方式适用于比较简单的参数通信,对于比较大的参数(比如Bitmap
)或者比较复杂的通信逻辑,ViewModel 才是更好的选择。Fragment 通过 ViewModel 通信也比较简单,只需要在创建 ViewModel 时,指定ViewModelStore
为 Fragment 的宿主 Activity 即可,这样多个 Fragment 中获取到的是同一个 ViewModel,fragment-ktx 库也提供了activityViewModels()
函数可以直接获取宿主 Activity 持有的 ViewModel。
上面的方式非常简单也非常方便,但也带来了一些问题,由于 ViewModel 存储在 Activity 的 ViewModelStore 中,当使用此 ViewModel 的 Fragment 全都被销毁时,ViewModel 仍然不会被释放,事实上此 ViewModel 已经泄漏了,除此以外,当我们再次创建 Fragment 使用 ViewModel 时,ViewModel 中仍然是上一次使用时产生的数据,由此可能会带来一些难以预料的问题。
为了解决上面的两个问题,比较常见的解决方案是在使用的 Fragment 销毁时,手动将 ViewModel 中的数据重置,如下:
class TestViewModel : ViewModel() {
private val dataList: MutableList<String> = mutableListOf()
fun addData(string: String) {
dataList.add(string)
}
// 没有使用 onCleared() 是因为此函数无法在外部调用
fun tearDown() {
dataList.clear()
}
}
然后在 Fragment 的onDestory()
中调用TestViewModel#tearDown()
即可。
这种手动调用的方式比较繁琐,可以借助LifecycleEventObserver
以及 ViewModel 的onCleared()
函数来自动进行“拆卸”ViewModel:
open class AutoTearDownViewModel(
private val lifecycleOwner: LifecycleOwner
) : ViewModel(), LifecycleEventObserver {
init {
if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
onCleared()
} else {
lifecycleOwner.lifecycle.addObserver(this)
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
onCleared()
lifecycleOwner.lifecycle.removeObserver(this)
}
}
}
class TestViewModel(val lifecycleOwner: LifecycleOwner) : AutoTearDownViewModel(lifecycleOwner) {
private val dataList: MutableList<String> = mutableListOf()
override fun onCleared() {
dataList.clear()
}
}
class AutoTearDownViewModelFactory(
private val lifecycleOwner: LifecycleOwner
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(LifecycleOwner::class.java).newInstance(lifecycleOwner)
}
}
在 Fragment 中可以通过使用ViewModelProvider(requireActivity(), AutoTearDownViewModelFactory(this))
创建 ViewModel,在多个 Fragment 中使用时,创建的 ViewModel 会跟随**第一个创建此 ViewModel **的 Fragment 的生命周期。
此种方式虽然不用我们手动去调用清理函数,但是仍然无法解决 ViewModel 泄漏的问题,并且还是依赖开发者手动在onCleared()
中清理或重置数据,在开发时很容易忘记。
那如何能够让不再使用的 ViewModel 能够自动从 Activity 的 ViewModelStore 中消失呢?在上面我们通过LifecycleEventObserver
实现了当 Fragment 销毁时自动清理 ViewModel,剩下需要做的就是当 Fragment 销毁时,将 ViewModel 从 Activity 的 ViewModelStore 中删掉。通过查看ViewModelStore
的源码不难发现,ViewModel 是存储在HashMap<String, ViewModel>
中,此字段是private
的,所以我们需要通过反射拿到这个 Map,从而将 ViewModel 删除,所以我们将AutoTearDownViewModel
进行以下改造:
// AutoTearDownViewModel.kt
const val VIEW_MODEL_KEY = "auto_tear_down_view_model"
fun <T> getVmKey(clazz: Class<T>): String {
return VIEW_MODEL_KEY + ":" + clazz.canonicalName
}
open class AutoTearDownViewModel(
private val fragment: Fragment
) : ViewModel(), LifecycleEventObserver {
init {
if (fragment.lifecycle.currentState == Lifecycle.State.DESTROYED) {
onCleared()
} else {
fragment.lifecycle.addObserver(this)
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
val mapField = ViewModelStore::class.java.getDeclaredField("mMap")
mapField.isAccessible = true
val viewModelMap = mapField.get(fragment.requireActivity().viewModelStore) as HashMap<String, ViewModel>
viewModelMap.remove(getVmKey(this::class.java))?.let {
val method = ViewModel::class.java.getDeclaredMethod("clear")
method.isAccessible = true
method.invoke(it)
}
fragment.lifecycle.removeObserver(this)
}
}
}
class AutoTearDownViewModelFactory(
private val fragment: Fragment,
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Fragment::class.java).newInstance(fragment) as T
}
}
通过上面的改造后,ViewModel 会跟随**第一个创建它的 Fragment **的生命周期,当此 Fragment 销毁时,ViewModel 也将被清理掉,下次再进入到此 Fragment 时,会创建一个新的 ViewModel。
需要注意的是,上面的代码在remove
了ViewModel
后,会调用一次ViewModel
的clear()
方法,而此方法是在androidx.lifecycle:lifecycle-viewmodel:2.1.0-alpha01
版本之后才被添加进去,如果是之前的版本,则需要调用onCleared()
方法。
为了使用方便,我们可以借助 Kotlin 扩展函数以及委派,实现一个类似activityViewModels()
的函数:
// AutoTearDownViewModelProvider.kt
inline fun <reified VM : ViewModel> Fragment.autoTearDownViewModel(): Lazy<VM> {
return AutoTearDownViewModelLazy(VM::class, this)
}
class AutoTearDownViewModelLazy<VM : ViewModel>(
private val viewModelClass: KClass<VM>,
private val fragment: Fragment
) : Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
return cached
?: ViewModelProvider(
fragment.requireActivity(),
AutoTearDownViewModelFactory(fragment)
).get(getVmKey(viewModelClass.java), viewModelClass.java).also {
cached = it
}
}
override fun isInitialized() = cached != null
}
上述提到的解决方案也并非完美,由于用到了反射,并且反射的字段和方法非public
,所以在之后的版本中如果 ViewModel 或 ViewModelStore 修改了方法声明可能会导致失效;对于配置更改的情况(比如旋转屏幕),ViewModel 仍然会被重建,这其实是和 ViewModel 的初衷相背的,但如果 App 限制了只能使用竖屏,那么此种方案也不失为一种比较好的方式。