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
中取出傳入的引數。當然,還要使用 viewModel
的 getter
方法從其中取出 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 的官方文件中,我們也能夠得到上面的總結,
這裡使用了 Activity rotated
,也就是 Activity 處於前臺的時候配置發生變化的情況,而不是處於後臺,不知道你之前有沒有注意這一點呢?
以上。
(如有疑問,可以在評論中交流)
如果你喜歡這篇文章,請點贊哦!你也可以在以下平臺關注我哦:
- 部落格:shouheng88.github.io/
- 掘金:juejin.im/user/585555…
- Github:github.com/Shouheng88
- CSDN:blog.csdn.net/github_3518…
- 微博:weibo.com/u/540115211…
所有的文章維護在:Github, Android-notes