Jetpack ViewModel 抽絲剝繭

前言

Jetpack AAC 系列文章:

Jetpack Lifecycle 該怎么看卖氨?還肝否?
Jetpack LiveData 是時候了解一下了
Jetpack ViewModel 抽絲剝繭

前兩篇分析了Lifecycle和LiveData镊绪,本篇將著重分析ViewModel及其三者的關聯(lián)。
通過本篇,你將了解到:

1仅颇、為什么需要ViewModel ?
2碘举、ViewModel 的使用方式
3忘瓦、ViewModel 原理掘地三尺
4、Lifecycle/LiveData/ViewModel 關聯(lián)

1引颈、為什么需要ViewModel 耕皮?

配置項更改到數(shù)據(jù)恢復

Android 配置項更改常用的即是橫豎屏切換,當橫豎屏切換的時候蝙场,Activity 會重建凌停,重新走onCreate(xx)...onResume(),此時Activity 已經(jīng)是全新的實例了李丰,因此之前的Activity 關聯(lián)的ViewTree 將會重建苦锨。
除了橫豎屏切換,其它的配置項更改也會重建Activity趴泌。


image.png

問題來了:之前ViewTree 綁定的數(shù)據(jù)如何恢復呢舟舒?

傳統(tǒng)的數(shù)據(jù)恢復方法

Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解 已經(jīng)分析過,此處再簡單提一下:

1嗜憔、在onSaveInstanceState 里保存ViewTree 相關的數(shù)據(jù)秃励。
2、在onRestoreInstanceState 里恢復ViewTree 相關的數(shù)據(jù)吉捶。

通過這兩個方法夺鲜,在配置項更改的時候可以將數(shù)據(jù)恢復。不過這種方式也有缺陷:

1呐舔、onSaveInstanceState 用Bundle存儲數(shù)據(jù)便于跨進程傳遞币励,因此其存儲上限受限于Binder(1M),不能用于恢復較大的數(shù)據(jù)珊拼,比如Bitmap食呻。
2、復雜的類需要實現(xiàn)Parcelable/Serializable 接口澎现。
3仅胞、onSaveInstanceState 在onStop 之后調用,調用比較頻繁剑辫。

ViewModel 能夠實現(xiàn)數(shù)據(jù)恢復功能干旧,也規(guī)避了以上問題。

UI 數(shù)據(jù)的統(tǒng)一管理

以前管理UI 數(shù)據(jù)的時候妹蔽,我們一般會定義一個Model椎眯,再定義一個Manager對其進行統(tǒng)一管理挠将,借助于ViewMode+LiveData,能夠更優(yōu)雅地管理數(shù)據(jù)盅视。

綜上捐名,ViewModel 能夠進行數(shù)據(jù)恢復以及UI 數(shù)據(jù)統(tǒng)一管理。

2闹击、ViewModel 的使用方式

橫豎屏切換數(shù)據(jù)恢復

說得ViewModel 很優(yōu)秀的樣子镶蹋,有代碼有真相,以橫豎屏切換為例赏半,看看如何使用它贺归。
定義ViewModel:

public class MoneyViewModel extends ViewModel {
    private int money;
    private String name = "官方ViewModel";

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

MoneyViewModel 繼承自ViewModel。
作為比對断箫,再定義一個純粹的類:

public class MyViewModel {
    private int money;
    private String name = "我的ViewModel";

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

兩者唯一的差別就是是否繼承自ViewModel拂酣。

xml里定義兩個TextView以及一個Button。


image.png

當點擊修改文本的時候仲义,將上面兩個TextView 修改婶熬,Activity里代碼如下:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_model);

        //先new 出Provider,再獲取ViewModel
        moneyViewModel = new ViewModelProvider(this).get(MoneyViewModel.class);
        myViewModel = new MyViewModel();

        TextView tvVM = findViewById(R.id.tv_vm);
        tvVM.setText(moneyViewModel.getName());

        TextView tvMyVM = findViewById(R.id.tv_my_vm);
        tvMyVM.setText(myViewModel.getName());

        Button btnChange = findViewById(R.id.btn_change);
        btnChange.setOnClickListener((v) -> {
            moneyViewModel.setName("官方 ViewModel 改變");
            tvMyVM.setText(moneyViewModel.getName());

            myViewModel.setName("我的 ViewModel 改變");
            tvVM.setText(myViewModel.getName());
        });
    }

點擊Button后埃撵,修改我的ViewModel和官方ViewModel赵颅,此時UI 刷新。隨后暂刘,將屏幕旋轉到橫屏饺谬,再查看UI 展示。


tt0.top-380809.gif

可以看出谣拣,豎屏切換到橫屏后募寨,官方ViewModel改變了,我的ViewModel沒有改變森缠。
顯而易見拔鹰,ViewModel 能夠在橫豎屏切換后恢復數(shù)據(jù)。

3贵涵、ViewModel 原理掘地三尺

ViewModelStore 的獲取

moneyViewModel列肢、myViewModel 同樣作為ViewModelActivity 的成員變量,ViewModelActivity 都重建了(重新New ViewModelActivity 實例)独悴,理論上來說成員變量也是重建了的,為啥moneyViewModel 可以保持數(shù)據(jù)呢锣尉?這也是我們要探究的ViewModel 原理刻炒。
從ViewModel 的創(chuàng)建開始分析:

moneyViewModel = new ViewModelProvider(this).get(MoneyViewModel.class);

ViewModelProvider 顧名思義:ViewModel 的提供者。
構造函數(shù)的形參為:ViewModelStoreOwner自沧,該接口的唯一方法:

ViewModelStore getViewModelStore();

我們看到上面?zhèn)魅肓藅his坟奥,也即是說咱們的ViewModelActivity實現(xiàn)了該接口树瞭。

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }

mFactory、mViewModelStore 為ViewModelProvider 成員變量爱谁。
mFactory 為創(chuàng)建ViewModel的工廠方法晒喷,mViewModelStore 為ViewModel的存儲器,它是通過 ViewModelStoreOwner.getViewModelStore()獲取的访敌。

ViewModelActivity 繼承自AppCompatActivity凉敲,進而繼承自ComponentActivity,而ComponentActivity 實現(xiàn)了ViewModelStoreOwner 接口寺旺,實現(xiàn)方法如下:

#ComponentActivity.java
    public ViewModelStore getViewModelStore() {
        ...
        ensureViewModelStore();
        return mViewModelStore;
    }

    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            //為空爷抓,從mLastNonConfigurationInstances 尋找
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // 直接恢復
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                //沒找到,則創(chuàng)建ViewModelStore
                mViewModelStore = new ViewModelStore();
            }
        }
    }

可以看出阻塑,ViewModelProvider. mViewModelStore 來源于ComponentActivity.mViewModelStore蓝撇,而ComponentActivity.mViewModelStore 的賦值有兩個地方:

1、從Activity.mLastNonConfigurationInstances里獲取陈莽。
2渤昌、全新創(chuàng)建,直接new ViewModelStore走搁。

image.png

ViewModel 的獲取

ViewModelProvider.get(MoneyViewModel.class)
#ViewModelProvider.java

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        //先從ViewModelStore里獲取
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof ViewModelProvider.OnRequeryFactory) {
                ((ViewModelProvider.OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            //viewModel 不為空独柑,說明找到了ViewModel
            return (T) viewModel;
        } else {
            ...
        }
        if (mFactory instanceof ViewModelProvider.KeyedFactory) {
            //根據(jù)工廠創(chuàng)建ViewModel
            viewModel = ((ViewModelProvider.KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        //將ViewModel 存儲到ViewModelStore 里
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }

與ViewModelStore 來源類似,ViewModel 來源于兩個地方:

1朱盐、從ViewModelStore 里獲取群嗤。
2、通過反射構造新的實例兵琳。

新生成的ViewModel 將會存儲到ViewModeStore里狂秘。
而ViewModeStore 就只有一個成員變量:

private final HashMap<String, ViewModel> mMap = new HashMap<>();

此處的Map 的key 即為自定義ViewModel的全限定類名+前綴。


image.png

Activity 重建對ViewModel 的影響

由上面分析可知躯肌,ViewModelStore 被Activity 持有者春,而ViewModel 被ViewModelStore 持有。
屏幕從豎屏切換到橫屏時清女,Activity 重建了钱烟,拿到的ViewModel 卻沒有變化,我們有理由相信ViewModelStore 沒有變嫡丙,而縱觀ViewModelStore 賦值拴袭,此時的ViewModelStore 很有可能從NonConfigurationInstances里獲取的。
接著分析 NonConfigurationInstances的來龍去脈曙博。

需要注意的是拥刻,NonConfigurationInstances 在Activity.java和ComponentActivity.java 里都有定義。

#Activity.java
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }

重點查看mLastNonConfigurationInstances 的賦值父泳,它的調用棧如下圖:


image.png

performLaunchActivity() 在新建Activity和重建Activity 時都會調用般哼,只是在新建Activity 調用時吴汪,最后的ActivityClientRecord.lastNonConfigurationInstances =null。

重點又流轉到ActivityClientRecord蒸眠,它是怎么確定的漾橙。

#ActivityThread.java
    public void handleRelaunchActivity(ActivityClientRecord tmp,
                                       PendingTransactionActions pendingActions) {
        ...
        //從mActivities 里獲取
        //final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
        ActivityClientRecord r = mActivities.get(tmp.token);
        ...
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
    }

現(xiàn)在看來有點眉目了,在重建Activity 時:

1楞卡、從mActivities(緩存)里尋找ActivityClientRecord霜运。
2、通過ActivityClientRecord 找到lastNonConfigurationInstances臀晃。

接下來看看ActivityClientRecord.lastNonConfigurationInstances 在哪賦值的觉渴。
我們知道Activity 重建時的步驟:

1、先將原來的Activity 銷毀徽惋。
2案淋、再重新新建一個Activity實例。

而Activity 銷毀時會調用

#ActivityThread.java
    ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                                int configChanges, boolean getNonConfigInstance, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        ...
        if (r != null) {
            activityClass = r.activity.getClass();
            ...
            if (getNonConfigInstance) {
                try {
                    //從Activity 里獲取
                    r.lastNonConfigurationInstances
                            = r.activity.retainNonConfigurationInstances();
                } catch (Exception e) {
                    ...
                }
            }
            ...
        }
        ...
        synchronized (mResourcesManager) {
            //移除ActivityClientRecord
            mActivities.remove(token);
        }
        ...
        return r;
    }

重點查看activity.retainNonConfigurationInstances():

#Activity.java
    NonConfigurationInstances retainNonConfigurationInstances() {
        //ComponentActivity 重寫了該方法
        Object activity = onRetainNonConfigurationInstance();
        ...
        NonConfigurationInstances nci = new NonConfigurationInstances();
        //activity = ComponentActivity.NonConfigurationInstances
        nci.activity = activity;
        ...
        return nci;
    }

接著看onRetainNonConfigurationInstance 方法险绘。

#ComponentActivity.java
    public final Object onRetainNonConfigurationInstance() {
        ViewModelStore viewModelStore = mViewModelStore;
        ...
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        //存儲ViewModeStore
        nci.viewModelStore = viewModelStore;
        return nci;
    }

終于惹骂,又看到了ViewModeStore帆疟。
ComponentActivity.NonConfigurationInstances 存儲了viewModelStore蜘澜,然后ComponentActivity.NonConfigurationInstances 存儲在Activity. NonConfigurationInstances.activity 里箕戳。

ViewModel 能夠恢復的真正原因

重新歸納小結一下:

1、Activity 新建時代咸,創(chuàng)建了ViewModeStore蹈丸,而ViewModelStore 里存儲了ViewModel。
2呐芥、豎屏切換為橫屏時逻杖,先將當前Activity 銷毀,此時會調用到performDestroyActivity()思瘟,該方法里將ViewModeStore 封裝在NonConfigurationInstances里荸百,而NonConfigurationInstances 最終賦值給ActivityClientRecord.lastNonConfigurationInstances里。
3滨攻、ActivityClientRecord 實例存儲在ActivityThread里的Map里(Activity 啟動時存放的)够话。
4、Activity 銷毀后光绕,重建Activity時女嘲,通過ActivityThread的Map 找到之前被銷毀的Activity關聯(lián)的ActivityClientRecord,從中取出lastNonConfigurationInstances賦值給Activity.mLastNonConfigurationInstances诞帐。
5欣尼、在Activity.onCreate()里調用new ViewModelProvider(this).get(MoneyViewModel.class) 時,發(fā)現(xiàn)此時的ViewModelStore == null景埃,于是從Activity.mLastNonConfigurationInstances里獲取媒至,此時就能夠拿到上一次的ViewModeStore,進而獲取到ViewModel谷徙。

以上步驟就是ViewMode 數(shù)據(jù)恢復的過程拒啰。


image.png

核心的地方在于:ViewModel 最終存儲在ActivityThread里,而ActivityThread一直存在(和進程生命周期一致)完慧,與Activity 生命周期毫無關聯(lián)谋旦。

這也就是為什么經(jīng)常說ViewModel里不能持有Activity 的引用,因為ViewModel 的生命周期比Activity 長屈尼。

講到這册着,你可能有疑惑了:ViewModel生命周期具體有多長呢?
ComponentActivity 里有通過Lifecycle監(jiān)聽Activity生命周期脾歧,當Activity 處在"ON_DESTROY"狀態(tài)時甲捏,有如下判斷:

#ComponentActivity.java
    if (event == Lifecycle.Event.ON_DESTROY) {
        if (!isChangingConfigurations()) {
            //如果配置項沒有發(fā)生變更,則認為是Activity 正常銷毀
            //清除ViewModelStore
            getViewModelStore().clear();
        }
    }

最終將ViewModel從ViewModeStore 的Map里移除鞭执。
當我們重寫ViewModel.onCleared()方法時司顿,在ViewModel 被清除時將會被調用,用于一些資源的釋放兄纺。

因此大溜,ViewModel 具體的生命周期如下:

1、當配置項沒發(fā)生變更時估脆,ViewModel 隨著Activity 銷毀而銷毀钦奋。
2、當配置項發(fā)生變更而導致Activity 重建時疙赠,ViewModel 生命周期長于Activity付材。

4、Lifecycle/LiveData/ViewModel 關聯(lián)

ViewModel 優(yōu)勢

通篇分析下來棺聊,發(fā)現(xiàn)ViewModel.java 本身很簡單伞租,系統(tǒng)為了恢復ViewModel 做了很多工作。
優(yōu)勢

1限佩、ViewModel 不限制類型葵诈,不限制大小。
2祟同、沒有onSaveInstanceState 保存/恢復數(shù)據(jù)時的缺陷作喘。
3、ViewModel 配合LiveData晕城,既可以分離UI和數(shù)據(jù)泞坦,又可以通過數(shù)據(jù)驅動UI。
4砖顷、ViewModel 通過Activity 獲取贰锁,只要拿到Activity實例就有機會拿到ViewModel赃梧,因此可以作為多個Fragment的數(shù)據(jù)共享中轉。

Lifecycle 與LiveData 關聯(lián)

LiveData 借助Lifecycle 實現(xiàn)生命周期監(jiān)聽豌熄,判別活躍與非活躍狀態(tài)等授嘀,實現(xiàn)一個有生命周期感知的數(shù)據(jù)。

ViewModel 與Lifecycle 關聯(lián)

沒啥關聯(lián)锣险,也沒有配合使用蹄皱。

ViewModel 與 LiveData 關聯(lián)

ViewModel 分離了UI 和數(shù)據(jù),想要通過數(shù)據(jù)驅動UI芯肤,得配合LiveData 使用更香巷折。
如下簡單示例:

public class LiveDataViewModel extends ViewModel {
    private MutableLiveData<String> mutableLiveData;

    public MutableLiveData<String> getLiveData() {
        if (mutableLiveData == null) {
            mutableLiveData = new MutableLiveData<>();
        }
        return mutableLiveData;
    }
}

更多例子都在github 上,有興趣可以查看崖咨。

在分析LiveData和ViewModel時锻拘,在一開始都沒有將兩者一并提出來,就是為了讓大家能夠清晰地認識到兩者的職能邊界击蹲,因為應用場景不一樣逊拍,兩者都可以單獨使用,而對于配置項更改敏感的場景际邻,兩者結合會更加方便芯丧。

下篇將分析ViewModel,徹底厘清為啥ViewModel能夠存儲數(shù)據(jù)以及運用場合世曾。

本文基于:implementation 'androidx.appcompat:appcompat:1.4.1' & Android 10.0

ViewModel 演示&工具

您若喜歡缨恒,請點贊、關注轮听,您的鼓勵是我前進的動力

持續(xù)更新中骗露,和我一起步步為營系統(tǒng)、深入學習Android

1血巍、Android各種Context的前世今生
2萧锉、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4述寡、View Measure/Layout/Draw 真明白了
5柿隙、Android事件分發(fā)全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7鲫凶、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8禀崖、Android事件驅動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10螟炫、Android 各種坐標徹底明了
11波附、Android Activity/Window/View 的background
12、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14掸屡、Android 存儲系列
15封寞、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17仅财、Android Jetpack 前置基礎系列
18钥星、Android Jetpack 易懂易學系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市满着,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贯莺,老刑警劉巖风喇,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缕探,居然都是意外死亡魂莫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門爹耗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耙考,“玉大人,你說我怎么就攤上這事潭兽【胧迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵山卦,是天一觀的道長鞋邑。 經(jīng)常有香客問我,道長账蓉,這世上最難降的妖魔是什么枚碗? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮铸本,結果婚禮上肮雨,老公的妹妹穿的比我還像新娘。我一直安慰自己箱玷,他們只是感情好怨规,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锡足,像睡著了一般椅亚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舱污,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天呀舔,我揣著相機與錄音,去河邊找鬼。 笑死媚赖,一個胖子當著我的面吹牛霜瘪,可吹牛的內容都是我干的。 我是一名探鬼主播惧磺,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼颖对,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了磨隘?” 一聲冷哼從身側響起缤底,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎番捂,沒想到半個月后个唧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡设预,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年徙歼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳖枕。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡魄梯,死狀恐怖,靈堂內的尸體忽然破棺而出宾符,到底是詐尸還是另有隱情酿秸,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布魏烫,位于F島的核電站允扇,受9級特大地震影響,放射性物質發(fā)生泄漏则奥。R本人自食惡果不足惜考润,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望读处。 院中可真熱鬧糊治,春花似錦、人聲如沸罚舱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽管闷。三九已至粥脚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間包个,已是汗流浹背刷允。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人树灶。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓纤怒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親天通。 傳聞我的和親對象是個殘疾皇子泊窘,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內容