【Android Jetpack教程】ViewModel原理分析

南方吳彥祖_藍斯發表於2021-10-13

ViewModel的定義: ViewModel旨在以注重生命週期的方式儲存和管理介面相關的資料。ViewModel本質上是檢視(View)與資料(Model)之間的橋樑,想想以前的MVC模式,檢視和資料都會寫在Activity/Fragment中,導致Activity/Fragment過重,後續難以維護,而ViewModel將檢視和資料進行了分離解耦,為檢視層提供資料。

ViewModel的特點:

ViewModel生命週期比Activity長

資料可在螢幕發生旋轉等配置更改後繼續留存。下面是ViewModel生命週期圖:

【Android Jetpack教程】ViewModel原理分析

可以看到即使是發生螢幕旋轉,旋轉之後拿到的ViewModel跟之前的是同一個例項,即發生螢幕旋轉時,ViewModel並不會消失重建;而如果Activity是正常finish(),ViewModel則會呼叫onClear()銷燬。

ViewModel中不能持有Activity引用

不要將Activity傳入ViewModel中,因為ViewModel的生命週期比Activity長,所以如果ViewModel持有了Activity引用,很容易造成記憶體洩漏。如果想在ViewModel中使用Application,可以使用ViewModel的子類AndroidViewModel,其在構造方法中需要傳入了Application例項:

public class AndroidViewModel extends ViewModel {    private Application mApplication;    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }    public <T extends Application> T getApplication() {        return (T) mApplication;
    }
}
複製程式碼

ViewModel使用舉例

引入ViewModel,在介紹Jetpack系列文章Lifecycle時已經提過一次,這裡再寫一下:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"複製程式碼

先看執行效果圖:

【Android Jetpack教程】ViewModel原理分析

頁面中有兩個Fragment,左側為列表,右側為詳情,當點選左側某一個Item時,右側會展示相應的資料,即兩個Fragment可以透過ViewModel進行通訊;同時可以看到,當螢幕發生旋轉的時候,右側詳情頁的資料並沒有丟失,而是直接進行了展示。

//ViewModelActivity.ktclass ViewModelActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.e(JConsts.VIEW_MODEL, "onCreate")
        setContentView(R.layout.activity_view_model)
    }
}
複製程式碼

其中的XML檔案:

//activity_view_model.xml<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="
    xmlns:app="
    xmlns:tools="
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".viewmodel.ViewModelActivity">
    <fragment
        android:id="@+id/fragment_item"
        android:name="com.example.jetpackstudy.viewmodel.ItemFragment"
        android:layout_width="150dp"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/fragment_detail"
        app:layout_constraintTop_toTopOf="parent" />
    <fragment
        android:id="@+id/fragment_detail"
        android:name="com.example.jetpackstudy.viewmodel.DetailFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toRightOf="@+id/fragment_item"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>複製程式碼

直接將Fragment以佈局方式寫入我們的Activity中,繼續看兩個Fragment:

//左側列表Fragmentclass ItemFragment : Fragment() {
    lateinit var mRvView: RecyclerView    //Fragment之間透過傳入同一個Activity來共享ViewModel
    private val mShareModel by lazy {
        ViewModelProvider(activity!!).get(ShareViewModel::class.java)
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = LayoutInflater.from(activity)
            .inflate(R.layout.layout_fragment_item, container, false)
        mRvView = view.findViewById(R.id.rv_view)
        mRvView.layoutManager = LinearLayoutManager(activity)
        mRvView.adapter = MyAdapter(mShareModel)        return view
    }    //構造RecyclerView的Adapter
    class MyAdapter(private val sViewModel: ShareViewModel) :
        RecyclerView.Adapter<MyAdapter.MyViewHolder>() {        class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val mTvText: TextView = itemView.findViewById(R.id.tv_text)
        }
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_fragment_left, parent, false)            return MyViewHolder(itemView)
        }
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            val itemStr = "item pos:$position"
            holder.mTvText.text = itemStr            //點選傳送資料
            holder.itemView.setOnClickListener {
                sViewModel.clickItem(itemStr)
            }
        }
        override fun getItemCount(): Int {            return 50
        }
    }
}
複製程式碼
//右側詳情頁Fragmentclass DetailFragment : Fragment() {
    lateinit var mTvDetail: TextView    //Fragment之間透過傳入同一個Activity來共享ViewModel
    private val mShareModel by lazy {
        ViewModelProvider(activity!!).get(ShareViewModel::class.java)
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {        return LayoutInflater.from(context)
            .inflate(R.layout.layout_fragment_detail, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        mTvDetail = view.findViewById(R.id.tv_detail)        //註冊Observer並監聽資料變化
        mShareModel.itemLiveData.observe(activity!!, { itemStr ->
            mTvDetail.text = itemStr
        })
    }
}
複製程式碼

最後貼一下我們的ViewModel:

//ShareViewModel.ktclass ShareViewModel : ViewModel() {
    val itemLiveData: MutableLiveData<String> by lazy { MutableLiveData<String>() }    //點選左側Fragment中的Item傳送資料
    fun clickItem(infoStr: String) {
        itemLiveData.value = infoStr
    }
}
複製程式碼

這裡再強調一下,兩個Fragment中ViewModelProvider(activity).get()傳入的是同一個Activity,從而得到的ViewModel是同一個例項,進而可以進行互相通訊。

上面使用ViewModel的寫法有兩個好處:

螢幕切換時儲存資料

  • 螢幕發生變化時,不需要重新請求資料,直接從ViewModel中再次拿資料即可。

Fragment之間共享資料

  • Activity 不需要執行任何操作,也不需要對此通訊有任何瞭解。
  • 除了 SharedViewModel 約定之外,Fragment 不需要相互瞭解。如果其中一個 Fragment 消失,另一個 Fragment 將繼續照常工作。
  • 每個 Fragment 都有自己的生命週期,而不受另一個 Fragment 的生命週期的影響。如果一個 Fragment 替換另一個 Fragment,介面將繼續工作而沒有任何問題。

ViewModel與onSaveInstance(Bundle)對比

  • ViewModel是將資料存到記憶體中,而onSaveInstance()是透過Bundle將序列化資料存在磁碟中
  • ViewModel可以儲存任何形式的資料,且大小不限制(不超過App分配的記憶體即可),onSaveInstance()中只能儲存可序列化的資料,且大小一般不超過1M(IPC通訊資料限制)

原始碼解析

ViewModel的存取

我們在獲取ViewModel例項時,並不是直接new出來的,而是透過ViewModelProvider.get()獲取的,顧名思義,ViewModelProvider意為ViewModel提供者,那麼先來看它的構造方法:

//ViewModelProvider.javapublic ViewModelProvider(@NonNull ViewModelStoreOwner owner) {    this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
            ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
            : NewInstanceFactory.getInstance());
}public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {    this(owner.getViewModelStore(), factory);
}public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}
複製程式碼

ViewModelProvider建構函式中的幾個引數:

  • ViewModelStoreOwner:ViewModel儲存器擁有者,用來提供ViewModelStore
  • ViewModelStore:ViewModel儲存器,用來儲存ViewModel
  • Factory:建立ViewModel的工廠
private final ViewModelStore mViewModelStore;private static final String DEFAULT_KEY =        "androidx.lifecycle.ViewModelProvider.DefaultKey";@MainThreadpublic <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();    if (canonicalName == null) {        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    }    //首先構造了一個key,直接呼叫下面的get(key,modelClass)
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}@MainThreadpublic <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {    //嘗試從ViewModelStore中獲取ViewModel
    ViewModel viewModel = mViewModelStore.get(key);    if (modelClass.isInstance(viewModel)) {        if (mFactory instanceof OnRequeryFactory) {
            ((OnRequeryFactory) mFactory).onRequery(viewModel);
        }        //viewModel不為空直接返回該例項
        return (T) viewModel;
    } else {        //noinspection StatementWithEmptyBody
        if (viewModel != null) {            // TODO: log a warning.
        }
    }    if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
    } else {        //viewModel為空,透過Factory建立
        viewModel = mFactory.create(modelClass);
    }    //將ViewModel儲存到ViewModelStore中
    mViewModelStore.put(key, viewModel);    return (T) viewModel;
}
複製程式碼

邏輯很簡單,首先嚐試透過ViewModelStore.get(key)獲取ViewModel,如果不為空直接返回該例項;如果為空,透過Factory.create建立ViewModel並儲存到ViewModelStore中。先來看Factory是如何建立ViewModel的,ViewModelProvider建構函式中,如果沒有傳入Factory,那麼會使用NewInstanceFactory:

//介面Factorypublic interface Factory {
   <T extends ViewModel> T create(@NonNull Class<T> modelClass);
}//預設Factory的實現類NewInstanceFactorypublic static class NewInstanceFactory implements Factory {   private static NewInstanceFactory sInstance;   //獲取單例
   static NewInstanceFactory getInstance() {       if (sInstance == null) {
          sInstance = new NewInstanceFactory();
         }        return sInstance;
       }      @Override
   public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {       try {           //反射建立
           return modelClass.newInstance();
       } catch (InstantiationException e) {           throw new RuntimeException("Cannot create an instance of " + modelClass, e);
       } catch (IllegalAccessException e) {           throw new RuntimeException("Cannot create an instance of " + modelClass, e);
       }
   }
}
複製程式碼

可以看到NewInstanceFactory的實現很簡單,直接透過傳入的class反射建立例項,泛型中限制必須是ViewModel的子類,所以最終建立的是ViewModel例項。

看完Factory,接著來看ViewModelStore:

public class ViewModelStore {    private final HashMap<String, ViewModel> mMap = new HashMap<>();    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);        //如果oldViewModel不為空,呼叫oldViewModel的onCleared釋放資源
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }    final ViewModel get(String key) {        return mMap.get(key);
    }    Set<String> keys() {        return new HashSet<>(mMap.keySet());
    }    //遍歷儲存的ViewModel並呼叫其clear()方法,然後清除所有ViewModel
    public final void clear() {        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}
複製程式碼

ViewModelStore類也很簡單,內部就是透過Map進行儲存ViewModel的。到這裡,我們基本看完了ViewModel的存取,流程如下:

【Android Jetpack教程】ViewModel原理分析

ViewModelStore的存取

上面聊了ViewModel的存取,有一個重要的點沒有說到,既然ViewModel的生命週期比Activity長,而ViewModel又是透過ViewModelStore存取的,那麼ViewModelStore又是如何存取的呢?在上面的流程圖中我們知道ViewModelStore是透過ViewModelStoreOwner提供的:

//介面ViewModelStoreOwner.javapublic interface ViewModelStoreOwner {    ViewModelStore getViewModelStore();
}
複製程式碼

ViewModelStoreOwner中介面方法getViewModelStore()返回的既是ViewModelStore。我們上面例子獲取ViewModel時是 ViewModelProvider(activity).get(ShareViewModel::class.java),其中的activity其實就是ViewModelStoreOwner,也就是Activity中實現了這個介面:

//ComponentActivity.javapublic class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,        ViewModelStoreOwner,.... {    @Override
    public ViewModelStore getViewModelStore() {      //Activity還未關聯到Application時,會拋異常,此時不能使用ViewModel
      if (getApplication() == null) {        throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        ensureViewModelStore();        return mViewModelStore;
    }    void ensureViewModelStore() {        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();            if (nc != null) {                //從NonConfigurationInstances中恢復ViewModelStore
                mViewModelStore = nc.viewModelStore;
            }            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
    }    @Override
    //覆寫了Activity的onRetainNonConfigurationInstance方法,在Activity#retainNonConfigurationInstances()方法中被呼叫,即配置發生變化時呼叫。
    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();
        ViewModelStore viewModelStore = mViewModelStore;        if (viewModelStore == null) {        //嘗試從之前的儲存中獲取NonConfigurationInstance
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();          if (nc != null) {              //從NonConfigurationInstances中恢復ViewModelStore
              viewModelStore = nc.viewModelStore;
          }
      }     if (viewModelStore == null && custom == null) {         return null;
     }     //如果viewModelStore不為空,當配置變化時將ViewModelStore儲存到NonConfigurationInstances中
     NonConfigurationInstances nci = new NonConfigurationInstances();
     nci.custom = custom;
     nci.viewModelStore = viewModelStore;     return nci;
 }    //內部類NonConfigurationInstances
    static final class NonConfigurationInstances {
       Object custom;
       ViewModelStore viewModelStore;
   }
}
複製程式碼

NonConfigurationInstances是ComponentActivity的靜態內部類,裡面包含了ViewModelStore。getViewModelStore()內部首先嚐試透過getLastNonConfigurationInstance()來獲取NonConfigurationInstances,不為空直接能拿到對應的ViewModelStore;否則直接new一個新的ViewModelStore

跟進去getLastNonConfigurationInstance()這個方法:

//Activity.javapublic class Activity extends ContextThemeWrapper {
  NonConfigurationInstances mLastNonConfigurationInstances;  public Object getLastNonConfigurationInstance() {      return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
  }  NonConfigurationInstances retainNonConfigurationInstances() {     //onRetainNonConfigurationInstance實現在子類ComponentActivity中實現
     Object activity = onRetainNonConfigurationInstance();
     .......     if (activity == null && children == null && fragments == null && loaders == null
            && mVoiceInteractor == null) {         return null;
     }
     NonConfigurationInstances nci = new NonConfigurationInstances();
     nci.activity = activity;
     ......     return nci;
 }  final void attach(Context context,
        .......,
        NonConfigurationInstances lastNonConfigurationInstances) {
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
   }  static final class NonConfigurationInstances {
      Object activity;
      ......
  }
}
複製程式碼

可以看到getLastNonConfigurationInstance()中是透過mLastNonConfigurationInstances是否為空來判斷的,搜尋一下該變數賦值的地方,就找到了Activity#attach()方法。我們知道Activity#attach()是在建立Activity的時候呼叫的,順藤摸瓜就可以找到了ActivityThread:

//ActivityThread.javafinal ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
Activity.NonConfigurationInstances lastNonConfigurationInstances;//1、將NonConfigurationInstances儲存到ActivityClientRecordActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,        int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    Class<? extends Activity> activityClass = null;    if (r != null) {
        ......        if (getNonConfigInstance) {            try {                //retainNonConfigurationInstances
                r.lastNonConfigurationInstances
                        = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                ......
            }
        }
    }    synchronized (mResourcesManager) {
        mActivities.remove(token);
    }    return r;
}//2、從ActivityClientRecord中獲取NonConfigurationInstancesprivate Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
  ......
  activity.attach(...., r.lastNonConfigurationInstances,....);
}
複製程式碼
  • 在執行performDestroyActivity()的時候,會呼叫Activity#retainNonConfigurationInstances()方法將生成的NonConfigurationInstances賦值給lastNonConfigurationInstances。
  • 在performLaunchActivity()中又會透過Activity#attach()將lastNonConfigurationInstances賦值給Activity.mLastNonConfigurationInstances,進而取到ViewModelStore。

ViewModelStore的存取都是間接在 ActivityThread中進行並儲存在 ActivityClientRecord中。在 Activity配置變化時, ViewModelStore可以在 Activity銷燬時得以儲存並在重建時重新從 lastNonConfigurationInstances中獲取,又因為 ViewModelStore提供了 ViewModel,所以 ViewModel也可以在 Activity配置變化時得以儲存,這也是為什麼 ViewModel的生命週期比 Activity生命週期長的原因了。

作者: 小馬快跑
連結:
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
更多Android技術分享可以關注@我,也可以加入QQ群號:Android進階學習群:345659112,一起學習交流。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2795781/,如需轉載,請註明出處,否則將追究法律責任。

相關文章