MVP 與 MVVM 優缺點總結
專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)
前言
主要闡述 Android App 架構之 MVP 與 MVVM 的優點與痛點,不介紹具體的實現方式。因為 MVP 架構簡單,無需介紹。而 MVVM 架構相對複雜,核心是 LifecycleOwner、LifecycleObserver、LifecycleRegistry 元件,在此之上,Google 還開發了 DataBinding、ViewModel、LiveData 以實現完整的 MVVM 架構。相關元件已收納至 JetPack Architecture 中。具體實踐可參考本人的開源專案 wanandroid_java
MVP
實際上按照程式碼在 App 中所處位置,描述為 VPM 更合適,即 View -> Presenter -> Model:
這個架構的核心就是 View 與 Presenter 都被抽象成了介面,是面向介面程式設計的一個實踐。因為面向介面,所以實現了依賴隔離,即無需知道具體的 Class 型別。
優點
- 只需定義好 View 與 Presenter 的介面,即可實現 UI 與業務邏輯獨立開發,可由不同的開發團隊分別實現
- 業務邏輯只在 Presenter 中進行維護,遵循了單一職責類的設計原則,提升了程式碼的可維護性
- 介面請求及快取策略只在 Model 中進行維護,同 Presenter 一樣,遵循了單一職責類的設計原則,提升了程式碼的可維護性
- 避免了 MVC 架構中,Controller 類過於龐大的問題。MVP 架構中,View、Presenter、Model 類的體積都不會太大,提升了程式碼的可讀性與可維護性
痛點
其實這並不是 MVP 架構的痛點,而是整個 Android App 開發的痛點,那就是對 UI 的操作必須在 Activity 與 Fragment 的生命週期之內,更細緻一點,最好在 onStart()
之後 onPause()
之前,否則極其容易出現各種異常。而 MVP 架構中,Presenter 對 Activity 與 Fragment 的生命週期是無感知的,所以我們需要手動新增相應的生命週期方法,並進行特殊處理,以避免出現異常或記憶體洩露。
MVVM
實際上按照程式碼在 App 中所處位置,描述為 VVMM 更合適,即 View -> ViewModel-> Model:
這個架構的核心就是 ViewModel 和 LiveData。ViewModel 的作用是保證當裝置因配置改變而重新建立 FragmentActivity(目前 ViewModel 僅支援 FragmentActivity 和 Fragment) 時,資料也不會丟失。LiveData 的作用是保證只在 FragmentActivity 或 Fragment 的生命週期狀態為 [onStarted, onResumed] 時,回撥 onChanged(T data),所以我們可以在 onChanged() 中安全的更新 UI。下面簡單介紹原始碼中是如何實現的:
ViewModel 不重建原理
- 儲存 ViewModel 例項:
/**
* Retain all appropriate fragment state. You can NOT
* override this yourself! Use {@link #onRetainCustomNonConfigurationInstance()}
* if you want to retain your own state.
*/
@Override
public final Object onRetainNonConfigurationInstance() {
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;
}
複製程式碼
- 恢復 ViewModel 例項:
/**
* Perform initialization of all fragments.
*/
@SuppressWarnings("deprecation")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null && nc.viewModelStore != null && mViewModelStore == null) {
mViewModelStore = nc.viewModelStore;
}
// omit the irrelevant codes
...
}
複製程式碼
由上述程式碼可見,在 onCreate() 中,如果 getLastNonConfigurationInstance() 不為 null,則將其中的 viewModelStore 恢復為成員變數 mViewModelStore,而 mViewModelStore 內部快取了 ViewModel 的例項:
public class ViewModelStore {
// key是固定字首加ViewModel實現類的類名,value是實現類例項
private final HashMap<String, ViewModel> 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);
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}
複製程式碼
LiveData 安全更新原理
- 註冊 observer 並監聽 LifecycleOwner 生命週期改變。第一個引數是 LifecycleOwner (一個介面,表示一個類擁有生命週期),目前的實現類有 FragmentActivity 和 Fragment (注:文中 Fragment 皆指 support library 中的 Fragment,目前 Google 已經遷移至 androidx.* 包下,android.app.* 包下的 Fragment 已經被標記為 Deprecated,不推薦使用)
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
// 將入參 observer 封裝為 LifecycleBoundObserver,這是很常見的寫法,也是設計原則之一:多用複合、少用繼承
// 為便於區分,筆者將入參 observer 稱為 inner observer
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
// 註冊到 Lifecycle 中,以監聽生命週期變化
owner.getLifecycle().addObserver(wrapper);
}
複製程式碼
- 能夠監聽生命週期改變的 LifecycleBoundObserver
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull
final LifecycleOwner mOwner;
LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
super(observer);
mOwner = owner;
}
/**
* 只有 STARTED 或者 RESUMED 才會返回 true
*/
@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
/**
* 生命週期改變時,都會回撥
*/
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
// destroy 時,移除 inner observer
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}
@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}
複製程式碼
父類:
private abstract class ObserverWrapper {
final Observer<? super T> mObserver;
boolean mActive;
int mLastVersion = START_VERSION;
ObserverWrapper(Observer<? super T> observer) {
mObserver = observer;
}
abstract boolean shouldBeActive();
boolean isAttachedTo(LifecycleOwner owner) {
return false;
}
void detachObserver() {
}
void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this);
}
}
}
複製程式碼
我們看到,每次生命週期改變時,都會回撥 onStateChanged()。如果是 destroy 就移除 inner observer,否則回撥 activeStateChanged(shouldBeActive()) 將 shouldBeActive() 的返回值賦給 mActive。而 shouldBeActive() 只有在生命週期為 STARTED 或者 RESUMED 時才返回 true,也只有 mActive 為 true時,才會回撥 onChanged:
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}
複製程式碼
因為 LiveData 支援非 UI 執行緒更新 mData ( postValue() 方法),所以引入了版本號以解決 ABA 問題。
優點
- ViewModel:因裝置配置改變導致 Activity 重建時,無需從 Model 中再次載入資料,減少了 IO 操作
- LiveData:更新 UI 時,不用再關注生命週期問題
- Data Binding: 可以有效減少模板程式碼的編寫,而且目前已經支援雙向繫結 (注意:不是所有的 UI 都需要使用 Data Binding,雖然通過 @BindingAdapter 我們真的可以“為所欲為”,最好還是隻用於需要繫結 Bean 類的佈局)
缺點
- 實際編寫 App 過程中,在展示最終資料之前,需要展示過渡 UI,比如一個進度條、一個炫酷的動畫、載入失敗時展示異常提示等,這些可以稱之為 ViewState。在 MVP 架構中,這些狀態的更新被定義在 View 介面中。但是 MVVM 架構沒有涉及。所以我們需要自行封裝一個 ViewData 類,用於封裝資料類和 ViewState,就像這樣:
/**
* [UI State: init->loading->loadSuccess|loadFailure]
* <p>
* Author: Yuloran
* Date Added: 2018/12/20 20:59
*
* @param <T> UI需要的資料型別
* @since 1.0.0
*/
public class BaseViewData<T>
{
@NonNull
private ViewState viewState;
private T viewData;
public BaseViewData(@NonNull ViewState viewState)
{
Objects.requireNonNull(viewState, "viewState is null!");
this.viewState = viewState;
}
public BaseViewData(@NonNull T viewData)
{
this.viewState = ViewState.LOAD_SUCCESS;
this.viewData = viewData;
}
@Nullable
public T getViewData()
{
return viewData;
}
public void setViewData(@NonNull T data)
{
this.viewData = data;
}
@NonNull
public ViewState getViewState()
{
return viewState;
}
public void setViewState(ViewState viewState)
{
this.viewState = viewState;
}
}
複製程式碼
這樣,便可以通過 LiveData 方便的更新 UI了。
-
由於 LiveData 使用的是觀察者模式,所以要避免產生迴圈呼叫。比如介面請求失敗時,通過 LiveData 回撥請求失敗,在回撥中發起新的介面請求。如果介面請求一直失敗,又沒有做特殊處理的話,就產生了迴圈呼叫,這會佔用大量的 CPU 資源,降低 App 的效能。
-
LiveData 在每次收到生命週期改變的回撥時,只要 inner observer 的 mActive 為 true,並且 mLastVersion != mVersion,都會回撥 onChanged(),這在某些場景下會有問題,比如在 onChanged() 中彈出對話方塊,或者跳轉至其它頁面。想象一下這個場景:Activity 因裝置配置改變而重建了,那麼當 Activity 走到 onStart() 時,LiveData 就會回撥 onChanged(),這是因為 Activity 重建後,雖然 ViewModel 沒有重建,但是 LiveData 的 inner observer 卻是重新註冊的,所以這個 observer 的 mLastVersion != mVersion,根據上文原始碼可知,此時一定會回撥 onChanged,進而導致錯誤的彈出了對話方塊或錯誤的跳轉至了其它頁面,使用 Google samples 裡面的 SingleLiveEvent 可以規避這個問題。
-
LiveData 本身是沒有 public 方法的,所以我們應該使用其子類 MutableLiveData。這樣設計,我們就可以在 Model 中使用 MutableLiveData,在 ViewModel 中,只對 View 提供 LiveData,避免 View 去更新 LiveData。本人也寫了一個類 SafeMutableLiveData,用於自動呼叫 setValue() 還是 postValue(),同時為 getValue() 提供一個預設值。
-
有些場景,比如網路連通性監聽,僅需要在確實發生改變時,回撥 onChanged(),但是目前 LiveData 無法實現。在 onCreate() 中,給 LiveData 新增 observer 後,只要走到 onStart(),必然會回撥 onChanged(),並將上一次的 mData 傳送過來,實際上此時並未產生網路連線改變的動作。所以,這種場景還需要特殊處理。
結語
架構設計的目的是提升程式碼的可讀性、可維護性,而不是過度提升程式碼複雜性,更不是隨意引入某某框架。Google 官方也說了,萬能架構是不存在的。如果是已經非常穩定的專案,則沒有必要重新架構。只不過作為開發人員,還是要保持對新技術的敏感性。
附
圖片來自 Google Android 官網