ViewModel 基礎使用和原始碼分析
前言
承接上篇的學習順序,本文主要是對 ViewModel
的學習。ViewModel
是用來儲存 UI 資料的類,並且會在配置變更後(如螢幕旋轉)繼續存在。先總結下 ViewModel
的特點:
-
提供 UI 介面的資料
-
負責和資料層通訊
-
配置變更後繼續存在
官方文件建議,我們應將應用的 UI 資料儲存在 ViewModel
中,而不是 Activity
中,確保資料不會受到 Configuration Change
的影響。 儘量弱化 Activity
的職責, Activity
僅負責如何在螢幕上顯示資料和接受使用者互動。
如果系統銷燬或重新建立 UI 控制器,儲存在其中的臨時資料會造成丟失。例如:在某個 Activity
Activity
重新建立。新建立的 Activity
必須重新獲取使用者列表。對於簡單的資料,可使用 onSaveInstanceState()
儲存,並在 onCreate()
中通過 Bundle
進行資料恢復。但是這種方法僅適用於少量資料,不適用儲存大量資料(如:使用者列表和 bitmaps
)。
另一個問題是,UI 控制器經常需要接受一些非同步回撥。UI 控制器需要管理這些非同步回撥,確保在介面銷燬時,不會發生潛在的記憶體洩漏問題。這種處理方式需要耗費大量的精力,並且在配置變更重新建立物件時,重新聯網獲取資料也會造成資源的浪費。
Activiy
和 Fragment
Activity
獲 Fragment
中。會造成該類程式碼膨脹,為日後的維護埋下了隱患。為 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
層會呼叫 ViewModel
的 onCleared()
釋放資源。但需要注意的是,開發者應該自己實現 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
中不應當持有Activity
,Fragment
,View
的引用。
如果 ViewModel
需要 Application
的 context
物件(如:使用系統服務),可以繼承自 AndroidViewModel
。
ViewModel 的生命週期
當獲取 ViewModel
時,ViewModel
物件的範圍限定為傳遞給 ViewModelProvider
的 LifecycleOwner
物件。當 Activity
finsih 或者 Fragment
detach 時,ViewModel
將會一直保留在記憶體中。
下圖展示了 Activity
和 ViewModel
的生命週期
我們應當在系統第一次呼叫 Activity
的 onCreate()
時,去獲取 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
})
}
}
複製程式碼
由於 MasterFragment
和 DetailFragment
擁有相同的宿主 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
包下的 FragmentActivity
和 Fragment
實現了 ViewModelStoreOwner
。該介面主要是在配置變更時,保留原有的 ViewModelStore
。
4. ViewModelStore
ViewModelStore
在內部通過 HashMap
來存放 ViewModel
。當 ViewModelStoreOwner
因配置發生變更時,該類會被系統保留,確保新建立的 Activity
能獲取到和之前一樣的 ViewModelStore
。
當 ViewModelStoreOwner
被銷燬,並且不會新建時。ViewModelStore
的 clear()
將會呼叫,繼而呼叫 ViewModel
的 onCleared()
釋放資源。
到目前為止,對於提出的第一個問題。我們已經清楚了。現在讓我們進入 FragmentActivity
中檢視系統是如何儲存 ViewModelStore
例項的。
FragmentActivity
- 從
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;
}
複製程式碼
- 現在我們看下
Activity
中的onRetainNonConfigurationInstance()
。
public Object onRetainNonConfigurationInstance() {
return null;
}
複製程式碼
通過註釋可以發現,該方法在 Activity
因配置變更並銷燬的時候由系統呼叫,具體的呼叫時機是在 onStop()
和 onDestory()
之間。
- 接下來我們檢視
FragmentActivity
中onRetainNonConfigurationInstance
的具體實現。
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
進行處理的。筆者不再做過多分析,感興趣的可自行研究。如分析有誤,還多請指正。