1. 程式人生 > 實用技巧 >朝花夕拾:老生常談的MVC、MVP及MVVM深入淺出

朝花夕拾:老生常談的MVC、MVP及MVVM深入淺出

MVP

什麼是MVP?

在瞭解MVP之前可以先觀察MVC的架構模式。

MVC中三個組成部分:1. View,即檢視中的各個控制元件;2. Controller,即Activity、Fragment;3. Model,即資料來源。

但是日常開發中能夠發現,對View層的控制也是在Activity中,這時引入Model層資料來源的獲取再與Controller層發生互動時,不難發現MVC三層互相都存在持有關係,也就產生了嚴重的耦合。

而MVP的架構實現就是將控制層下移,View層充當Activity、Fragment的存在,Model層保持原樣作為資料來源的獲取層存在,而View和Model層的通訊通過中間人Presenter來完成資料的傳遞,通過這樣的方式達到了解耦的目的。

通訊的方式就是互相持有,但中間人對於View層的持有使用弱引用的方式實現,以保證View的及時釋放。

記憶體洩漏

解耦的思想在上面已經有所表述,但解耦的背後還有一個我們非常關注的點 --記憶體洩漏。這個小模組可以分為兩個問題進行闡述:1. 什麼是記憶體洩漏?;2. 使用MVP框架能不能幫我們解決記憶體洩漏的問題?

什麼是記憶體洩漏?

想來這也是老生常談的問題了,簡單了說原本該釋放的東西最後竟然沒有釋放掉,而引起問題可能是一個變數、一個任務等等。

使用MVP框架能不能幫我們解決記憶體洩漏的問題?

其實這個問題我們應該這樣去進行發問MVP框架能不能幫我們解決View層記憶體洩漏的問題?如果使用標題的問題,其實這算是一個錯誤的命題,

那是否能夠解決這樣的問題呢?可以通過一個非常簡單的方法直接進行驗證。下面是一段程式碼示例,一個簡單的非同步執行緒延遲任務。

new Thread(new Runnable() {
    @Override
    public void run() {
        SystemClock.sleep(200000);
    }
}).start();

通過Android Studio整合的Profiler能力,在執行期間就可以直接分析記憶體資訊。

其中有一個打了紅框的按鈕,點選後可以打印出一段時間的記憶體分配情況。 你同樣可以直接點選對某個時間點進行分析。

這是有兩個資訊我們去進行關注,View層、Prensenter層、Model層所佔用的記憶體大小。

  1. View層

  1. Presenter層和Model層

上述的Presenter層中已經開啟了非同步執行緒,能夠明顯發現View層所佔用的記憶體明顯大於Presenter層和Model層。而如果這個時候使得讓我去選擇記憶體洩漏的類,最後的選擇肯定是傾向是少的一方,而MVP給我們帶來了選擇的空間,這也是MVP架構下為我們帶來的一大好處。

這裡也得出了一個標題的結論,MVP架構能夠緩解記憶體洩漏問題,但不能解決它。

手擼一個MVP架構

在MVP架構中,我們會存在兩種程式碼風格,M層或P層做複雜的邏輯處理,選擇其中一層做複雜的邏輯處理,在這裡我更喜歡這些事情由M層負責完成。

M-V-P

首先是Presenter層,這一層作為中間人,和Model層以及View層同時存在通訊,也就需要對兩者同時進行持有,另外為了能夠在View層銷燬不用時,Presenter層能夠不發生記憶體洩漏問題,對於View層引用方法採用的是弱引用的方式書寫。

abstract class BaseMvpPresenter<V: IMvpView, M: IMvpModel> : IMvpPresenter {
    private var vWeakReference: WeakReference<V>? = null
    protected val model: M by lazy { createModel() } 
    override fun bindView(mvpView: IMvpView) {
        vWeakReference = WeakReference(mvpView as V)
    }
    override fun unBindView() {
        if (vWeakReference != null) {
            vWeakReference?.clear()
            vWeakReference = null
        }
    }
    fun getView(): V? = vWeakReference?.get()
    abstract fun createModel(): M
}

接下來是Model層,這一層是資料來源的存在,而資料來源的獲取方法都是使用者自定義,這裡Model層只需要在持有Presenter層的前提下做能力預留即可。

abstract class BaseMvpModel<P: IMvpPresenter> (val p: P): IMvpModel

最後是View層,更具體一點就是Activity、Fragment這些類,一個同樣避不開的話題就是持有,View層同樣需要先對Presenter層進行持有,也就有了如下的初版程式碼。

abstract class BaseMvpActivity<P : BaseMvpPresenter<*, *>> : AppCompatActivity(), IMvpView {
    protected var p: P? = null

    abstract fun getPresenter(): P
}

但是需要思考的一個問題Activity、Fragment什麼時候應該和Presenter層發生通訊呢?為了能夠適應全生命週期的變化,自然最後的考慮就是Activity的onCreate()和onDestroy(),Fragment的onAttach()和onDetach()方法中了。

abstract class BaseMvpActivity<P : BaseMvpPresenter<*, *>> : AppCompatActivity(), IMvpView {
    protected var p: P? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        p = getPresenter()
        p?.bindView(this)
    }
    override fun onDestroy() {
        super.onDestroy()
        p?.unBindView()
    }
    abstract fun getPresenter(): P
}
abstract class BaseMvpFragment<P : BaseMvpPresenter<*, *>> : Fragment(), IMvpView {
    protected var p: P? = null
    override fun onAttach(context: Context) {
        super.onAttach(context)
        p = getPresenter()
        p?.bindView(this)
    }
    override fun onDetach() {
        super.onDetach()
        p?.unBindView()
    }
    abstract fun getPresenter(): P
}

完成以上的一系列步驟,其實已經完成了整個架構的構建,但是如果直接繼承去玩這套框架的時候是不是感覺還欠缺了什麼東西?通訊

建立通訊

在剛剛全部程式碼基礎上,不論Model層、Presenter層還是View層都已經做好了最基礎的事情,就是持有。但是通訊一定需要持有,持有不一定能夠通訊。顯然在現有的程式碼基礎上,通訊設施是當務之急。

這也就引出了新的通訊層Contract,當然它的本名應該說是協議層,就像TCP / UDP啥的,在不同的層次之間引出了這樣一個介面類,他負責的事情就是MVP三層的通訊是什麼樣的。以下便是一段協議層的示例:

interface MainContract {
    interface Model {
        fun execute()
    }
    interface View<T: IMvpModel> {
        fun handleResponse(data: T)
    }
    interface Presenter<T: IMvpModel> {
        fun request()
        fun response(data: T)
    }
}

通過在不同的層次引入這些介面,並完成其具體實現,最後就實現了一套完整的MVP架構。

MVVM

MVVM架構其實和MVP架構整體上相似,但是ViewModel層和View層屬於雙向通訊,使用了DataBinding的能力,使得ViewModel的生成、與View層的繫結完全由系統直接完成簡化了開發的流程。但是從設計上出發的時候,MVP更有利於我們對於整體架構的理解。

入門MVVM

  1. 能力引入
android {
  dataBinding {
        enabled true
    }
}

使用Kotlin程式設計的開發者需要引入kotlin-kapt

  1. 使用

使用方面可以分為兩個小部分:佈局使用、繫結使用

  • 佈局使用

在佈局使用中和平常的XML編寫會有一定的出入,需要使用進行第一層的包裹,而其中的程式碼編寫又可以分為資料區佈局區兩個部分。佈局區和平常的書寫方式保持一致,重點關注資料區,他需要以做第一層包裹,用於標示資料區,是對變數的定義,其中標籤是定義該變數變數名,標籤是定義該變數的型別。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="user"
            type="com.clericyi.android.helper.LoginModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.responseCode}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • 繫結使用

完成佈局繫結以後需要Sync,這一套能力實現和ButterKnife一樣都會產生一個新的Binding檔案,但是這個Binding擁有更為強大的資料繫結能力。另外這個Binding檔案的命名是和XML檔案保持一致的,比如activity_main.xml => ActivityMainBinding,activity_main_1.xml => ActivityMain1Binding。

val ac = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
ac.user = p?.let { LoginModel(it, "200") }

以上一套非常簡單的程式碼過後就已經完成了這套生態下的大部分事項,從開發成本上來講是優於MVP架構的。