1. 程式人生 > 其它 >Android mvvm框架之ViewModel原理

Android mvvm框架之ViewModel原理

ViewModel 為 Android 元件間實現資料共享提供了一種更鬆耦合的方式。同時在維護元件狀態方面也更加便捷,再也不用採用繁瑣的 save instance 的方式了。

目錄

  • 1 什麼是 ViewModel
    • 1.1 先考慮兩個場景
    • 1.2 缺點
    • 1.3 特別說明
    • 1.4 ViewModel 解決的問題
  • 2 ViewModel 實現原理
    • 2.1 ViewModel 類
    • 2.2 ViewModel 的構造過程
  • 3 ViewModel 與配置無關的原理(與宿主 Controller 俱生俱滅)
    • 3.1 ViewModelStore 樹
    • 3.2 系統級的配置無關支援
  • 4 FragmentActivity 中的 ViewModel 生命週期
  • 5 多 Controller 共享 ViewModel
  • 6 關於工廠模式的一點思考


1 什麼是 ViewModel

1.1 先考慮兩個場景

  • 場景一:我們開發的 APP 可以轉屏,轉屏後將觸發 Controller(Activity or Fragment) 的重建,為了維護轉屏前後資料的一致性,我們要麼將需要維護的資料以 Bundle 的形式在 onSaveInstance 中儲存,必要的時候需要對複合資料實現繁瑣的 Parceable 介面,如果資料量太大則我們必須將資料持久化,在轉屏後重新拉取資料(from database or networks);
  • 場景二:我們的 Activity 中同時維護了多個 Fragment,每個 Fragment 需要共享一些資料,傳統的做法是由宿主 Activity 持有共享資料,並暴露資料獲取介面給各個寄生 Fragment。

1.2 缺點

隨著業務規模的擴大,以上的兩種場景在傳統實現方法中顯得越來越繁瑣且不易維護,且資料模組不易獨立進行測試。

1.3 特別說明

關於場景一,同樣的場景還適用於各種配置相關的資訊發生變化的情況,比如鍵盤、系統字型、語言區域等,它們的共同作用是都會導致當前 Controller 的重建。

1.4 ViewModel 解決的問題

ViewModel 是 android 新的 mvvm 框架的一部分,它的出現就是為了解決以上兩個場景中資料與 Controller 耦合過度的問題。其 基本原理 是:維護一個與配置無關的物件,該物件可儲存 Controller 中需要的任何資料,其生命週期與宿主 Controller 的生命週期保持一致,不因 Controller 的重建而失效(注意:Controller 的重建仍然在 Controller 生命週期內,並不會產生一個新的生命週期,即 Controller 的 onDestroy 並不會呼叫)

這意味著無論是轉屏還是系統字型變化等因配置變化產生的 Controller 重建都不會回收 ViewModel 中維護的資料,重建的 Controller 仍然可以從同一個 ViewModel 中通過獲取資料恢復狀態。



2 ViewModel 實現原理

2.1 ViewModel 類

如果大家去看一下 ViewModel 類的實現,會發現雖然它是一個 abstract 類,但是沒有暴露任何外部可訪問的方法,其預留的方法都是 package 訪問許可權的,
其預留了一些資料清理工作的功能,推測可能是系統保留用作以後擴充套件,因為與我們對 ViewModel 原理的理解沒有什麼關聯,我們暫且略過。

2.2 ViewModel 的構造過程

我們用一個結構圖來剖析 ViewModel 的構造過程:

如圖所示:

  • 所有已經例項化的 ViewModel 都快取在一個叫做 ViewModelStore 的封裝物件中,其實質是一個 HashMap;
  • ViewModelStore 與具體的 Controller 繫結,並與宿主 Controller 俱生俱滅,所以這就解釋了為何 ViewModel 與宿主 Controller 的生命週期是一樣長了,因為快取它的 ViewModelStore 與宿主 Controller 壽命相等;
  • 獲取 ViewModel 例項的過程委託給了一個叫做 ViewModelProvider 的工具類,它包含一個建立 ViewModel 的工廠類 Factory 和一個對 ViewModelStore 的引用;
  • 總的構造過程為:先從 ViewModelStore 中獲取快取的 ViewModel,若沒有快取過則用 Facotry 例項化一個新的 ViewModel 並快取,具體的過程分為 4 步,具體可參考圖示。

本小節剩下部分分析原始碼,對於只關心原理的同學此部分可以略過:

我們在獲取 ViewModel 的時候,一般通過如下方式:

// 在 Controller(這裡以 Fragment 為例)的 onCreate 方法中呼叫
final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);

我們看一下 ViewModelProviders.of() 的實現:

public static ViewModelProvider of(@NonNull Fragment fragment) {
    return of(fragment, null);
}
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
    Application application = checkApplication(checkActivity(fragment));
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }

    // 最終用宿主 Controller 的 ViewModelStore 和一個 Factory 例項化一個
    // ViewModelProvider
    return new ViewModelProvider(fragment.getViewModelStore(), factory);
}

我們再看一下 ViewModelProvider.get() 方法獲取 ViewModel 例項的過程:

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");
    }

    // 我們看到了 ViewModel 在 ViewModelStore 中的 key 表示
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {

    // 先檢查快取中是否存在
    ViewModel viewModel = mViewModelStore.get(key);
    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        return (T) viewModel;
    } else {
        //noinspection StatementWithEmptyBody
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }

    // 快取中沒有,通過 Factory 構造
    if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
    } else {
        viewModel = (mFactory).create(modelClass);
    }

    // 新例項儲存快取
    mViewModelStore.put(key, viewModel);
    //noinspection unchecked
    return (T) viewModel;
}


3 ViewModel 與配置無關的原理(與宿主 Controller 俱生俱滅)

上一節我們說到,ViewModel 之所以能夠與宿主 Controller 保持生命週期一致,是因為儲存它的 ViewModelStore 與宿主 Controller 生命週期一致。那麼為什麼 ViewModelStore 能夠保持和 Controller 生命週期一致呢?

這裡我們需要先理清 FragmentActivity 和其寄生的 Fragment 的 ViewModelStore 之間的關係:

3.1 ViewModelStore 樹

如圖所示:

  • 每個 ViewModelStore 依附於其宿主 Controller,所以各個 Controller 的 ViewModelStore 組成一個樹狀的引用關係;
  • 處於頂層的 ViewModelStore 依附於 FragmentActivity,它除了儲存使用者級的 ViewModel 以外,還儲存其兒子 Fragment 的 FragmentManagerViewModel;
  • FragmentManagerViewModel 主要維護兩個物件:所屬 Fragment 的 ViewModelStore 和其兒子 Fragment 的 FragmentManagerViewModel 的引用,注意圖中的紅色部分,所有二級及以下的子孫 Fragment 都共用同一個父節點的 Child FragmentManagerModel,這樣當父 Fragment 銷燬的時候方便一次性清除其所有子 Fragment 共用的 FragmentManagerViewModel;
  • 但是二級及以下的子孫 Fragment 的 ViewModelStore 都是獨立的,一個 Fragment 自身的 ViewModel 變化應該不影響其兄弟節點的 ViewModel,所以可以推匯出,它們共同的 FragmentManagerViewModel 應該是維護了一個儲存各個子 Fragment 的 ViewModelStore 的容器,大家如果細看 FragmentManagerViewModel 的原始碼,實際上就是這麼做的。

所以,我們看到,處於頂層的 FragmentActivity 的 ViewModelStore 是一個超級 Store,它引用了所有的 ViewModels,包括自身的資料、所有子孫 Fragment 的 ViewModels,只要各子孫 Fragment 不清除自有 ViewModelStore,則所有的資料都維護在這棵 ViewModelStore 樹中。

那麼在配置發生變化的時候,ViewModelStore 樹如何保持不變呢?

3.2 系統級的配置無關支援

將 ViewModelStore 作為配置無關資料進行保持,在 FragmentActivity 中是這麼做的:

是的,流程就是這麼簡單,只需要將 ViewModelStore 封裝在一個特殊物件中儲存並在 FragmentActivity 的 onRetainNonConfigurationInstance() 方法中返回即可:

/**
 * Called by the system, as part of destroying an
 * activity due to a configuration change, when it is known that a new
 * instance will immediately be created for the new configuration.  You
 * can return any object you like here, including the activity instance
 * itself, which can later be retrieved by calling
 * {@link #getLastNonConfigurationInstance()} in the new activity
 * instance.
 */
@Override
@Nullable
public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;

    // ...省略與原理無關程式碼

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

這樣,在頂層源頭上就保證了所有 Controller 的 ViewModels 不會在傳送配置變化的時候由於 Controller 重建而被銷燬。

另外在 Fragment 層中,必須區分 Fragment 例項銷燬時到底是因為呼叫了 onDestroy 還是配置發生了變化,如果是前者則必須清理自身持有的 ViewModelStore,如果是後者則不能清理:

如圖所示,也說明了 Fragment 的 ViewModel 生命週期與該 Fragment 生命週期是一致的。

// FragmentManagerImpl.moveToState()
//...省略
boolean beingRemoved = f.mRemoving && !f.isInBackStack(); // 是否 destroy,如果只是配置變化,則為 false
if (beingRemoved || mNonConfig.shouldDestroy(f)) {
    boolean shouldClear;
    if (mHost instanceof ViewModelStoreOwner) {
        // 說明這是第一層 Fragment,只要頂層 ViewModelStore 沒有清除該 FragmentManagerViewModel 就說明不用清理
        shouldClear = mNonConfig.isCleared();
    } else if (mHost.getContext() instanceof Activity) {
        // 說明是 FragmentActivity 的子孫 Fragment,根據是否是配置變化來判斷是否需要清理
        Activity activity = (Activity) mHost.getContext();
        shouldClear = !activity.isChangingConfigurations();
    } else {
        shouldClear = true;
    }
    if (beingRemoved || shouldClear) {
        // 只有確實 destroy 了才需要清理
        mNonConfig.clearNonConfigState(f);
    }
    f.performDestroy();
    dispatchOnFragmentDestroyed(f, false);
}
//...省略


4 FragmentActivity 中的 ViewModel 生命週期

最後,我們還需要說明一下,FragmentActivity 中的 ViewModel 的生命週期是如何保持與 FragmentActivity 一致的,除了上一節中 FragmentActivity.onRetainNonConfigurationInstance() 中的配置無關保證以外,還需要保證在 Activity 真正銷燬的時候其所持有的 ViewModel 也應該被清理。

其程式碼實現非常簡單,只需要觀察該 Activity 的 Lifecycle 狀態,並在銷燬狀態時進行清理即可,關於 Lifecycle 我們將用專門的章節進行說明,以下為清理程式碼:

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            // 觀察到 Activity 被銷燬
            if (!isChangingConfigurations()) {
                // 若不是配置變化,則清理
                getViewModelStore().clear();
            }
        }
    }
});


5 多 Controller 共享 ViewModel

我們參考第3.1節的 ViewModelStore 樹可知,如果多個 Controller 需要共享同一個 ViewModel 的話,我們只需要將該 ViewModel 儲存在這些 Controller 共同的父 Controller 的 ViewModelStore 中即可,而這些子 Controller 可以通過如下方式獲取這個共享的 ViewModel:

[Fragment/FragmentActivity] parentContrl = ... // 共同的父 Controller
final CommonViewModel viewModel = ViewModelProviders.of(parentContrl).get(CommonViewModel.class);

這樣我們就解決了在 1.1 節中提到的第二個場景共享資料的問題。



6 關於工廠模式的一點思考

回到第2節中如何獲取一個 ViewModel 例項的過程,我們發現,ViewModelProviders 實際相當於一個 簡單工廠 模式,而 Facotry 則是一個 工廠方法 模式,前者根據不同的引數構造不同的 ViewModelProvider,後者則可以實現不同的具體 Factory 來構造不同的 ViewModel。

這裡有兩層抽象:

  • 如何實現並例項化一個數據模型,各個資料模型的實現細節是不可預知的 -> ViewModel;
  • 採用什麼樣的規則去得到一個 ViewModel 例項,且該規則是一個統一可預知的規則,但並不關心 ViewModel 實現的細節 -> ViewModelProvider。

所以我們看到,當一個物件的構造是採用了統一的規則時(比如 ViewModelProvider),適合用簡單工廠模式來實現,因為該規則本身可以被封裝;而當一個物件的構造方式沒有統一規則可以遵循,其實現細節更多與業務相關時,其可被封裝的部分僅為它的 new 方法,這時更適合用工廠方法模式來實現。




說明:

本文參考的 androidx 版本為

core: 1.1.0

lifecyle: 2.2.0-alpha01

fragment: 1.1.0-alpha09


如果大家喜歡我寫的文章,歡迎關注我的公眾號——小舍,將有更多有趣的內容分享給大家: