Android Architecture Component 解析之 ViewModel

概覽

在考慮到UI 元素生命周期變化的環(huán)境下何之,架構(gòu)組件中的 ViewModel 主要用于存儲(chǔ)以及管理UI相關(guān)的數(shù)據(jù)。ViewModel 可以在配置信息變化(configuration change) 時(shí) (例如屏幕的旋轉(zhuǎn)) 保持?jǐn)?shù)據(jù)狀態(tài)咽筋。

眾所周知的是溶推,Android系統(tǒng)框架管理著UI控制器(Activity / Fragment)的生命周期。出于對(duì)用戶行為以及設(shè)備事件的響應(yīng)奸攻,系統(tǒng)可能會(huì)銷毀或重建UI控制器蒜危,而這些行為是完全不受控制的。如果系統(tǒng)銷毀 或是 重建UI控制器時(shí)睹耐,任何UI相關(guān)的暫存數(shù)據(jù)都將會(huì)被清除辐赞。

另一個(gè)問題是,UI控制器通常需要頻繁地調(diào)用異步接口硝训,耗費(fèi)一定的時(shí)間獲取數(shù)據(jù)响委。除了維護(hù)相關(guān)的接口外新思,在UI控制器被重新創(chuàng)建時(shí)往往會(huì)發(fā)起重復(fù)的請(qǐng)求,這無(wú)疑會(huì)造成資源的浪費(fèi)赘风。

UI 控制器如Activity / Fragment主要用于展示UI數(shù)據(jù)夹囚,響應(yīng)用戶請(qǐng)求,以及處理系統(tǒng)的事件邀窃,如授權(quán)訪問請(qǐng)求崔兴。在UI 控制器中加入請(qǐng)求加載數(shù)據(jù)庫(kù)或是網(wǎng)絡(luò)的數(shù)據(jù)的邏輯將使得類不斷膨脹。將大量無(wú)關(guān)的職責(zé)分配給UI 控制器蛔翅,而不是將各項(xiàng)職責(zé)分配出去則會(huì)導(dǎo)致出現(xiàn) God Object,同時(shí)會(huì)降低整體項(xiàng)目的可測(cè)試性位谋。

ViewModel 的生命周期 ( outlive Fragment or Activity ?)

且看官方給出的ViewModel生命周期圖示:


viewmodel-lifecycle

對(duì)照Demo中的實(shí)際效果:

vm_test

利用ViewModel 在旋轉(zhuǎn)屏幕后山析,可以看到ViewModel的instance 還是同一個(gè),而且其中的score 數(shù)據(jù)依然保存了下來(lái)掏父。(觀察實(shí)際的log也可以印證這一點(diǎn))

如果直接按back 鍵退出笋轨,可以看到幾乎在ActivityonDestroy() 方法調(diào)用的同時(shí),ViewModel#onCleared()方法被調(diào)用赊淑,此時(shí)ViewModel也隨即銷毀了爵政。

顯而易見的是,除了數(shù)據(jù)已經(jīng)保存外陶缺,還有一個(gè)好處在于省去了需要手動(dòng)關(guān)聯(lián)/解除ViewModel與界面生命周期的調(diào)用钾挟。即不必在先前自實(shí)現(xiàn)的ViewModel|Presenter中添加類似OnDestroy()方法,并發(fā)起顯示的調(diào)用了饱岸。

實(shí)現(xiàn)

以上的ViewModel中實(shí)現(xiàn)了‘自知’的生命周期掺出,這究竟是如何做到的呢?讓我們退一步回到在使用Fragment時(shí)苫费,看如何在轉(zhuǎn)屏條件下保存內(nèi)存中的臨時(shí)數(shù)據(jù)汤锨。

從Fragment的角度來(lái)看狀態(tài)保存

在轉(zhuǎn)屏?xí)r,按默認(rèn)的情況百框,FragmentManager會(huì)負(fù)責(zé)銷毀相關(guān)聯(lián)的Fragment, Fragment的生命周期 onPause, onStop, onDestroy會(huì)被相繼調(diào)用闲礼。然而Fragment有一個(gè)特性可以設(shè)置Fragment#setRetainIntance(true)保存當(dāng)前的自身實(shí)例(instance)。當(dāng)該選項(xiàng)設(shè)置后铐维,F(xiàn)ragment 當(dāng)前實(shí)例被保存下來(lái)了柬泽,然后隨即被傳給新的Activity。留存的Fragment實(shí)際利用了Fragment關(guān)聯(lián)的View可以被銷毀以及重新創(chuàng)建方椎,而不需要銷毀Fragment自身聂抢。

在發(fā)生configuration 改變的時(shí)候,Fragment 內(nèi)部的View與Activity 包含的View 一樣棠众,都會(huì)因?yàn)檫@一原因被銷毀以及重新創(chuàng)建琳疏。即當(dāng)有一個(gè)新的configuration變化時(shí)有决,很有可能需要新的資源,為了使用更好適配的資源空盼,View將會(huì)重新創(chuàng)建书幕。

隨后FragmentManager會(huì)檢查每一個(gè)Fragment的retainInstance屬性,如果值為默認(rèn)的false揽趾,FragmentManager會(huì)將Fragment實(shí)例銷毀台汇,F(xiàn)ragment和它關(guān)聯(lián)的View都將會(huì)在新的Activity中重新創(chuàng)建;而如果retainInstance值為true篱瞎,當(dāng)前Fragment關(guān)聯(lián)的View將會(huì)銷毀而保留下Fragment的實(shí)例苟呐,在新的Activity創(chuàng)建時(shí),新的FragmentManager會(huì)找到留存的Fragment俐筋,并重建它的View牵素。

留存的Fragment沒有被銷毀,而只是從快要銷毀的Activity上解綁了澄者。表示為retain狀態(tài)的Fragment仍然存在笆呆,只是不被任何Activity持有。整個(gè)轉(zhuǎn)屏過(guò)程粱挡,對(duì)應(yīng)生命周期的變化可見下圖:

代碼尋蹤

按照上述Fragment實(shí)現(xiàn)的邏輯赠幕,架構(gòu)組件中的實(shí)現(xiàn)又是怎樣的呢?讓我們從源碼中尋找線索, 以Activity中的ViewModel創(chuàng)建為例询筏。

ViewModelProviders.of(activity).get(YourViewModel.class)

ViewModelProviders.of(activity)
    -> new ViewModelProvider(ViewModelStores.of(activity), factory) // # factory 為默認(rèn)的 AndroidViewModelFactory
            ViewModelStores.of(activity)
                -> ViewModelStore
                HolderFragment#holderFragmentFor(activity).getViewModelStore()
                    HolderFragmentManager#holderFragmentFor(activity)
                        FragmentManager fm = activity.getSupportFragmentManager();
                        HolderFragment holder = fm.findFragmentByTag(HOLDER_TAG)
                        if (holder != null) {
                            return holder;
                        }

                        holder = new HolderFragment();
                            public HolderFragment() {
                                setRetainInstance(true); // retained!
                            }
                            public void onDestroy() 
                                super.onDestroy();
                                mViewModelStore.clear();
                                    for (ViewModel vm : mMap.values()) {
                                        vm.onCleared(); // notifies ViewModels that they are no longer used.
                                    }

                        fm.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
                holder.getViewModelStore()

viewModelProvider.get(modelClass) //YourViewModel.class
    get(key, modelClass)
        ViewModel viewModel = mViewModelStore.get(key); // 檢查是否已經(jīng)有緩存
        if (modelClass.isInstance(viewModel)) {
            return (T) viewModel
        }

        viewModel = mFactory.create(modelClass);
            AndroidViewModelFactory#create(modelClass)
                if(AndroidViewModel.class.isAssignableFrom(modelClass)) // 檢查是否為AndroidViewModel類別
                    return modelClass.getConstructor(Application.class).newInstance(mApplication);
                super.create(modelClass)
                    NewInstanceFactory#create(modelClass)
                        return modelClass.newInstance(); // 調(diào)用默認(rèn)構(gòu)造方法構(gòu)造實(shí)例

        mViewModelStore.put(key, viewModel); // 添加ViewModel到緩存中
        return (T) viewModel;

可以發(fā)現(xiàn)存儲(chǔ)的ViewModel實(shí)際也是利用Fragment 設(shè)置為retained的特性實(shí)現(xiàn)的榕堰。它的生命周期與內(nèi)置的不可見的HolderFragment是緊密聯(lián)系的。在Fragment最終被銷毀時(shí)屈留,ViewModelonCleared用于執(zhí)行清理動(dòng)作的回調(diào)方法同時(shí)被觸發(fā)局冰。

與 onSaveInstanceState() 的異同

ViewModel保持的實(shí)際是暫存的UI中的數(shù)據(jù),但是它們并沒有被持久化(persisted)灌危。一旦相關(guān)的UI控制器被銷毀或是App進(jìn)程被暫停(由于系統(tǒng)資源的限制)康二,ViewModel和所包含的數(shù)據(jù)將被GC處理。

onSaveInstanceState() 該方法在以下2種情況下用來(lái)保留少量的UI相關(guān)數(shù)據(jù)勇蝙。

  • App在后臺(tái)由于資源限制被暫停(stopped)
  • 配置變化 (configuration change)

系統(tǒng)在UI層面已經(jīng)利用onSaveInstanceState() 去保存View的狀態(tài)信息(如 EditText中已輸入的文本, ListView中的滑動(dòng)位置等)沫勿。由于該方法在主線程中執(zhí)行,并且序列化本身會(huì)有一定的內(nèi)存開銷味混,即要求此方法能較快執(zhí)行产雹,以免出現(xiàn)UI掉幀或是更嚴(yán)重的性能的問題。所以它的設(shè)計(jì)并不適合大數(shù)據(jù)的存儲(chǔ)翁锡。

Fragment#setRetainInstance(true) 方法利用retained Fragment 實(shí)例蔓挖,可以保留較大規(guī)模的數(shù)據(jù) 如 bitmap 或是像網(wǎng)絡(luò)連接(network connections)這樣復(fù)雜的對(duì)象。

值得注意的是馆衔,ViewModel 只在由配置信息變化改變(configuration change)引起的銷毀過(guò)程中留存瘟判,當(dāng)進(jìn)程被暫停時(shí)怨绣,它也被銷毀了。

ViewModel 以及 Retained Fragment 測(cè)試源碼

Github: ArchComponentPlayground

其它ViewMode實(shí)現(xiàn)

https://github.com/inloop/AndroidViewModel

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拷获,一起剝皮案震驚了整個(gè)濱河市篮撑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匆瓜,老刑警劉巖赢笨,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異驮吱,居然都是意外死亡茧妒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門左冬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嘶伟,“玉大人,你說(shuō)我怎么就攤上這事又碌。” “怎么了绊袋?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵毕匀,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我癌别,道長(zhǎng)皂岔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任展姐,我火速辦了婚禮躁垛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘圾笨。我一直安慰自己教馆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布擂达。 她就那樣靜靜地躺著土铺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪板鬓。 梳的紋絲不亂的頭發(fā)上悲敷,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音俭令,去河邊找鬼后德。 笑死,一個(gè)胖子當(dāng)著我的面吹牛抄腔,可吹牛的內(nèi)容都是我干的瓢湃。 我是一名探鬼主播理张,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼箱季!你這毒婦竟也來(lái)了涯穷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤藏雏,失蹤者是張志新(化名)和其女友劉穎拷况,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掘殴,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赚瘦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奏寨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片起意。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖病瞳,靈堂內(nèi)的尸體忽然破棺而出揽咕,到底是詐尸還是另有隱情,我是刑警寧澤套菜,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布亲善,位于F島的核電站,受9級(jí)特大地震影響逗柴,放射性物質(zhì)發(fā)生泄漏蛹头。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一戏溺、第九天 我趴在偏房一處隱蔽的房頂上張望渣蜗。 院中可真熱鬧,春花似錦旷祸、人聲如沸耕拷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斑胜。三九已至,卻和暖如春嫌吠,著一層夾襖步出監(jiān)牢的瞬間止潘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工辫诅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凭戴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓炕矮,卻偏偏與公主長(zhǎng)得像么夫,于是被迫代替她去往敵國(guó)和親者冤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容