聊聊Activity持有ViewModel的清理

在"單Activity"架構(gòu)的App中柏卤,頁面功能常常由Fragment承載全释,而Fragment之間的通信方式往往有兩種:通過Fragment Result API或ViewModel;通過Fragment Result API的格式如下:

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 此函數(shù)在 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"
            // 此函數(shù)在 fragment-ktx 中
            setFragmentResult("requestKey", bundleOf("bundleKey" to result))
        }
    }
}

FragmentA只有處于STARTED狀態(tài)下才會接收到結(jié)果井氢。此種方式適用于比較簡單的參數(shù)通信狰挡,對于比較大的參數(shù)(比如Bitmap)或者比較復(fù)雜的通信邏輯吴趴,ViewModel才是更好的選擇。Fragment通過ViewModel通信也比較簡單亭畜,只需要在創(chuàng)建ViewModel時扮休,指定ViewModelStore為Fragment的宿主Activity即可,這樣多個Fragment中獲取到的是同一個ViewModel拴鸵,fragment-ktx庫也提供了activityViewModels()函數(shù)可以直接獲取宿主Activity持有的ViewModel玷坠。

上面的方式非常簡單也非常方便,但也帶來了一些問題劲藐,由于ViewModel存儲在Activity的ViewModelStore中八堡,當(dāng)使用此ViewModel的Fragment全都被銷毀時,ViewModel仍然不會被釋放聘芜,事實上此ViewModel已經(jīng)泄漏了兄渺,除此以外,當(dāng)我們再次創(chuàng)建Fragment使用ViewModel時厉膀,ViewModel中仍然是上一次使用時產(chǎn)生的數(shù)據(jù)溶耘,由此可能會帶來一些難以預(yù)料的問題二拐。

為了解決上面的兩個問題,比較常見的解決方案是在使用的Fragment銷毀時凳兵,手動將ViewModel中的數(shù)據(jù)重置百新,如下:

class TestViewModel : ViewModel() {
    private val dataList: MutableList<String> = mutableListOf()
  
    fun addData(string: String) {
        dataList.add(string)
    }
  
    // 沒有使用onCleared()是因為此函數(shù)無法在外部調(diào)用
    fun tearDown() {
        dataList.clear()
    }
}

然后在Fragment的onDestory()中調(diào)用TestViewModel#tearDown()即可。

這種手動調(diào)用的方式比較繁瑣庐扫,可以借助LifecycleEventObserver以及ViewModel的onCleared()函數(shù)來自動進行“拆卸”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))創(chuàng)建ViewModel饭望,在多個Fragment中使用時,創(chuàng)建的ViewModel會跟隨第一個創(chuàng)建此ViewModel的Fragment的生命周期形庭。

此種方式雖然不用我們手動去調(diào)用清理函數(shù)铅辞,但是仍然無法解決ViewModel泄漏的問題,并且還是依賴開發(fā)者手動在onCleared()中清理或重置數(shù)據(jù)萨醒,在開發(fā)時很容易忘記斟珊。

那如何能夠讓不再使用的ViewModel能夠自動從Activity的ViewModelStore中消失呢?在上面我們通過LifecycleEventObserver實現(xiàn)了當(dāng)Fragment銷毀時自動清理ViewModel富纸,剩下需要做的就是當(dāng)Fragment銷毀時囤踩,將ViewModel從Activity的ViewModelStore中刪掉。通過查看ViewModelStore的源碼不難發(fā)現(xiàn)晓褪,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會跟隨第一個創(chuàng)建它的Fragment的生命周期好港,當(dāng)此Fragment銷毀時愉镰,ViewModel也將被清理掉,下次再進入到此Fragment時媚狰,會創(chuàng)建一個新的ViewModel岛杀。

需要注意的是,上面的代碼在removeViewModel后崭孤,會調(diào)用一次ViewModelclear()方法类嗤,而此方法是在androidx.lifecycle:lifecycle-viewmodel:2.1.0-alpha01版本之后才被添加進去,如果是之前的版本辨宠,則需要調(diào)用onCleared()方法遗锣。

為了使用方便,我們可以借助Kotlin擴展函數(shù)以及委派嗤形,實現(xiàn)一個類似activityViewModels()的函數(shù):

// 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修改了方法聲明可能會導(dǎo)致失效笔咽;對于配置更改的情況(比如旋轉(zhuǎn)屏幕)搔预,ViewModel仍然會被重建,這其實是和ViewModel的初衷相背的叶组,但如果App限制了只能使用豎屏拯田,那么此種方案也不失為一種比較好的方式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末甩十,一起剝皮案震驚了整個濱河市船庇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侣监,老刑警劉巖鸭轮,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異橄霉,居然都是意外死亡窃爷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門酪劫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吞鸭,“玉大人,你說我怎么就攤上這事覆糟。” “怎么了遮咖?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵滩字,是天一觀的道長。 經(jīng)常有香客問我御吞,道長麦箍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任陶珠,我火速辦了婚禮挟裂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揍诽。我一直安慰自己诀蓉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布暑脆。 她就那樣靜靜地躺著渠啤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪添吗。 梳的紋絲不亂的頭發(fā)上沥曹,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音,去河邊找鬼妓美。 笑死僵腺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶栋。 我是一名探鬼主播辰如,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼委刘!你這毒婦竟也來了丧没?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤锡移,失蹤者是張志新(化名)和其女友劉穎呕童,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淆珊,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡夺饲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了施符。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片往声。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖戳吝,靈堂內(nèi)的尸體忽然破棺而出浩销,到底是詐尸還是另有隱情,我是刑警寧澤听哭,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布慢洋,位于F島的核電站,受9級特大地震影響陆盘,放射性物質(zhì)發(fā)生泄漏普筹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一隘马、第九天 我趴在偏房一處隱蔽的房頂上張望太防。 院中可真熱鬧,春花似錦酸员、人聲如沸蜒车。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽醇王。三九已至,卻和暖如春崭添,著一層夾襖步出監(jiān)牢的瞬間寓娩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棘伴,地道東北人寞埠。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像焊夸,于是被迫代替她去往敵國和親仁连。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345