Android 面試題:為什麼 Activity 都重建了 ViewModel 還存在?—— Jetpack 系列(3)
theme: jzman
請點贊,你的點贊對我意義重大,滿足下我的虛榮心。
🔥 Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 跟我一起成長。
前言
ViewModel 是 Jetpack 元件中較常用的元件之一,也是實現 MVVM 模式或 MVI 模式的標準組件之一。在這篇文章裡,我將與你討論 ViewModel 實用和麵試常見的知識點。如果能幫上忙請務必點贊加關注,這對我非常重要。
這篇文章是 Jetpack 系列文章第 3 篇,專欄文章列表:
一、架構元件:
- 1、Lifecycle:生命週期感知型元件的基礎
- 2、LiveData:生命週期感知型資料容器
- 3、ViewModel:資料驅動型介面控制器(本文)
- 4、Flow:LiveData 的替代方案
- 5、從 MVC 到 MVP、MVVM、MVI:Android UI 架構演進
- 6、ViewBinding:新一代檢視繫結方案
- 7、Fragment:模組化的微型 Activity
- 8、RecyclerView:可複用型列表檢視
- 9、Navigation:單 Activity 多 Fragment 的導航方案
- 10、Dagger2:從 Dagger2 到 Hilt 玩轉依賴注入(一)
- 11、Hilt:從 Dagger2 到 Hilt 玩轉依賴注入(二)
- 12、OnBackPressedDispatcher:處理回退事件的新姿勢
二、其他:
- 1、AppStartup:輕量級初始化框架
- 2、DataStore:新一代鍵值對儲存方案
- 3、Room:ORM 資料庫訪問框架
- 4、WindowManager:加強對多視窗模式的支援
- 5、WorkManager:加強對後臺任務的支援
- 6、Compose:新一代檢視開發方案
1. 認識 ViewModel
1.1 為什麼要使用 ViewModel?
ViewModel 的作用可以區分 2 個維度來理解:
- 1、介面控制器維度: 在最初的 MVC 模式中,Activity / Fragment 中承擔的職責過重,因此,在後續的 UI 開發模式中,我們選擇將 Activity / Fragment 中與檢視無關的職責抽離出來,在 MVP 模式中叫作 Presenter,在 MVVM 模式中叫作 ViewModel。因此,我們使用 ViewModel 來承擔介面控制器的職責,並且配合 LiveData / Flow 實現資料驅動。
- 2、資料維度: 由於 Activity 存在因配置變更銷燬重建的機制,會造成 Activity 中的所有瞬態資料丟失,例如網路請求得到的使用者資訊、視訊播放資訊或者非同步任務都會丟失。而 ViewModel 能夠應對 Activity 因配置變更而重建的場景,在重建的過程中恢復 ViewModel 資料,從而降低使用者體驗受損。
關於 MVVM 等模式的更多內容,我們在 5、從 MVC 到 MVP、MVVM、MVI:Android UI 架構演進 這篇文章討論過。
MVVM 模式示意圖:
MVI 模式示意圖:
ViewModel 生命週期示意圖:
1.2 ViewModel 的使用方法
- 1、新增依賴: 在 build.gradle 中新增 ViewModel 依賴,需要注意區分過時的方式:
```gradle // 過時方式(lifecycle-extensions 不再維護) implementation "androidx.lifecycle:lifecycle-extensions:2.4.0"
// 目前的方式: def lifecycle_version = "2.5.0" // Lifecycle 核心類 implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" ```
- 2、模板程式碼: ViewModel 通常會搭配 LiveData 使用,以下為使用模板,相信大家都很熟悉了:
NameViewModel.kt
class NameViewModel : ViewModel() {
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
}
MainActivity.kt
``` class MainActivity : AppCompatActivity() {
private val model: NameViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// LiveData 觀察者
val nameObserver = Observer<String> { newName ->
// 更新檢視
nameTextView.text = newName
}
// 註冊 LiveData 觀察者,this 為生命週期宿主
model.currentName.observe(this, nameObserver)
// 修改 LiveData 資料
button.setOnClickListener {
val anotherName = "John Doe"
model.currentName.value = anotherName
}
}
} ```
1.3 ViewModel 的建立方式
建立 ViewModel 例項的方式主要有 3 種,它們最終都是通過第 1 種 ViewModelProvider 完成的:
- 方法 1: ViewModelProvider 是建立 ViewModel 的工具類:
示例程式
``` // 不帶工廠的建立方式 val vm = ViewModelProvider(this).get(MainViewModel::class.java) // 帶工廠的建立方式 val vmWithFactory = ViewModelProvider(this, MainViewModelFactory()).get(MainViewModel::class.java)
// ViewModel 工廠 class MainViewModelFactory(
) : ViewModelProvider.Factory {
private val repository = MainRepository()
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
} ```
- 方法 2: 使用 Kotlin by 委託屬性,本質上是間接使用了 ViewModelProvider:
示例程式
```kotlin // 在 Activity 中使用 class MainActivity : AppCompatActivity() { // 使用 Activity 的作用域 private val viewModel : MainViewModel by viewModels() }
// 在 Fragment 中使用 class MainFragment : Fragment() { // 使用 Activity 的作用域,與 MainActivity 使用同一個物件 val activityViewModel : MainViewModel by activityViewModels() // 使用 Fragment 的作用域 val viewModel : MainViewModel by viewModels() } ```
- 方法 3: Hilt 提供了注入部分 Jetpack 架構元件的支援
示例程式
```kotlin @HiltAndroidApp class DemoApplication : Application() { ... }
@HiltViewModel class MainViewModel @Inject constructor() : ViewModel() { ... }
@AndroidEntryPoint
class MainHiltActivity : AppCompatActivity(){
val viewModel by viewModels
依賴項
gradle
// Hilt ViewModel 支援
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0"
// Hilt 註解處理器
kapt "androidx.hilt:hilt-compiler:1.0.0"
需要注意的是,雖然可以使用依賴注入普通物件的方式注入 ViewModel,但是這相當於繞過了 ViewModelProvider 來建立 ViewModel。這意味著 ViewModel 例項一定不會存放在 ViewModelStore 中,將失去 ViewModel 恢復介面資料的特性。
錯誤示例
kotlin
@AndroidEntryPoint
class MainHiltActivity : AppCompatActivity(){
@Inject
lateinit var viewModel : MainViewModel
}
2. ViewModel 實現原理分析
2.1 ViewModel 的建立過程
上一節提到,3 種建立 ViewModel 例項的方法最終都是通過 ViewModelProvider 完成的。ViewModelProvider 可以理解為建立 ViewModel 的工具類,它需要 2 個引數:
- 引數 1 ViewModelStoreOwner: 它對應於 Activity / Fragment 等持有 ViewModel 的宿主,它們內部通過 ViewModelStore 維持一個 ViewModel 的對映表,ViewModelStore 是實現 ViewModel 作用域和資料恢復的關鍵;
- 引數 2 Factory: 它對應於 ViewModel 的建立工廠,預設時將使用預設的 NewInstanceFactory 工廠來反射建立 ViewModel 例項。
建立 ViewModelProvider 工具類後,你將通過 get() 方法來建立 ViewModel 的例項。get() 方法內部首先會通過 ViewModel 的全限定類名從對映表(ViewModelStore)中取快取,未命中才會通過 ViewModel 工廠建立例項再快取到對映表中。
正因為同一個 ViewModel 宿主使用的是同一個 ViewModelStore 對映表,因此在同一個宿主上重複呼叫 ViewModelProvider#get() 返回同一個 ViewModel 例項。
ViewModelProvider.java
```java // ViewModel 建立工廠 private final Factory mFactory; // ViewModel 儲存容器 private final ViewModelStore mViewModelStore;
// 預設使用 NewInstanceFactory 反射建立 ViewModel public ViewModelProvider(ViewModelStoreOwner owner) { this(owner.getViewModelStore(), ... NewInstanceFactory.getInstance()); }
// 自定義 ViewModel 建立工廠 public ViewModelProvider(ViewModelStoreOwner owner, Factory factory) { this(owner.getViewModelStore(), factory); }
// 記錄宿主的 ViewModelStore 和 ViewModel 工廠 public ViewModelProvider(ViewModelStore store, Factory factory) { mFactory = factory; mViewModelStore = store; }
@NonNull
@MainThread
public
// Fragment
@NonNull
@MainThread
public
// 預設的 ViewModel 工廠 public static class NewInstanceFactory implements Factory {
private static NewInstanceFactory sInstance;
@NonNull
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}
@NonNull
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
// 反射建立 ViewModel 物件
return modelClass.newInstance();
}
} ```
ViewModelStore.java
```java
// ViewModel 本質上就是一個對映表而已
public class ViewModelStore {
//
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
} ```
ViewModel 宿主是 ViewModelStoreOwner 介面的實現類,例如 Activity:
ViewModelStoreOwner.java
java
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
androidx.activity.ComponentActivity.java
```java public class ComponentActivity extends androidx.core.app.ComponentActivity implements ContextAware, LifecycleOwner, ViewModelStoreOwner ... {
// ViewModel 的儲存容器
private ViewModelStore mViewModelStore;
// ViewModel 的建立工廠
private ViewModelProvider.Factory mDefaultFactory;
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 已簡化,後文補全
mViewModelStore = new ViewModelStore();
}
return mViewModelStore;
}
} ```
2.2 by viewModels() 實現原理分析
by 關鍵字是 Kotlin 的委託屬性,內部也是通過 ViewModelProvider 來建立 ViewModel。關於 Kotlin 委託屬性的更多內容,我們在 Kotlin | 委託機制 & 原理 & 應用 這篇文章討論過,這裡不重複。
ActivityViewModelLazy.kt
```kotlin
@MainThread
public inline fun
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
} ```
ViewModelLazy.kt
```kotlin
public class ViewModelLazy
override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
// 最終也是通過 ViewModelProvider 建立 ViewModel 例項
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
} ```
2.3 ViewModel 如何實現不同的作用域
ViewModel 內部會為不同的 ViewModel 宿主分配不同的 ViewModelStore 對映表,不同宿主是從不同的資料來源來獲取 ViewModel 的例項,因而得以區分作用域。
具體來說,在使用 ViewModelProvider 時,我們需要傳入一個 ViewModelStoreOwner 宿主介面,它將在 getViewModelStore() 介面方法中返回一個 ViewModelStore 例項。
- 對於 Activity 來說,ViewModelStore 例項是直接儲存在 Activity 的成員變數中的;
- 對於 Fragment 來說,ViewModelStore 例項是間接儲存在 FragmentManagerViewModel 中的
對映表中的。
這樣就實現了不同的 Activity 或 Fragment 分別對應不同的 ViewModelStore 例項,進而區分不同作用域。
androidx.activity.ComponentActivity.java
```java public class ComponentActivity extends androidx.core.app.ComponentActivity implements ContextAware, LifecycleOwner, ViewModelStoreOwner ... {
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 已簡化,後文補全
mViewModelStore = new ViewModelStore();
}
return mViewModelStore;
}
} ```
Fragment.java
java
@NonNull
@Override
public ViewModelStore getViewModelStore() {
// 最終呼叫 FragmentManagerViewModel#getViewModelStore(Fragment)
return mFragmentManager.getViewModelStore(this);
}
FragmentManagerViewModel.java
```java
//
@NonNull ViewModelStore getViewModelStore(@NonNull Fragment f) { ViewModelStore viewModelStore = mViewModelStores.get(f.mWho); if (viewModelStore == null) { viewModelStore = new ViewModelStore(); mViewModelStores.put(f.mWho, viewModelStore); } return viewModelStore; } ```
2.4 為什麼 Activity 在螢幕旋轉重建後可以恢復 ViewModel?
ViewModel 底層是基於原生 Activity 因裝置配置變更重建時恢復資料的機制實現的,這個其實跟 Fragment#setRetainInstance(true)
持久 Fragment 的機制是相同的。當 Activity 因配置變更而重建時,我們可以將頁面上的資料或狀態可以定義為 2 類:
- 第 1 類 - 配置資料: 例如視窗大小、多語言字元、多主題資源等,當裝置配置變更時,需要根據最新的配置重新讀取新的資料,因此這部分資料在配置變更後便失去意義,自然也就沒有存在的價值;
- 第 2 類 - 非配置資料: 例如使用者資訊、視訊播放資訊、非同步任務等非配置相關資料,這些資料跟裝置配置沒有一點關係,如果在重建 Activity 的過程中丟失,不僅沒有必要,而且會損失使用者體驗(無法快速恢復頁面資料,或者丟失頁面進度)。
基於以上考慮,Activity 是支援在裝置配置變更重建時恢復 第 2 類 - 非配置資料 的,原始碼中存在 NonConfiguration
字眼的程式碼,就是與這個機制相關的程式碼。我將整個過程大概可以概括為 3 個階段:
- 階段 1: 系統在處理 Activity 因配置變更而重建時,會先呼叫
retainNonConfigurationInstances
獲取舊 Activity 中的資料,其中包含 ViewModelStore 例項,而這一份資料會臨時儲存在當前 Activity 的 ActivityClientRecord(屬於當前程序,下文說明); - 階段 2: 在新 Activity 重建後,系統通過在 Activity#onAttach(…) 中將這一份資料傳遞到新的 Activity 中;
- 階段 3: Activity 在構造 ViewModelStore 時,會優先從舊 Activity 傳遞過來的這份資料中獲取,為空才會建立新的 ViewModelStore。
對於 ViewModel 來說,相當於舊 Activity 中所有的 ViewModel 對映表被透明地傳遞到重建後新的 Activity 中,這就實現了恢復 ViewModel 的功能。總結一下重建前後的例項變化,幫助你理解:
- Activity: 構造新的例項;
- ViewModelStore: 保留舊的例項;
- ViewModel: 保留舊的例項(因為 ViewModel 儲存在 ViewModelStore 對映表中);
- LiveData: 保留舊的例項(因為 LiveData 是 ViewModel 的成員變數);
現在,我們逐一分析這 3 個階段的原始碼執行過程:
階段 1 原始碼分析:
Activity.java
```java // 階段 1:獲取 Activity 的非配置相關資料 NonConfigurationInstances retainNonConfigurationInstances() { // 1.1 構造 Activity 級別的非配置資料 Object activity = onRetainNonConfigurationInstance(); // 1.2 構造 Fragment 級別的費配置資料資料 FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
...
// 1.3 構造並返回 NonConfigurationInstances 非配置相關資料類
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.activity = activity;
nci.fragments = fragments;
...
return nci;
}
// 1.1 預設返回 null,由 Activity 子類定義 public Object onRetainNonConfigurationInstance() { return null; } ```
androidx.activity.ComponentActivity.java
```java private ViewModelStore mViewModelStore;
// 1.1 ComponentActivity 在 onRetainNonConfigurationInstance() 中寫入了 ViewModelStore @Override @Nullable public final Object onRetainNonConfigurationInstance() { ViewModelStore viewModelStore = mViewModelStore; // 這一個 if 語句是處理異常邊界情況: // 如果重建的 Activity 沒有呼叫 getViewModelStore(),那麼舊的 Activity 中的 ViewModel 並沒有被取出來, // 因此在準備再一次儲存當前 Activity 時,需要檢查一下舊 Activity 傳過來的資料。 if (viewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { viewModelStore = nc.viewModelStore; } } // ViewModelStore 為空說明當前 Activity 和舊 Activity 都沒有 ViewModel,沒必要儲存和恢復 if (viewModelStore == null) { return null; } NonConfigurationInstances nci = new NonConfigurationInstances(); // 儲存 ViewModelStore 物件 nci.viewModelStore = viewModelStore; return nci; } ```
ActivityThread.java
java
// Framework 呼叫 retainNonConfigurationInstances() 獲取非配置資料後,
// 會通過當前程序記憶體臨時儲存這一份資料,這部分原始碼我們暫且放到一邊。
階段 2 原始碼分析:
Activity.java
```java // 階段 2:在 Activity#attach() 中傳遞舊 Activity 的資料 NonConfigurationInstances mLastNonConfigurationInstances;
final void attach(Context context, ActivityThread aThread, ... NonConfigurationInstances lastNonConfigurationInstances) { ... mLastNonConfigurationInstances = lastNonConfigurationInstances; ... } ```
至此,舊 Activity 的資料就傳遞到新 Activity 的成員變數 mLastNonConfigurationInstances 中。
階段 3 原始碼分析:
androidx.activity.ComponentActivity.java
```java public class ComponentActivity extends androidx.core.app.ComponentActivity implements ContextAware, LifecycleOwner, ViewModelStoreOwner ... {
private ViewModelStore mViewModelStore;
private ViewModelProvider.Factory mDefaultFactory;
// 階段 3:Activity 的 ViewModelStore 優先使用舊 Activity 傳遞過來的 ViewModelStore
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 3.1 優先使用舊 Activity 傳遞過來的 ViewModelStore
NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
// 3.2 否則建立新的 ViewModelStore
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
} ```
Activity.java
``` // 這個變數在階段 2 賦值 NonConfigurationInstances mLastNonConfigurationInstances;
// 返回從 attach() 中傳遞過來的舊 Activity 資料 public Object getLastNonConfigurationInstance() { return mLastNonConfigurationInstances != null ? mLastNonConfigurationInstances.activity : null; } ```
至此,就完成 ViewModel 資料恢復了。
現在,我們回過頭來分析下 ActivityThread 這一部分原始碼:
ActivityThread 中的呼叫過程:
在 Activity 因配置變更而重建時,系統將執行 Relaunch 重建過程。系統在這個過程中通過同一個 ActivityClientRecord 來完成資訊傳遞,會銷燬當前 Activity,緊接著再馬上重建同一個 Activity。
- 階段 1: 在處理 Destroy 邏輯時,呼叫 Activity#retainNonConfigurationInstances() 方法獲取舊 Activity 中的非配置資料,並臨時儲存在 ActivityClientRecord 中;
- 階段 2: 在處理 Launch 邏輯時,呼叫 Activity#attach(…) 將 ActivityClientRecord 中臨時儲存的非配置資料傳遞到新 Activity 中。
ActivityThread.java
```java private void handleRelaunchActivityInner(ActivityClientRecord r, ...) { final Intent customIntent = r.activity.mIntent; // 處理 onPause() performPauseActivity(r, false, reason, null / pendingActions /); // 處理 onStop() callActivityOnStop(r, true / saveState /, reason); // 階段 1:獲取 Activity 的非配置相關資料 handleDestroyActivity(r.token, false, configChanges, true, reason);
// 至此,Activity 中的 第 2 類 - 非配置資料就記錄在 ActivityClientRecord 中,
// 並通過同一個 ActivityClientRecord 重建一個新的 Activity
// 階段 2:在 Activity#attach() 中傳遞舊 Activity 的資料
handleLaunchActivity(r, pendingActions, customIntent);
// 至此,舊 Activity 中的非配置資料已傳遞到新 Activity
}
public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance, String reason) { ActivityClientRecord r = performDestroyActivity(token, finishing, configChanges, getNonConfigInstance, reason); ... if (finishing) { ActivityTaskManager.getService().activityDestroyed(token); } }
// 階段 1:獲取 Activity 的非配置相關資料 // 引數 finishing 為 false // 引數 getNonConfigInstance 為 true ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance, String reason) { ActivityClientRecord r = mActivities.get(token); // 儲存非配置資料,呼叫了階段 1 中提到的 retainNonConfigurationInstances() 方法 if (getNonConfigInstance) { r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances(); } // 執行 onDestroy() mInstrumentation.callActivityOnDestroy(r.activity); return r; }
public Activity handleLaunchActivity(ActivityClientRecord r, PendingTransactionActions pendingActions, Intent customIntent) { final Activity a = performLaunchActivity(r, customIntent); }
// 階段 2:在 Activity#attach() 中傳遞舊 Activity 的資料 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { // 建立新的 Activity 例項 Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); // 建立或獲取 Application 例項,在這個場景裡是獲取 Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 傳遞 lastNonConfigurationInstances 資料 activity.attach(appContext, ..., r.lastNonConfigurationInstances,...); // 清空臨時變數 r.lastNonConfigurationInstances = null; ... } ```
2.5 ViewModel 的資料在什麼時候才會清除
ViewModel 的資料會在 Activity 非配置變更觸發的銷燬時清除,具體分為 3 種情況:
- 第 1 種: 直接呼叫 Activity#finish() 或返回鍵等間接方式;
- 第 2 種: 異常退出 Activity,例如記憶體不足;
- 第 3 種: 強制退出應用。
第 3 種沒有給予系統或應用儲存資料的時機,記憶體中的資料自然都會被清除。而前 2 種情況都屬於非配置變更觸發的,在 Activity 中存在 1 個 Lifecycle 監聽:當 Activity 進入 DESTROYED 狀態時,如果 Activity 不處於配置變更重建的階段,將呼叫 ViewModelStore#clear() 清除 ViewModel 資料。
androidx.activity.ComponentActivity.java
```java public class ComponentActivity extends androidx.core.app.ComponentActivity implements ContextAware, LifecycleOwner, ViewModelStoreOwner ... {
private ViewModelStore mViewModelStore;
private ViewModelProvider.Factory mDefaultFactory;
public ComponentActivity() {
// DESTROYED 狀態監聽
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
mContextAwareHelper.clearAvailableContext();
// 是否處於配置變更引起的重建
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}
} ```
Activity.java
```java boolean mChangingConfigurations = false;
public boolean isChangingConfigurations() { return mChangingConfigurations; } ```
3. ViewModel 的記憶體洩漏問題
ViewModel 的記憶體洩漏是指 Activity 已經銷燬,但是 ViewModel 卻被其他元件引用。這往往是因為資料層是通過回撥監聽器的方式返回資料,並且資料層是單例物件或者屬於全域性生命週期,所以導致 Activity 銷燬了,但是資料層依然間接持有 ViewModel 的引用。
如果 ViewModel 是輕量級的或者可以保證資料層操作快速完成,這個洩漏影響不大可以忽略。但如果資料層操作並不能快速完成,或者 ViewModel 儲存了重量級資料,就有必要採取措施。例如:
- 方法 1: 在 ViewModel#onCleared() 中通知資料層丟棄對 ViewModel 回撥監聽器的引用;
- 方法 2: 在資料層使用對 ViewModel 回撥監聽器的弱引用(這要求 ViewModel 必須持有回撥監聽器的強引用,而不能使用匿名內部類,這會帶來編碼複雜性);
- 方法 3: 使用 EventBus 代替回撥監聽器(這會帶來編碼複雜性);
- 方法 4: 使用 LiveData 的 Transformations.switchMap() API 包裝資料層的請求方法,這相當於在 ViewModel 和資料層中間使用 LiveData 進行通訊。例如:
MyViewModel.java
```java
// 使用者 ID LiveData
MutableLiveData userIdLiveData = new MutableLiveData
// 使用者資料 LiveData LiveData userLiveData = Transformations.switchMap(userIdLiveData, id -> // 呼叫資料層 API repository.getUserById(id));
// 設定使用者 ID // 每次的 userIdLiveData 的值發生變化,repository.getUserById(id) 將被呼叫,並將結果設定到 userLiveData 上 public void setUserId(String userId) { this.userIdLiveData.setValue(userId); } ```
4. ViewModel 和 onSaveInstanceState() 的對比
ViewModel 和 onSaveInstanceState() 都是對資料的恢復機制,但由於它們針對的場景不同,導致它們的實現原理不同,進而優缺點也不同。
- 1、ViewModel: 使用場景針對於配置變更重建中非配置資料的恢復,由於記憶體是可以滿足這種儲存需求的,因此可以選擇記憶體儲存。又由於記憶體空間相對較大,因此可以儲存大資料,但會受到記憶體空間限制;
- 2、onSaveInstanceState() :使用場景針對於應用被系統回收後重建時對資料的恢復,由於應用程序在這個過程中會消亡,因此不能選擇記憶體儲存而只能選擇使用持久化儲存。又由於這部分資料需要通過 Bundle 機制在應用程序和 AMS 服務之間傳遞,因此會受到 Binder 事務緩衝區大小限制,只可以儲存小規模資料。
如果是正常的 Activity 退出,例如返回鍵或者 finish(),都不屬於 ViewModel 和 onSaveInstanceState() 的應用場景,因此都不會儲存和恢復資料。
5. 總結
到這裡,Jetpack 中的 ViewModel 元件就講完了。下一篇文章,我們來討論 LiveData 的替代方案 Flow。關注我,帶你瞭解更多。
參考資料
- ViewModel 概覽 —— 官方文件
- 儲存介面狀態 —— 官方文件
- ViewModel 的 SavedState 模組 —— 官方文件
- ViewModel 和 LiveData:為設計模式打 Call 還是唱反調? —— 官方博文
你的點贊對我意義重大!微信搜尋公眾號 [彭旭銳],希望大家可以一起討論技術,找到志同道合的朋友,我們下次見!
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
- LeetCode 周賽 336,多少人直接 CV?
- LeetCode 周賽 335,純純手速場!
- LeetCode 雙週賽 98,腦筋急轉彎轉不過來!
- Android IO 框架 Okio 的實現原理,到底哪裡 OK?
- 12 張圖看懂 CPU 快取一致性與 MESI 協議,真的一致嗎?
- Android 序列化框架 Gson 原理分析,可以優化嗎?
- 為什麼計算機中的負數要用補碼錶示?
- 什麼是二叉樹?
- 我把 CPU 三級快取的祕密,藏在這 8 張圖裡
- 全網最全的 ThreadLocal 原理詳細解析 —— 原理篇
- 程式設計師學習 CPU 有什麼用?
- WeakHashMap 和 HashMap 的區別是什麼,何時使用?
- 萬字 HashMap 詳解,基礎(優雅)永不過時 —— 原理篇
- Java 面試題:說一下 ArrayDeque 和 LinkedList 的區別?
- Java 面試題:說一下 ArrayList 和 LinkedList 的區別?
- Java 面試題:ArrayList 可以完全替代陣列嗎?
- 已經有 MESI 協議,為什麼還需要 volatile 關鍵字?
- JVM 系列(6)吊打面試官:為什麼 finalize() 方法只會執行一次?
- 使用字首和陣列解決"區間和查詢"問題
- NDK 系列(5):JNI 從入門到實踐,萬字爆肝詳解!