美文网首页Android架构
MVP开发模式漫谈

MVP开发模式漫谈

作者: 波澜步惊 | 来源:发表于2020-05-16 20:29 被阅读0次

    前言

    最近Review公司代码,发现很多人只知MVP模式其名,而不知其原理,滥用Presenter类,有些甚至都没有用P接口,导致项目难以维护。翻阅了网络上很多关于MVP的文章,大多数都没有给出可研究的实例,笔者觉得,写一个"相对标准"版的教程是有必要的。本文将从MVP开发模式的思想,到对MVC远古代码进行改造,一步一步提炼出MVP开发框架。并提供可研究的Github项目Demo

    正文大纲

    • 最早的MVC
    • 后来的MVP
    • 新出炉的MVVM
    • MVP模型图
    • MVP的优势和问题
    • MVP框架Demo解读
      • 完成核心功能
      • 接口/基类抽取
      • 解决Bug
      • 框架代码分离

    正文

    最早的MVC

    最早的开发模式是MVC,

    层级 含义
    M 数据层,纯粹获取数据
    V 视图层,一般指的是xml布局文件
    C 控制层,一般用ActivityFragment

    随着业务扩展,版本迭代,往往会发现一个上千行ActivityFragment,臃肿不堪看起来简直难受,而且难以维护。

    后来的MVP

    层级 含义
    M 依然是数据层,只负责数据的获取.其他一改不管,传入入参和回调函数,M层的代码会调用回掉函数通知上一层
    V 视图层,由之前的xml布局文件,扩充到Activity/Fragment/自定义View/自定义ViewGroup 只要是UI界面相关的东西,全都归类到V层。
    P 新概念,单词 Presenter,翻译为:表现层。功能类似于 粘合剂,它作为中间层,连接数据层M和视图层V, 专门处理 M和V这两口子之间杂七杂八的破事儿。

    M层,很纯粹,前面说过,只负责数据获取以及通知上层。理论上来说,M层可以单独进行测试,检查数据接口是否正常。V层,也很纯粹,V只负责界面元素的调度,它不会去管任何和具体业务相关的事。V层也可以安排假数据单独进行交互性测试。P, 则像个媒人,把两个纯粹的"男女"撮合起来。

    最新出炉的MVVM

    MVP优于MVC,但是随着谷歌发布DatabindingMVVM成了更加新潮的选择,利用数据模型的双向绑定,开发中确实可以节约不少代码量,但是MVVM也有不尽如人意的地方,比如Debug困难, 比如代码侵入XML,比如学习成本较高等等。本文仅提及一下,不作深入讨论。

    MVP模型图

    Activity作为标准的V,它调用P层的方法来执行业务逻辑,P层则调用M层的接口来执行数据获取。

    之后,M层通过P给的callback回调函数,通知PP则根据具体的业务需求,执行VUI调度接口。

    image.png

    MVP的优势与问题

    利用MVP,原来MVCCActivityFragment膨胀的问题解决了。

    但是随之而来的,由于我们用了P层来处理业务逻辑,随着版本迭代,业务量越来越多,旧代码不敢删,新代码越来越多,P的代码也逐渐膨胀,于是又出现了和MVC相似的窘境。

    没事,也有办法,使用接口约束,将大的P类,分成若干个小的P类,保证代码整洁清晰。一段时间之内,可能确实有效果。但是时间长了,会发现P接口很多,P实例类也很多,可能还存在各种错综复杂的继承关系,难以管理,找一个业务的Bug,看到一大堆的P类,脑壳也是很疼的。

    于是 Contract 思想来帮我们解决这个问题。

    所谓Contract,翻译为:合同,契约。它负责来对某一个业务的M V P三层进行统筹管理,如果你要去找一个 业务Bug,很简单,找到该业务的Contract类,所有的M层,V层,P层接口一目了然。

    class XXXContract{
        interface Model{xxx}
        interface View{xxx}
        interface Presenter{xxx}
    }
    

    但是,问题总是源源不断。后续的,还有MVP中的内存泄漏的问题,因为P层要持有V层的引用。以及复杂业务下Contract的管理问题,需要一定的学习研究时间成本。

    而且最关键的是,我们做一个MVP开发框架,是为了什么?是为了立标准立标准是为了团队协作开发的可持续发展,保证项目代码可以健康地发展迭代,而不用为了重构而大费精力。但是,思考一下,一个很小的业务,一定需要我们定MVP三层来实现么?也不一定。

    MVP框架Demo解读

    开发一个MVP框架,除了框架源码之外,还需要一个完整的文档资料,供使用者阅读学习。

    本文Demo地址:https://github.com/18598925736/MvpDemo,建议下载源码对照阅读。

    一般框架的开发设计思路都是,

    • 完成核心功能

      MVP开发框架的核心功能,就是分离 M模型层V视图层P控制层

    • 接口/基类抽取

      对于常用的类,定义统一的抽象类或者接口,来约束该类的行为。这里必须对Activity和Fragment,还有自定义View进行抽取,因为他们都有可能成为 V视图层。 这里可能会用到 泛型约束。

    • 解决Bug

      为了快速实现功能,可能会存在一些隐藏Bug,比如MVP的内存泄漏问题。解决MVP内存泄漏带来的View空指针问题等等。

    • 框架代码分离

      框架代码应该单独存在一个module中,以便团队复用。

    接下来以这4个步骤分别解读, 以一个最简单的登录功能为例:

    完成核心功能

    要分层,首先要找好分层的对象。先来看一段远古MVC代码,标准的C层写法. 其中M层,我使用的是HttpRequestManager,统一管理所有网络请求,V层XML我就不展示了:

    class LoginActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login)
    
            btnLogin.setOnClickListener {
    
                val username = tvUsername.text.trim().toString()
                val password = tvPassword.text.trim().toString()
    
                progressBar.visibility = View.VISIBLE
                HttpRequestManager().doLogin(username, password, object : HttpCallback<UserBean> {
                    override fun onSuccess(result: UserBean?) {
                        progressBar.visibility = View.INVISIBLE
                        dataView.text = result.toString()
                    }
    
                    override fun onFailure(e: Exception?) {
                        progressBar.visibility = View.INVISIBLE
                        dataView.text = e.toString()
                    }
    
                })
            }
        }
    }
    

    众所周知,MVCC层会随着业务无限膨胀。这里我要将其中的业务逻辑抽离出来形成P层。再把 HttpRequestManager 封装到M层。V层 原本是XML。现在把Activity扩充为V层的一部分。

    分层架构有一个铁则:接口分离。分层之后,层级之间,不允许存在类和类的直接依赖关系, 必须通过接口进行约束,要建立依赖,也必须通过接口。具体做法如下:

    先分析M层,网络请求是doLogin,传入参数是字符串类型的usernamepassword,以及一个HttpCallback回调。抽离成 LoginModel , 以及 它的实现类 LoginModelImpl:

    interface LoginModel {
        fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
    }
    
    class LoginModelImpl : LoginModel {
        override fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>) {
            HttpRequestManager().doLogin(username, password, httpCallback)
        }
    }
    

    然后是,V层,如果仅仅考虑UI元素调度的话,V层中必须存在这么几个方法,用接口来约束:

    interface LoginView {
        fun showLoading() // 展示加载中
        fun hideLoading() // 隐藏加载中
        fun onError(msg: String) // 展示报错信息
        fun onSuccess(msg: String) // 展示成功信息
    }
    

    Activity抽离成V层

    class LoginActivity : AppCompatActivity(), LoginView {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login)
            btnLogin.setOnClickListener {}
        }
    
        override fun showLoading() {
            progressBar.visibility = View.VISIBLE
        }
    
        override fun hideLoading() {
            progressBar.visibility = View.INVISIBLE
        }
    
        override fun onError(msg: String) {
            dataView.text = msg
        }
    
        override fun onSuccess(msg: String) {
            dataView.text = msg
        }
    }
    

    然后就是重点 P层,它是 M和V层的连接点,它负责调度M层的业务接口,也负责通知V层更新UI。它目前的核心任务就是,doLogin,用接口来表示:

    interface LoginPresenter {
        fun doLogin(username: String, password: String)
    }
    

    三层已经分离完毕。接下来建立接口依赖。

    先用P层连接V和M.

    class LoginPresenterImpl : LoginPresenter {
    
        var model: LoginModel?
        var view: LoginView?
    
        constructor(view: LoginView) {
            this.view = view
            model = LoginModelImpl()
        }
    
        override fun doLogin(username: String, password: String) {
            val m = model ?: return
            val v = view ?: return
    
            v.showLoading()
            m.doLogin(username, password, object : HttpCallback<UserBean> {
                override fun onSuccess(result: UserBean?) {
                    v.hideLoading()
                    v.onSuccess(result.toString())
                }
    
                override fun onFailure(e: Exception?) {
                    v.hideLoading()
                    v.onError(e.toString())
                }
            })
        }
    }
    

    然后用V连接P:

    class LoginActivity : AppCompatActivity(), LoginView {
        private val presenter: LoginPresenter = LoginPresenterImpl(this)
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login)
            btnLogin.setOnClickListener {
                val username = tvUsername.text.trim().toString()
                val password = tvPassword.text.trim().toString()
                presenter.doLogin(username, password)
            }
        }
        ...
    }
    

    到目前为止:MVP三层之间的关系就变成了下图所示:

    image.png

    至此,MVP三层之间再也没有直接的关联关系,他们想要调用下层,或者通知上层,都必须通过接口。分层完成。

    接口/基类抽取

    这里讨论的基类,主要指的是Activity或者Fragment,至于自定义View,虽然也可以作为View层 的一个实例,但是由于它的声明周期是跟随它所在的FragmentActivity,所以这里不予讨论。另外,M和P层也定义接口来约束行为,哪怕是空接口,也是有必要的,避免业务扩展时猝不及防。

    先看接口抽取

    三个顶层接口

    /**
     * 模型基准接口,
     * 目前是暂时空白
     */
    interface BaseModel {
    }
    
    interface BasePresenter<V : BaseView>  {
    }
    
    /**
     * 规定所有V层 对象共有的行为
     */
    interface BaseView {
    
        /**
         * 显示加载中
         */
        fun showLoading()
    
        /**
         * 隐藏加载
         */
        fun hideLoading()
    
        /**
         * 当数据加载失败时
         */
        fun onError(msg: String)
    
    }
    

    M和P的接口暂时是空,V的接口 BaseView约束一些V层实例所共有的特性,当数据加载时,可能需要显示进度条菊花,当数据加载失败时,可能要做出提示。那数据加载成功时为什么没有定方法呢? 这是因为,数据加载成功之后,界面所要加载的JavaBean不尽相同,我尝试过很多次使用泛型来进行约束,效果都不理想,最后把 onSuccess放到了更下层接口中。 而,interface BasePresenter<V : BaseView> 增加泛型参数,是为了约束实现类所绑定的View层实例,要求View层实例必须实现BaseView

    然后是基类抽取 BaseActivityBaseFragment

    /**
     * Activity 基 类
     * 使用该类创建实体Activity类,必须在泛型中先指定它的 P类
     */
    abstract class BaseActivity<T : BasePresenter<BaseView>> : AppCompatActivity() {
        /**
         * 布局ID
         */
        abstract fun getLayoutId(): Int
    
        /**
         * 界面元素初始化
         */
        abstract fun init()
    
        /**
         * 业务处理类P
         */
        lateinit var mPresenter: BasePresenter<BaseView>
    
        /**
         * P类对象强转, 强转之后才可以在V层使用
         */
        abstract fun castPresenter(): T
    
        /**
         * 綁定业务处理类对象
         */
        abstract fun bindPresenter()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(getLayoutId())
            bindPresenter()
            init()
        }
    }
    
    abstract class BaseFragment<T : BasePresenter<BaseView>> : Fragment() {
        /**
         * 布局ID
         */
        abstract fun getLayoutId(): Int
    
        /**
         * 界面元素初始化
         */
        abstract fun init(view: View)
    
        /**
         * 业务处理类P
         */
        lateinit var mPresenter: BasePresenter<BaseView>
    
        /**
         * P类对象强转, 强转之后才可以在V层使用
         */
        abstract fun castPresenter(): T
    
        /**
         * 綁定业务处理类对象
         */
        abstract fun bindPresenter()
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val root = inflater.inflate(getLayoutId(), container, false)
            bindPresenter()
            init(root)
            return root
        }
    }
    

    abstract class BaseFragment<T : BasePresenter<BaseView>>

    abstract class BaseActivity<T : BasePresenter<BaseView>>

    增加泛型约束是为了在保持三层架构接口隔离的同时,能够调用到 真正的P实例的方法。

    那么,既然 MVP顶层接口Activity基类有了,上面的 LoginView,LoginActivty,LoginModelLoginPresenter, 代码就要变更成这样:

    interface LoginView : BaseView {// 继承BaseView的行为
        fun onSuccess(msg: String)// 并且有自己特有的行为
    }
    
    interface LoginModel : BaseModel {// 继承BaseModel
        fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
    }
    
    interface LoginPresenter : BasePresenter<BaseView> {// 继承BasePresenter
        fun doLogin(username: String, password: String)
    }
    
    class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {// 实现LoginView的所有行为
        // 泛型设为 LoginPresenter
        override fun showLoading() {
            progressBar.visibility = View.VISIBLE
        }
    
        override fun hideLoading() {
            progressBar.visibility = View.INVISIBLE
        }
    
        override fun onError(msg: String) {
            dataView.text = msg
        }
    
        override fun onSuccess(msg: String) {
            dataView.text = msg
        }
    
        override fun getLayoutId(): Int {
            return R.layout.activity_login
        }
    
        override fun init() {
            btnLogin.setOnClickListener {
                val username = tvUsername.text.trim().toString()
                val password = tvPassword.text.trim().toString()
                castPresenter().doLogin(username, password)
            }
        }
    
        override fun castPresenter(): LoginPresenter {
            return mPresenter as LoginPresenterImpl
        }
    
        override fun bindPresenter() {
            mPresenter = LoginPresenterImpl(this)
        }
    }
    

    接口和基类抽取都完成了。那么稍加整理之后,项目结构应该是:

    image.png

    解决Bug

    那么来解决上文提到的霸哥吧

    P层膨胀

    接口过多,P实现类也过多,Contract 思维.

    上代码:

    class LoginContract {
    
        /**
         * 定义数据接口
         */
        interface Model : BaseModel {
            fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
        }
    
        /**
         * 定义View层的界面处理
         */
        interface View : BaseView {
            fun checkParams(): Boolean
            fun handleLoginResult(result: UserBean?)
        }
    
        /**
         * 定义P层的业务逻辑调用
         */
        interface Presenter : BasePresenter<BaseView> {
            fun doLogin(username: String, password: String)
        }
    
        // 这里是不是可以提供静态方法,得到具体的P和M对象
        companion object {
            fun getPresenter(view: View): Presenter {
                return LoginPresenter(view)
            }
    
            fun getModel(): Model {
                return LoginModel()
            }
        }
    
    
    }
    

    一个登录业务,

    • Model层,只需要提供一个doLogin方法即可,参数为字符串类型的usernamepassword

    • View层,则需要校验界面用户名,密码参数是否为空,我提供了一个checkParams方法,并且它需要处理登录之后的回调,我定一个接口 handleLoginResult

    • Presenter层,它是要被View层调用的,我提供一个函数 doLogin, 参数usernamepassword

    此外,提供2个共生体方法,getPresenter() 和 getModel() 只是为了让开发者统一的一个地方来获取M和P。至于V实例,一般都是ActivityFragment,这两个东西,ActivityAMS创建实例的,不用我们多管,想管也管不着,Fragment 则一般会手动去new 或者 用Fragment的静态方法 getInstance。所以此处不提供get方法。这里并不是一定要用共生体静态方法,也可以把Contract类写成单例,这里我节约时间没有这么做。

    有了 LoginContract 类之后,再去 按照这里面约束的M,V,P接口去创建MVP三层的实体类。比如上面的M

    open class LoginModel : LoginContract.Model {
    
        override fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>) {
            HttpRequestManager.doLogin(username, password, httpCallback)
        }
    
    }
    
    open class LoginActivity : BaseActivity<LoginContract.Presenter>(), LoginContract.View {
        ...
    
        override fun init() {
            btnLogin.setOnClickListener {
                if (checkParams()) {
                    val username = tvUsername.text.trim().toString()
                    val password = tvPassword.text.trim().toString()
                    castPresenter().doLogin(username, password)
                } else {
                    onError("有参数为空..")
                }
    
            }
        }
    
        override fun checkParams(): Boolean {
            return tvUsername.text.isNotEmpty() && tvPassword.text.isNotEmpty()
        }
    
        override fun handleLoginResult(result: UserBean?) {
            Log.d("handleLoginResult", result.toString())
            dataView.text = result.toString()
        }
    
        ...
    }
    
    open class LoginPresenter(view: LoginContract.View) : LoginContract.Presenter {
        //P类,持有M和V的引用
        // 为什么我要把 model 放在外面?一个业务类P,只会有一个model么?如果需要多个数据源呢?
        var model: LoginContract.Model? = null
        var view: LoginContract.View? = view
        override fun doLogin(username: String, password: String) {
            val v = view ?: return
            val m = model ?: return
            v.showLoading()
            m.doLogin(username, password, object : HttpCallback<UserBean> {
                override fun onSuccess(result: UserBean?) {
                    v.hideLoading()
                    v.handleLoginResult(result)
                }
    
                override fun onFailure(e: Exception?) {
                    v.hideLoading()
                    v.onError(e.toString())
                }
    
            })
        }
    
        ...
    }
    

    这样就能把一个业务的MVP三层统筹管理。韩信点兵多多益善,兵多不是问题,只要有擅长统兵的将领,我把将领管理好就行。

    这里也是一样,再多的Presenter类,Model类,业务再复杂, 只要管理有方,就不会天下大乱。Contract层正是我们的统兵将领

    MVP复用问题

    有了Contract,问题就真的完全解决了么?非也!

    随着产品锦鲤的脑洞大开,各种奇葩的业务又来了。

    比如:

    之前有一个登录业务,一切都好好的,突然,产品要求,在原来的基础上,增加一个SSSVIP登录的功能,原来的登录业务代码保留,因为普通用户还用得着,SSSVIP爸爸客户们要用尊贵的 登录界面,What the ***!

    之前我们用的MVP开发模式,加入了Contract层 统筹管理所有的MVP三层的所有类。试想一下,是不是每一个业务都需要开辟自己的MVP三层?答案是否定的,比如这里的 SSSVIP登录业务, 99%的业务代码可能都是一摸一样的,唯一不同的就是 登录接口要新增传入一个新的UserType=SSSVIP参数而已,那之前的 登录业务代码还用得着么?

    当然用得着,作为一个有洁癖的程序猿,我们不允许重复代码。请看:

    但是要说一句,每一个独立的业务都有自己的Contract,这是一定的,因为Contract就代表了当前业务的统兵将领SSSVIP登录业务的Contract如下:

    class LoginContract2 {
        interface Model : LoginContract.Model {
            fun doLogin2(
                username: String,
                password: String,
                userType: String,
                httpCallback: HttpCallback<UserBean>
            )
        }
    
        interface View : LoginContract.View {
            fun getUserType(): String
            fun handleLoginResultForSSSVIP(result: UserBean?)
            fun onErrorForSSSVIP(msg: String)
        }
    
        interface Presenter : LoginContract.Presenter {
            fun doLogin2(username: String, password: String, userType: String)
        }
    
        // 这里是不是可以提供静态方法,得到具体的P和M对象
        companion object {
            fun getPresenter(view: View): Presenter {
                return LoginPresenter2(view)
            }
    
            fun getModel(): Model {
                return LoginModel2()
            }
        }
    
    }
    

    上面的代码中,

    内部接口 Model继承自 之前LoginContract.Model,这意味着,之前的登录接口可以复用。

    内部接口View继承自 之前LoginContract.View,之前登录View层的约束不用重复写一遍了。

    内部接口Presenter也继承自 之前LoginContract.Presenter

    继承之后,只需要增加SSSVIP登录所需的特别方法就可以了,前面的逻辑完全复用起来了。

    Model新增的接口:doLogin2() 只是新增了一个userType参数

    View层新增了3个接口。

    • getUserType()获取当前的userType,
    • handleLoginResultForSSSVIP()尊贵的VIP怎么可以和普通用户用一样的登录回调呢?安排起来。
    • 最后的 onErrorForSSSVIP()接口,让SSSVIP的登录报错也与众不同。

    P层,新增一个doLogin2()接口,和原来相比多了一个userType参数,V层调用这个接口把userType传递给P,最终给到M。

    剩下的共生体,没有变化,只是为了让开发者在统一的地方获得M和P的实例。

    Contract的统筹之下,MVP三层复用问题解决了。那么接下来就是SSSVIP登录业务的 MVP三层实例,如何防止重复代码。

    先看:Model

    class LoginModel2 : LoginContract2.Model, LoginModel() {
        override fun doLogin2(
            username: String,
            password: String,
            userType: String,
            httpCallback: HttpCallback<UserBean>
        ) {
            HttpRequestManager.doLogin2(username, password, userType, httpCallback)
        }
    }
    

    它要继承 LoginContract2.Model接口 ,就必须实现 该接口的方法,但是由于 LoginContract2.Model 接口继承了 LoginContract.Model ,原则上在这里必须实现这两个方法 doLogindoLogin2,但是很明显,如果把doLogin再写一遍,那就太low了。解决方式为:在实现 LoginContract2.Model 的同时,继承LoginModel类。这样,即可以实现SSSVIP特有的M层接口,又不用把普通用户的登录Model方法再写一遍。

    剩下的 V和P也是类似:

    class LoginPresenter2(view: LoginContract2.View) : LoginPresenter(view), LoginContract2.Presenter {
    
       override fun doLogin2(username: String, password: String, userType: String) {
           val m = model as LoginContract2.Model // 类型转换成 Login2Activity专用的 Model
           val v = view as LoginContract2.View
           v.showLoading()
           m.doLogin2(username, password, userType, object : HttpCallback<UserBean> {
               override fun onSuccess(result: UserBean?) {
                   v.hideLoading()
                   v.handleLoginResultForSSSVIP(result)
               }
    
               override fun onFailure(e: Exception?) {
                   v.hideLoading()
                   v.onErrorForSSSVIP(e.toString())
               }
           })
    
       }
    
       ...
    }
    

    doLogin不用再写一遍。

    class LoginActivity2 : LoginActivity(), LoginContract2.View {
        override fun getLayoutId(): Int {
            return R.layout.activity_login2
        }
    
        override fun init() {
            super.init()
            // SSSVIP登录
            btnLogin2.setOnClickListener {
                if (checkParams()) {
                    val username = tvUsername.text.trim().toString()
                    val password = tvPassword.text.trim().toString()
                    castPresenter().doLogin2(username, password, getUserType())
                } else {
                    onErrorForSSSVIP("有参数为空..")
                }
            }
        }
    
        override fun getUserType(): String {
            return "SSSSVIP"
        }
    
        override fun handleLoginResultForSSSVIP(result: UserBean?) {
            // 为SSSVIP专门准备的登录结果处理
            Log.d("handleLoginResult", result.toString())
            dataView.text = "尊贵的 ${getUserType()} \n${result.toString()}"
        }
    
        override fun onErrorForSSSVIP(msg: String) {
            dataView.text = "尊贵的${getUserType()} \n$msg"
        }
    
    }
    

    LoginActivity2中,实现了尊贵VIP专享的登录结果回调,以及登录错误提示。

    最后的效果:

    GIF.gif

    MVP三层,代码重用的问题也OK了。

    内存泄漏

    到了这里MVP的所有问题都解决了么?并没有。作为一个MVP架构,P层需要持有V层的引用,如果持有的是Activity,那么当Activity自己finish了自己,如果发现有另外的对象持有了Activity的引用,并且这个对象还是GCRoot(比如它正在执行耗时方法)那么Activity也是不能回收的。这种内存泄漏的问题有很多说法,网上也有很多解决方案,比较常见的就是,定义一个基类BasePresenter,提供一个抽象方法 abstract fun release(),要求所有的子类都去调用它来释放掉 View的引用,然后 在 BaseActivity中去调用这个 release()方法 。 这种做法确实可以 防止内存泄漏,但是随着jetpack开源组件的推广普及,出现了更加简洁的写法,Lifecycle. 使用LifeCycle可以比传统方法更加简洁优雅地处理内存泄漏。

    定义一个基类 BasePresenter, 让他实现LifecycleObserver接口, 然后所有的P类实例就都变成了生命周期的观察者,可以随时接收View层生命周期的变化。当然,目前我们关心的只是onCreateonDestrory,这两个生命周期关系着 view层的绑定和解绑。

    interface BasePresenter<V : BaseView> : LifecycleObserver {
    
        /**
         * 自动感知Activity/Fragment 的 onCreate生命周期,开始初始化一些全局变量
         *
         *
         */
        @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
        fun onCreate()
    
        /**
         * 自动感知Activity/Fragment 的 onDestroy生命周期,释放全局变量
         */
        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        fun onDestroy()
    
    }
    

    依然用LoginPresenter举例:

    open class LoginPresenter(view: LoginContract.View) : LoginContract.Presenter {
    
        //P类,持有M和V的引用
        var model: LoginContract.Model? = null
        var view: LoginContract.View? = view
    
        override fun doLogin(username: String, password: String) {
            ...
        }
    
        override fun onCreate() {
            model = LoginContract.getModel()
        }
    
        override fun onDestroy() {
            model = null
            view = null
        }
    }
    

    一个P类,必须持有modelview的实例,model用来调用数据接口,view用来调用界面元素。上面的代码中,view的绑定,我用构造函数来传递,时机上和onCreate相同。而在view onDestroy的时候,直接让view=null释放引用. 这样,在Activity onDestroy,即将回收的时候,引用链断开了,杜绝内存泄漏。Fragment的处理也是类似。

    当然,还有一个重要步骤,注册观察者到View实例中

    abstract class BaseActivity<T : BasePresenter<BaseView>> : AppCompatActivity() {
        ...
    
        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            lifecycle.addObserver(mPresenter) // 利用 lifecycle 防止内存泄漏
        }
    }
    

    LifeCycle使用非常简单,原理上这里就不细讲了,非本文重点。

    内存泄漏解决之后的空指针问题

    比较容易忽略的是,防止内存泄漏之后的目的达成了,如果在P执行M层数据的过程中,Activity被回收,P类的view成员被置为NULL,就很有可能报出空指针异常,造成崩溃,所以,在P类中,用到view的地方,最好都判空,因为不一定view什么时候会解绑。或者整个方法抛出空指针异常,也可以。比较简单,这个就不举代码实例了。请看下图:

    image-20200516170453865.png

    框架代码分离

    基础框架,和具体的app module的代码,毕竟分属不同的层级。使用时最好是分离到不同的module中。

    image-20200516165859307.png

    比较大团队,或者比较正规的团队,都会把基础框架打包成AAR给公司开发组去引用,或者放置到本地仓库中,使用Gradle来进行依赖,来达成框架共用的目的。分离之后还有一个好处,就是当变更框架时,所有使用到框架的开发者都会感知框架的变化,从而做出调整,让整个团队的开发节奏保持一致,有利于提高开发效率,减少没必要的沟通成本。

    结语

    框架有了,规矩也有有了。如果人人都按照框架来开发,那么项目的维护成本就会大大降低。那是不是每个业务都要按照框来做呢?不能这么绝对,如果实在是一个非常小的页面,很小的功能,使用MVP分层,反而显得有点大材小用。所以,到底用还是不用,应该视情况而定。但是有一点可以肯定,有了框架约束,项目重构起来会更加的顺畅,对团队只有好处没有坏处。

    笔者水平有限,粗略提炼了这个MVP开发框架,github地址为:https://github.com/18598925736/MvpDemo 。如果读者有心的话,可以根据本文思路,对框架进行完善,欢迎Fork 。发现有错误的地方也欢迎批评指正。

    相关文章

      网友评论

        本文标题:MVP开发模式漫谈

        本文链接:https://www.haomeiwen.com/subject/ykorohtx.html