朝花夕拾:老生常谈的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层所占用的内存大小。
- View层
- 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
- 能力引入
android {
dataBinding {
enabled true
}
}
使用Kotlin编程的开发者需要引入kotlin-kapt
- 使用
使用方面可以分为两个小部分:布局使用、绑定使用
- 布局使用
在布局使用中和平常的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架构的。