Android 面試題:為什麼 Activity 都重建了 ViewModel 還存在?—— Jetpack 系列(3)

語言: CN / TW / HK

theme: jzman

請點贊,你的點贊對我意義重大,滿足下我的虛榮心。

🔥 Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 跟我一起成長。

前言

ViewModel 是 Jetpack 元件中較常用的元件之一,也是實現 MVVM 模式或 MVI 模式的標準組件之一。在這篇文章裡,我將與你討論 ViewModel 實用和麵試常見的知識點。如果能幫上忙請務必點贊加關注,這對我非常重要。


這篇文章是 Jetpack 系列文章第 3 篇,專欄文章列表:

一、架構元件:

二、其他:

  • 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 T get(Class modelClass) { String canonicalName = modelClass.getCanonicalName(); if (canonicalName == null) { throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels"); } // 使用類名作為快取的 KEY return get(DEFAULT_KEY + ":" + canonicalName, modelClass); }

// Fragment @NonNull @MainThread public T get(String key, Class modelClass) { // 1. 先從 ViewModelStore 中取快取 ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { return (T) viewModel; } // 2. 使用 ViewModel 工廠建立例項 viewModel = mFactory.create(modelClass); ... // 3. 儲存到 ViewModelStore mViewModelStore.put(key, viewModel); return (T) viewModel; }

// 預設的 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 { // 雜湊表 private final HashMap mMap = new HashMap<>();

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 ComponentActivity.viewModels( noinline factoryProducer: (() -> Factory)? = null ): Lazy { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory }

return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)

} ```

ViewModelLazy.kt

```kotlin public class ViewModelLazy ( private val viewModelClass: KClass, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory ) : Lazy { private var cached: VM? = null

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 // 對映表 private final HashMap mViewModelStores = new HashMap<>();

@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。關注我,帶你瞭解更多。


參考資料

你的點贊對我意義重大!微信搜尋公眾號 [彭旭銳],希望大家可以一起討論技術,找到志同道合的朋友,我們下次見!

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿