探祕Android黑科技-ViewModel如何無視ConfigurationChange

catsuo發表於2018-06-05

背景介紹

ViewModel是Android Architecture Components中提供的一個元件,其作用是為UI元件提供需要展現的資料內容,幫助開發者更優雅簡單高效的實現多個元件間的資料共享。其中值得注意的是ViewModel的生命週期,以往我們將UI展示的資料直接快取在對應的UI元件中,遇到ConfigurationChange等事件UI元件重新建立,我們快取的資料也隨之銷燬。但ViewModel可以在記憶體中長期被持有而不受ConfigurationChange的影響,直到相關聯的UI元件真正銷燬的時候ViewModel才隨之釋放。下面是來自官方的對ViewModel生命週期的描述圖:

探祕Android黑科技-ViewModel如何無視ConfigurationChange

本文主要探討文章標題提到的問題,關於ViewModel的細節介紹可以參閱官方文件

開門見山

如果堆程式碼不如自己去閱讀原始碼,那樣反而來的更清晰直觀。所以後面的部分不會打原始碼戰術,但是也有一些個人分析的過程描述,如果只關心黑科技是啥,可以直接跳到最後結論部分。

由官方的demo我們可以看到使用ViewModel的簡單方法如下:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}
複製程式碼

我們需要實現一個派生自ViewModel的類。接著在需要的UI元件中通過ViewModelProviders.of().get()來獲取我們定義的ViewModel例項。demo中看到真正使用時往往在ViewModel中以LiveData來封裝需要UI元件展現的資料,於是在獲取我們定義的ViewModel例項後便可以通過其內部的LiveData來新增觀察者,進而實現資料變更時能及時的通知到UI元件。關於LiveData的細節介紹可以參閱官方文件。 我們還是回到ViewModel。就以demo中看到的ViewModelProviders為切入點。

我畫了一張類圖,打算嘗試按照自己的理解將他們的關係講清楚,那也就基本瞭解為什麼ViewModel對ConfigurationChange免疫了。也為大家查閱原始碼或者提出異議提供一個思路。

探祕Android黑科技-ViewModel如何無視ConfigurationChange
檢視原圖

圖中虛線箭頭表示依賴關係,箭頭尾部對應的類依賴箭頭頭部對應的類。 中間的實線箭頭表示類的組合關係,箭頭尾部對應的類中包含箭頭頭部對應的類的例項。

類圖簡述

  • ViewModelProvider依賴ViewModelProviders類來建立ViewModelProvider例項。
  • ViewModelProvider中有一個ViewModelStore型別的成員變數mViewModelStore。ViewModelProviders在建立ViewModelProvider的過程中需要依賴ViewModelStores來建立ViewModelStore例項並賦值給ViewModelProvider的成員變數mViewModelStore。
  • ViewModelStore依賴ViewModelStores類來建立ViewModelStore例項。
  • ViewModelStores建立ViewModelStore的過程中需要依賴HolderFragment的支援,建立好的ViewModelStore儲存在HolderFragment的成員變數mViewModelStore中,通過getViewModelStore()方法返回其內部儲存的ViewModelStore型別物件給ViewModelProvider。
  • 最終通過ViewModelProvider類的get()方法獲取ViewModel例項。

ViewModelProvider例項

ViewModel對ConfigurationChange免疫,換個角度可以理解為在不同的UI元件例項中獲取的ViewModel例項相同。 根據demo得知,我們通過ViewModelProviders.of().get()這樣一個鏈式呼叫來獲取ViewModel例項。 所以要獲得一個ViewModel分為兩個步驟:

第一步,呼叫ViewModelProviders.of()方法獲取一個ViewModelProvider例項。這正是圖中左上部分表達的資訊:

ViewModelProvider依賴於ViewModelProviders,ViewModelProvider是通過ViewModelProviders提供的一系列of方法構造出來的。通過ViewModelProvider的構造方法的定義可以看到在構造時需要傳入ViewModelStore和Factory。ViewModelStore是真正快取ViewModel的地方,Factory則用於在快取未命中時建立ViewModel。

第二步,通過ViewModelProvider.get()方法獲取ViewModel例項,這從圖中的ViewModelProvider和ViewModelStore的關係能有所體現:

ViewModelProvider內部有一個ViewModelStore型別的成員變數mViewModelStore和一個用於建立ViewModel的Factory成員變數mFactory。他們是在建立物件時通過構造方法傳入的。呼叫ViewModelProvider.get()方法其實是通過內部的ViewModelStore.get()方法來獲取ViewModel,而如果獲取為空,則通過mFactory構建一個新的ViewModel。

ViewModelStore中儲存一個Map鍵值對,這裡是真正快取ViewModel的地方,所以ViewModelProvider.get()最終會調到ViewModelStore的這個Map中來找快取的ViewModel。 因為最終是從ViewModelStore的Map鍵值對中查詢快取的ViewModel,所以要保證ViewModel的唯一性其實就是保證ViewModelStore的唯一性。 前面提到ViewModelProvider中的mViewModelStore是先通過ViewModelStores構造好,然後利用其構造方法傳遞到ViewModelProvider中的。所以接下來的重點就是ViewModelStores如何構造一個ViewModelStore。

ViewModelStore例項

ViewModelStore通過ViewModelStores構造,其構造過程需要圖中的ViewModelStores和HolderFragment類一起完成。 從圖中可以看到ViewModelStore是通過ViewModelStores.of()方法來構造的,ViewModelStores.of()方法內部會直接呼叫HolderFragment的靜態方法holderFragmentFor(),該靜態方法返回一個HolderFragment物件,這個HolderFragment物件就是一個派生自Fragment的類,只不過它不包含任何View樹結構。接著呼叫它的getViewModelStore()方法返回HolderFragment的成員變數mViewModelStore,這個mViewModelStore是作為成員變數在HolderFragment類載入時直接new出來的。所以一個HolderFragment物件一定對應唯一一個ViewModelStore物件例項。同時這個mViewModelStore例項會作為前面構造ViewModelProvider時的引數傳進去。也就是說ViewModelProvider和HolderFragment中的mViewModelStore物件指向的是同一個例項,這個例項是先在HolderFragment中構造初始化之後傳遞到ViewModelProvider中去的。 既然ViewModelStore是通過HolderFragment建立出來的,一個HolderFragment例項內部唯一對應一個ViewModelStore例項,那現在的問題就變成如何保證HolderFragment的唯一性了。 到這裡,黑科技終於登場。其實就是利用Fragment的setRetainInstance()方法。通過在例項化HolderFragment的時候呼叫該方法,並傳入true作為引數。HolderFragment就可以在其宿主的Fragment或Activity重走生命週期時在記憶體中持久化其自身例項,即此時Fragment對應的View會從View樹中移除,但是Fragment例項本身不會銷燬,在下一次宿主Fragment或Activity重走生命週期時會複用記憶體中持久化的Fragment例項。以此來達到HolderFragment例項的唯一性。關於setRetainInstance()方法的使用詳情可以參閱官方文件

總結

  • ViewModel要無視ConfigurationChange事件,就得保證在ConfigurationChange事件發生時UI元件生命週期重走過程中獲取到的ViewModel例項與之前的是同一個例項。
  • ViewModel最終會快取在ViewModelStore的Map中,而一個HolderFragment例項與ViewModelStore例項一一對應。所以問題最終回到如何保證HolderFragment的唯一性。
  • HolderFragment派生自Fragment。利用Fragment的setRetainInstance()方法保證HolderFragment在ConfigurationChange事件發生時可以複用記憶體中同一個HolderFragment例項,進而保證HolderFragment的唯一性,達到無視ConfigurationChange的目的。

相關文章