聊聊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。

需要注意的是,上面的代码在removeViewModel后,会调用一次ViewModelclear()方法,而此方法是在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限制了只能使用竖屏,那么此种方案也不失为一种比较好的方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,387评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,845评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,091评论 0 246
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,308评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,662评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,795评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,008评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,466评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,687评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,181评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,531评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,177评论 3 239
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,126评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,862评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,734评论 2 274