淺談 ViewModel 的生命週期控制

WngShhng發表於2019-01-15

1、從一個 Bug 說起

想必有過一定開發經驗的同學對 ViewModel 都不會陌生,它是 Google 推出的 MVVM 架構模式的一部分。這裡它的基礎使用我們就不介紹了,畢竟這種型別的文章也遍地都是。今天我們著重來探討一下它的生命週期。

起因是這樣的,昨天在修復程式中的 Bug 的時候遇到了一個異常,是從 ViewModel 中獲取儲存的資料的時候報了空指標。我啟用了開發者模式的 “不保留活動” 之後很容易地重現了這個異常。出現錯誤的原因也很簡單,相關的程式碼如下:

    private ReceiptViewerViewModel viewModel;

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
        handleIntent(savedInstanceState);
        // ... 
    }

    private void handleIntent(Bundle savedInstanceState) {
        LoadingStatus loadingStatus;
        if (savedInstanceState == null) {
            loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
        }
        viewModel.setLoadingStatus(loadingStatus);
    }
複製程式碼

在方法 doCreateView() 中我獲取了 viewModel 例項,然後在 handleIntent() 方法中從 Intent 中取出傳入的引數。當然,還要使用 viewModelgetter 方法從其中取出 loadingStatus 並使用。在使用的時候拋了空指標。

顯然,一般情況下是不會出現問題的,但是如果 Activity 在後臺被銷燬了,那麼再重建的時候就會出現空指標異常。

解決方法也比較簡單,在 onSaveInstanceState() 方法中將資料快取起來即可,即:

    private void handleIntent(Bundle savedInstanceState) {
        LoadingStatus loadingStatus;
        if (savedInstanceState == null) {
            loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
        } else {
            loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
        }
        viewModel.setLoadingStatus(loadingStatus);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
    }
複製程式碼

現在的問題是 ViewModel 的生命週期問題,有人說在 doCreateView() 方法的 1 處得到的不是之前的 ViewModel 嗎,資料不是之前已經設定過了嗎?所以,這牽扯 ViewModel 是在什麼時候被銷燬和重建的問題。

2、ViewModel 的生命週期

有的人希望使用 ViewModel 快取 Activity 的資訊,然後在 doCreateView() 方法的 1 處得到之前的 ViewModel 例項,這樣 ViewModel 的資料就是 Activity 銷燬之前的資料,這可行嗎?我們從原始碼角度來看下這個問題。

首先,每次獲取 viewmodel 例項的時候都會呼叫下面的方法來獲取 ViewModel 例項。從下面的 get() 方法中可以看出,例項化過的 ViewModel 是從 mViewModelStore 中獲取的。如果由 ViewModelStores.of(activity) 方法得到的 mViewModelStore 不是同一個,那麼得到的 ViewModel 也不是同一個。

下面方法中的 get() 方法中後續的邏輯是如果之前沒有快取過 ViewModel,那麼就構建一個新的例項並將其放進 mViewModelStore 中。這部分程式碼邏輯比較簡單,我們不繼續分析了。

    // ViewModelProviders#of()
    public static ViewModelProvider of(@NonNull FragmentActivity activity) {
        ViewModelProvider.AndroidViewModelFactory factory =
                ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
        return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
    }

    // ViewModelProvider#get()
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
        }

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

我們回到上述 of() 方法的 1 處,來看下 ViewModelStores.of() 方法,其定義如下:

    // ViewModelStores#of()
    public static ViewModelStore of(@NonNull FragmentActivity activity) {
        if (activity instanceof ViewModelStoreOwner) {
            return ((ViewModelStoreOwner) activity).getViewModelStore();
        }
        return holderFragmentFor(activity).getViewModelStore();
    }

    // HolderFragment#holderFragmentFor()
    public static HolderFragment holderFragmentFor(FragmentActivity activity) {
        return sHolderFragmentManager.holderFragmentFor(activity);
    }
複製程式碼

這裡會從 holderFragmentFor() 方法中獲取一個 HolderFragment 例項,它是一個 Fragment 的實現類。然後從該例項中獲取 ViewModelStore 的例項。所以,ViewModel 對生命週期的管理與 Glide 和 RxPermission 等框架的處理方式一致,就是使用一個空的 Fragment 來進行生命週期管理。

對於 HolderFragment,其定義如下。從下面的程式碼我們可以看出,上述用到的 ViewModelStore 例項就是 HolderFragment 的一個區域性變數。所以,ViewModel 使用空的 Fragment 管理生命週期實錘了。

    public class HolderFragment extends Fragment implements ViewModelStoreOwner {
        private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
        private ViewModelStore mViewModelStore = new ViewModelStore();

        public HolderFragment() {
            setRetainInstance(true);
        }

        // ...
    }
複製程式碼

此外,我們注意到上面的 HolderFragment 的構造方法中還呼叫了 setRetainInstance(true) 這一行程式碼。我們進入該方法看它的註釋:

Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:

就是說,當 Activity 被重建的時候該 Fragment 會被保留,然後傳遞給新建立的 Activity. 但是,這隻適用於不處於後臺的 Fragment. 所以,如果 Activity 處於後臺的時候,Fragment 不會保留,那麼它得到的 ViewModelStore 例項就不同了。

所以,總結下來,準確地將:當 Activity 處於前臺的時候被銷燬了,那麼得到的 ViewModel 是之前例項過的 ViewModel;如果 Activity 處於後臺時被銷燬了,那麼得到的 ViewModel 不是同一個。舉例說,如果 Activity 因為配置發生變化而被重建了,那麼當重建的時候,ViewModel 是之前的例項;如果因為長期處於後臺而被銷燬了,那麼重建的時候,ViewModel 就不是之前的例項了。

回到之前的 holderFragmentFor() 方法,我們看下這裡具體做了什麼,其定義如下。

    // HolderFragmentManager#holderFragmentFor()
    HolderFragment holderFragmentFor(FragmentActivity activity) {
        // 使用 FragmentManager 獲取 HolderFragment
        FragmentManager fm = activity.getSupportFragmentManager();
        HolderFragment holder = findHolderFragment(fm);
        if (holder != null) {
            return holder;
        }
        // 從雜湊表中獲取 HolderFragment
        holder = mNotCommittedActivityHolders.get(activity);
        if (holder != null) {
            return holder;
        }

        if (!mActivityCallbacksIsAdded) {
            mActivityCallbacksIsAdded = true;
            activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
        }
        holder = createHolderFragment(fm);
        // 將新的例項放進雜湊表中
        mNotCommittedActivityHolders.put(activity, holder);
        return holder;
    }
複製程式碼

首先,嘗試使用 FragmentManager 來獲取 HolderFragment,如果獲取不到就從 mNotCommittedActivityHolders 中進行獲取。這裡的 mNotCommittedActivityHolders 是一個雜湊表,每次例項化的新的 HolderFragment 會被新增到雜湊表中。

另外,上面的方法中還使用了 ActivityLifecycleCallbacks 對 Activity 的生命週期進行監聽。其定義如下,

    private ActivityLifecycleCallbacks mActivityCallbacks =
            new EmptyActivityLifecycleCallbacks() {
                @Override
                public void onActivityDestroyed(Activity activity) {
                    HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
                }
            };
複製程式碼

當 Activity 被銷燬的時候會從雜湊表中移除對映關係。所以,每次 Activity 被銷燬的時候雜湊表中的對映關係都不存在了。而之所以 ViewModel 能夠實現在 Activity 配置發生變化的時候獲取之前的 ViewModel 是通過上面的 setRetainInstance(true)findHolderFragment(fm) 來實現的。

總結

以上就是 ViewModel 的生命週期的總結。我們只是通過對主流程的分析研究了它的生命週期的流程,實際上內部還有許多小細節,邏輯也比較簡單,我們就不一一說明了。

其實,從 Google 的官方文件中,我們也能夠得到上面的總結,

viewmodel-lifecycle

這裡使用了 Activity rotated,也就是 Activity 處於前臺的時候配置發生變化的情況,而不是處於後臺,不知道你之前有沒有注意這一點呢?

以上。

(如有疑問,可以在評論中交流)


如果你喜歡這篇文章,請點贊哦!你也可以在以下平臺關注我哦:

所有的文章維護在:Github, Android-notes

相關文章