1. 程式人生 > >ViewModel 基礎使用和原始碼分析

ViewModel 基礎使用和原始碼分析

前言

承接上篇的學習順序,本文主要是對 ViewModel 的學習。ViewModel 是用來儲存 UI 資料的類,並且會在配置變更後(如螢幕旋轉)繼續存在。先總結下 ViewModel 的特點:

  • 提供 UI 介面的資料

  • 負責和資料層通訊

  • 配置變更後繼續存在

官方文件建議,我們應將應用的 UI 資料儲存在 ViewModel 中,而不是 Activity 中,確保資料不會受到 Configuration Change 的影響。 儘量弱化 Activity 的職責, Activity 僅負責如何在螢幕上顯示資料和接受使用者互動。

如果系統銷燬或重新建立 UI 控制器,儲存在其中的臨時資料會造成丟失。例如:在某個 Activity

中展示使用者列表,因配置變更導致 Activity 重新建立。新建立的 Activity必須重新獲取使用者列表。對於簡單的資料,可使用 onSaveInstanceState() 儲存,並在 onCreate() 中通過 Bundle 進行資料恢復。但是這種方法僅適用於少量資料,不適用儲存大量資料(如:使用者列表和 bitmaps)。

另一個問題是,UI 控制器經常需要接受一些非同步回撥。UI 控制器需要管理這些非同步回撥,確保在介面銷燬時,不會發生潛在的記憶體洩漏問題。這種處理方式需要耗費大量的精力,並且在配置變更重新建立物件時,重新聯網獲取資料也會造成資源的浪費。

ActiviyFragment

主要是用來顯示 UI 資料,接受使用者的互動請求或者處理系統通訊(許可權請求)。如果把從資料庫或網路獲取的資料,都一窩蜂的堆積在 ActivityFragment 中。會造成該類程式碼膨脹,為日後的維護埋下了隱患。為 UI 控制器分配過多的任務,違背了單一職責原則,也會使單元測試變得困難。

將資料和 UI 分離,將會讓開發和維護變得更加高效和容易。囉嗦了這麼多,下面正式進入本文的主題 ViewModel

實現 ViewModel

Android Jetpack Components 提供了 ViewModel 類,用來給 UI 控制器提供資料。ViewModel 在配置變更時自動保留,以便儲存的資料用於下一個 Activity

Fragment 的例項。下面是一個計數器的例子,來展示 ViewModel 的資料在配置變更後繼續存在的特性。

class MainViewModel : ViewModel() {

    var count = 0

}
複製程式碼

我們把按鈕點選次數的 count 屬性儲存在 ViewModel 中,接下來在 Activity 中使用。

@SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        btn.setOnClickListener {
            mainViewModel.count++
            tvCount.text = "點選次數:${mainViewModel.count}"
        }
    }
複製程式碼

通過一個 TextView 顯示按鈕點選的次數,當旋轉螢幕,Activity 重新建立,但 count 被沒有被銷燬。

如果 Activity 重新建立,它將接受到由第一個 Activity 建立的相同 MainViewModel 例項。當 Activity 關閉,framework 層會呼叫 ViewModelonCleared()釋放資源。但需要注意的是,開發者應該自己實現 onCleared(),而不用關心 onCleared() 的呼叫時機。

如果你的 ViewModel 需要通過建構函式傳遞引數,可以使用 ViewModelFactory 來建立自定義建構函式。如:

class LoginViewModelFactory(
        private val repo: LoginDataSourceRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            LoginViewModel(repo) as T
}
複製程式碼

注意: 不要將 Context 傳入 ViewModel 中,也就是說 ViewModel 中不應當持有 ActivityFragmentView 的引用。

如果 ViewModel 需要 Applicationcontext 物件(如:使用系統服務),可以繼承自 AndroidViewModel

ViewModel 的生命週期

當獲取 ViewModel 時,ViewModel 物件的範圍限定為傳遞給 ViewModelProviderLifecycleOwner 物件。當 Activity finsih 或者 Fragment detach 時,ViewModel 將會一直保留在記憶體中。

下圖展示了 ActivityViewModel 的生命週期

viewmodel-lifecycle.png

我們應當在系統第一次呼叫 ActivityonCreate() 時,去獲取 ViewModel 物件。當系統因配置變更時,重新建立 Activity 時,ViewModel 還是第一次獲取到的例項。

Fragment 共享資料

Activity 中內嵌一個或者多個 Fragment 是常見的做法。一般情況下,Fragment 之間通訊,都是採用介面回撥或者 EventBus 。現在又多了一個新的選擇 ViewModel,以下是官方的示例程式碼:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        model.selected.observe(this, Observer<Item> { item ->
            // Update the UI
        })
    }
}
複製程式碼

由於 MasterFragmentDetailFragment 擁有相同的宿主 Activity,因此獲取到的 ViewModel 示例也是一樣的。

原始碼解析

在分析原始碼前,我們先思考下面兩個問題:

  • ViewModel 是通過什麼儲存的?

  • 系統在因配置變更,是如何保留 ViewModel 的例項?

1. ViewModelProviders

還是以 MainActivity 中的程式碼作為切入點。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("onCreate")
        setContentView(R.layout.activity_main)
        // 步驟 1
        val viewModelProvider = ViewModelProviders.of(this)
        // 步驟 2
        val mainViewModel = viewModelProvider.get(MainViewModel::class.java)
    }
    
}
複製程式碼

通過 of() 方法,可以獲取一個 ViewModelProvider 示例。

    public static ViewModelProvider of(@NonNull FragmentActivity activity,
            @Nullable Factory factory) {
        Application application = checkApplication(activity);
        if (factory == null) {
            factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
        }
        return new ViewModelProvider(ViewModelStores.of(activity), factory);
    }
複製程式碼

2. ViewModelStores

ViewModelStores 主要是用來提供 ViewModelStore 的例項。通過 of() 返回一個 ViewModelStore

  public static ViewModelStore of(@NonNull FragmentActivity activity) {
        if (activity instanceof ViewModelStoreOwner) {
            return ((ViewModelStoreOwner) activity).getViewModelStore();
        }
        return holderFragmentFor(activity).getViewModelStore();
    }
複製程式碼

3. ViewModelStoreOwner

@SuppressWarnings("WeakerAccess")
public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}
複製程式碼

SDK 27 及以上版本,supprot 包下的 FragmentActivityFragment 實現了 ViewModelStoreOwner。該介面主要是在配置變更時,保留原有的 ViewModelStore

4. ViewModelStore

ViewModelStore 在內部通過 HashMap 來存放 ViewModel。當 ViewModelStoreOwner 因配置發生變更時,該類會被系統保留,確保新建立的 Activity 能獲取到和之前一樣的 ViewModelStore

ViewModelStoreOwner 被銷燬,並且不會新建時。ViewModelStoreclear() 將會呼叫,繼而呼叫 ViewModelonCleared() 釋放資源。

到目前為止,對於提出的第一個問題。我們已經清楚了。現在讓我們進入 FragmentActivity 中檢視系統是如何儲存 ViewModelStore 例項的。

FragmentActivity

  1. onCreate() 出發
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        super.onCreate(savedInstanceState);

        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            mViewModelStore = nc.viewModelStore;
        }
       
       ...
    }
複製程式碼

現在可以肯定的是,ViewModelStore 物件是儲存在 NonConfigurationInstances 中。getLastNonConfigurationInstance() 是定義在 Activity 中的,該方法用來獲取之前 onRetainNonConfigurationInstance() 返會的 Object物件。

  * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
     */
    @Nullable
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }
複製程式碼
  1. 現在我們看下 Activity 中的onRetainNonConfigurationInstance()
public Object onRetainNonConfigurationInstance() {
        return null;
    }
複製程式碼

通過註釋可以發現,該方法在 Activity因配置變更並銷燬的時候由系統呼叫,具體的呼叫時機是在 onStop()onDestory() 之間。

  1. 接下來我們檢視 FragmentActivityonRetainNonConfigurationInstance 的具體實現。
 public final Object onRetainNonConfigurationInstance() {
        if (mStopped) {
            doReallyStop(true);
        }

        Object custom = onRetainCustomNonConfigurationInstance();

        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        if (fragments == null && mViewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = mViewModelStore;
        nci.fragments = fragments;
        return nci;
    }
複製程式碼

高興的是,我們在該方法中發現了 mViewModelStore 的身影。

現在讓我們梳理下,系統對於 ViewModel 儲存的邏輯。當 Activity 因配置變更銷燬時,系統會呼叫 onRetainNonConfigurationInstance() 儲存 ViewModel。在新建 Activity 中的 onCreate() 方法通過 getLastNonConfigurationInstance() 獲取 NonConfigurationInstances。繼而獲取先前的 ViewModel 例項。

到此為止 步驟 1 中程式碼的執行流程就分析完了,步驟 2 的程式碼,我們簡單看下。

    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.
            }
        }

        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }
複製程式碼

Activity 因配置變更重建時,mViewModelStore 還是一開始建立的示例,因此返回的 ViewModel 物件和最初的一樣。

總結

筆者是基於 SDK 版本 27 ,Lifecycle 版本 1.1.1 分析的。需要注意的是系統在 SDK 27 之前是通過一個不可見的 Fragment ,將 setRetainInstance() 設定為 true 進行處理的。筆者不再做過多分析,感興趣的可自行研究。如分析有誤,還多請指正。

參考資料:
官方文件
B 站視訊講解