MVP 與 MVVM 優缺點總結

Yuloran發表於2019-01-04

專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

前言

主要闡述 Android App 架構之 MVP 與 MVVM 的優點與痛點,不介紹具體的實現方式。因為 MVP 架構簡單,無需介紹。而 MVVM 架構相對複雜,核心是 LifecycleOwner、LifecycleObserver、LifecycleRegistry 元件,在此之上,Google 還開發了 DataBinding、ViewModel、LiveData 以實現完整的 MVVM 架構。相關元件已收納至 JetPack Architecture 中。具體實踐可參考本人的開源專案 wanandroid_java,這是一個 MVVM 架構的最小化實踐,便於理解以及探索 MVVM 架構背後的實現原理。

MVP

實際上按照程式碼在 App 中所處位置,描述為 VPM 更合適,即 View -> Presenter -> Model:

MVP 與 MVVM 優缺點總結

這個架構的核心就是 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:

MVP 與 MVVM 優缺點總結

這個架構的核心就是 ViewModel 和 LiveData。ViewModel 的作用是保證當裝置因配置改變而重新建立 FragmentActivity(目前 ViewModel 僅支援 FragmentActivity 和 Fragment) 時,資料也不會丟失。LiveData 的作用是保證只在 FragmentActivity 或 Fragment 的生命週期狀態為 [onStarted, onResumed] 時,回撥 onChanged(T data),所以我們可以在 onChanged() 中安全的更新 UI。下面簡單介紹原始碼中是如何實現的:

ViewModel 不重建原理

  1. 儲存 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;
    }
複製程式碼
  1. 恢復 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 安全更新原理

  1. 註冊 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);
    }
複製程式碼
  1. 能夠監聽生命週期改變的 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 類的佈局)

缺點

  1. 實際編寫 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了。

  1. 由於 LiveData 使用的是觀察者模式,所以要避免產生迴圈呼叫。比如介面請求失敗時,通過 LiveData 回撥請求失敗,在回撥中發起新的介面請求。如果介面請求一直失敗,又沒有做特殊處理的話,就產生了迴圈呼叫,這會佔用大量的 CPU 資源,降低 App 的效能。

  2. 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 可以規避這個問題。

  3. LiveData 本身是沒有 public 方法的,所以我們應該使用其子類 MutableLiveData。這樣設計,我們就可以在 Model 中使用 MutableLiveData,在 ViewModel 中,只對 View 提供 LiveData,避免 View 去更新 LiveData。本人也寫了一個類 SafeMutableLiveData,用於自動呼叫 setValue() 還是 postValue(),同時為 getValue() 提供一個預設值。

  4. 有些場景,比如網路連通性監聽,僅需要在確實發生改變時,回撥 onChanged(),但是目前 LiveData 無法實現。在 onCreate() 中,給 LiveData 新增 observer 後,只要走到 onStart(),必然會回撥 onChanged(),並將上一次的 mData 傳送過來,實際上此時並未產生網路連線改變的動作。所以,這種場景還需要特殊處理。

結語

架構設計的目的是提升程式碼的可讀性、可維護性,而不是過度提升程式碼複雜性,更不是隨意引入某某框架。Google 官方也說了,萬能架構是不存在的。如果是已經非常穩定的專案,則沒有必要重新架構。只不過作為開發人員,還是要保持對新技術的敏感性。

  1. Lifecycle.State

lifecycle-states

  1. ViewModel Lifecycle

viewmodel-lifecycle

圖片來自 Google Android 官網

相關文章