热门技术

MVVM漫谈

2020-06-05  本文已影响0人  波澜步惊

前言

在开发模式的演进过程中 MVC,MVP,MVVM一一登上舞台。但是并不意味着MVVM一定就比MVC MVP优秀。不同的项目有不同的体量,开发中要根据项目体量选择合适的开发模式。

市面上介绍mvvm的项目不在少数,但是看了很多,都在介绍源码原理,开发中的踩坑过程,而且有的是过时的资料,却很少见到能够直接从项目需求入手帮助不熟悉MVVM的开发者从入门到熟悉原理,再到框架优化的。这个空缺我来补充,如果提及到一些其他原理性的东西不便长篇展示,我会引用链接,或者引用他文的原文加以说明。

特别说明:

以下所有信息都基于截止到 2020年6月2日10点23分的最新官方资料和源码版本。如果存在任何历史版本的差异,本文不会过分纠结。

正文大纲

正文

有关MVVM的几个重要组件

ViewModel

ViewModel 是androidx包中抽象类。它是谷歌开放给全世界开发者用来改善项目架构的一个组件。

image.png

既然是探索ViewModel的本源,那就从它的官方注解开始吧。

image.png

这段话的大意是:

ViewModel是一个准备和管理Activity和Fragment的数据的类。它也可以掌控Activity、Fragment和应用中其他部分的通讯。

一个ViewModel总是关联到一个域(Activity或Fragment)被创建。并且只要域是存活的,ViewModel就会一直被保留。比如。如果域是一个Activity,ViewModel就会存活,直到Activity被finish。

换句话说,这意味着,如果它的持有者由于配置改变而被销毁时(比如屏幕旋转),ViewModel并不会被销毁。新的持有者实例,将会仅仅重新连接到已经存在的ViewModel。

ViewMode存在的目的,就是为Activity/Fragment 获得以及保留 必要信息。 Activity / Fragment 应该可以观察到VIewModel的变化,ViewModel通常通过LiveData或者DataBinding 暴露信息。你也可以你自己喜欢的使用可观察的结构框架。

ViewModel仅有的职责,就是为UI管理数据,它不应该访问到你任何的View层级 或者 持有Activity 、Fragment的引用。

谷歌爸爸其实已经把意思讲的很明白,上面一段话中有几个重点:

核心功能

ViewModel的核心功能:在适当的时机执行回收动作,也就是 onCleared() 函数释放资源。而这个合适的时机,可以理解为 Activity销毁,或者Fragment解绑。

借用一张图来解释,就是:

image.png

在整个Activity还处于存活状态时,ViewModel都会存在。而当Activity被finish的时候,ViewModel的onCleared函数将会被执行,我们可以自己定义函数内容,清理我们自己的资源,在Activity被销毁之后。该ViewModel也不再被任何对象持有,下次GC时它将被GC回收。

基本用法

创建一个新的项目,定义我们自己的UserModel类,继承ViewModel:

import android.util.Log
import androidx.lifecycle.ViewModel

class UserModel : ViewModel() {

    init {
        Log.d("hankTag", "执行ViewModel必要的初始化")
    }
    override fun onCleared() {
        super.onCleared()
        Log.d("hankTag", "执行ViewModel 清理资源")
    }

    fun doAction() {
        Log.d("hankTag", "执行ViewModel doAction")
    }
}

在View层使用定义好的ViewModel:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取ViewModel对象
        val userModel = ViewModelProvider(this).get(UserModel::class.java)
        // 使用ViewModel对象的函数
        userModel.doAction()
    }
}

就这么简单,运行程序能看到日志:

image.png

同时ViewModelProvider也支持两个参数的构造函数,除了上面的owner=this之外,还可以传入另一个Factory参数。

如果不传入这个Factory,源码中会在拿到ViewModel的class对象之后通过无参构造函数进行反射创建对象。但是如果ViewModel要用有参构造函数来创建的话,那就必须借助Factory:

// ViewModel
class UserModel(i: Int, s: String) : ViewModel() {

    var i: Int = i
    var s: String = s

    init {
        Log.d("hankTag", "执行ViewModel必要的初始化")
    }

    override fun onCleared() {
        super.onCleared()
        Log.d("hankTag", "执行ViewModel 清理资源")
    }

    fun doAction() {
        Log.d("hankTag", "执行ViewModel doAction: i = $i, s : $s")
    }

}

// ViewModelFactory
class UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(Int::class.java, String::class.java).newInstance(i, s)
    }
}
// View层
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取ViewModel对象
        val userModel = ViewModelProvider(this, UserModelFactory(1, "s")).get(UserModel::class.java)
        // 使用ViewModel对象的函数
        userModel.doAction()
    }
}

运行结果:

06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel必要的初始化
06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel doAction: i = 1, s : s
06-02 11:20:57.836 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel 清理资源

核心原理

源码探索的目标是,ViewModel是如何感知Activity的生命周期清理自身资源的。其实也就是看 onCleared函数是如何被调用的。

image.png

上面标重点的注释的意思是:创建一个ViewModelProvider,这将会创建ViewModel并且把他们保存到给定的owner所在的仓库中。这个函数最终调用了重载的构造函数:

image.png

这个构造函数有两个参数,一个store,是刚才通过owner拿到的,一个是,Factory。store顾名思义,是用来存储ViewModel对象的,而Factory的意义,是为了通过class反射创建对象做准备的。

使用构造函数创建出一个ViewModelProvider对象之后,再去get(UserModel::class.java)

image.png

通过一个class对象,拿到他的canonicalName全类名。然后调用重载get方法来获取真实的ViewModel对象。

image.png

这个get函数有两个参数,其一,key,字符串类型。用于做标记,使用的是一个定死的字符串常量DEFAULT_KEY拼接上modelClass的全类名,其二,modelClass的class对象,内部代码会使用class进行反射,最终创建出ViewModel对象。

上面提到了一个重点:Store仓库,创建出来的ViewModel都会被存入owner所在的仓库。那么,阅读仓库的源码:

image.png

那么一个Activity,它作为ViewModelStoreOwner,他自己的viewModelStore何时清理?

image.png

答案是:onDestroy() . 但是这里有一个特例,配置改变,比如屏幕旋转时,ViewModelStore并不会被清理。并且,Fragment的源码中也有类似的调用:

image.png

总结

ViewModel的核心,是自动清理资源。我们可以重写onCleared函数,这个函数将会被ViewModel所在的Activity/Fragment 执行onDestory的时候被调用,但是当屏幕旋转的时候,并不会清理。在ViewModel的架构中,有几个关键类,


DataBinding

DataBinding,单词意思: 数据绑定,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel层。DataBinding 能够省去我们一直以来的findViewById() 步骤,大量减少Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常.

DataBinding

ViewModel 的注释中我们得知,DataBinding是向View层暴露ViewModel的一种方式。但是事实上并非如此,DataBinding只是数据绑定,它和ViewModel抽象类没有半毛钱关系。DataBinding绑定的双方:是 数据(别多想,就是纯粹的数据,不涉及到生命周期视图。而MVVM的核心是ViewModel抽象类,核心功能是感知持有者Activity/Fragment的生命周期来释放资源,防止泄露。我们使用DataBinding,创建封装数据类型,也不用继承ViewModel抽象类。至于ViewModel抽象类的注释上为什么这么说,我也是很费解。但是看了许多DataBinding的资料,项目,包括在自己的项目中使用DataBinding之后,它给我的感受就是:很糟糕。没错,糟透了,也许是因为时代进步了,也许是因为我的代码洁癖,DataBinding放入我的代码,我总感觉有一种黏乎乎的感觉,就和最早的JSP一样,一个HTML文件中,混入了HTML标签,js代码,以及 java代码,尽管我承认DataBinding的功能很强大,但是使用起来确实不舒服。有一些老代码如果大量使用了这种写法,我们了解一些DataBinding核心原理也是有必要的。

核心功能

DataBinding的核心功能是:支持View和数据的单向或者双向绑定关系,并且最新版源码支持 setLifecycleOwner 设置生命周期持有者

基本用法

在所在module的build.gradle文件中,找到androd节点:插入以下代码来开启DataBinding

dataBinding{
    enabled true
}

改造布局xml,使用<layout></layout>标签包裹原来的布局,并且插入<data>节点

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <import type="androidx.databinding.ObservableMap" />
        <import type="androidx.databinding.ObservableList" />

        <variable
            name="userBean"
            type="com.zhou.databinding.UserBean" />
        <variable
            name="map"
            type="ObservableMap<String, Object>" />

        <!-- 首先在这里定义,然后才能在代码中使用 -->
        <!-- 定义顶层字段 -->
        <variable
            name="title"
            type="java.lang.String" />
    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <!-- 双向绑定 -->
        <TextView
            android:id="@+id/tvTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={title}"
            android:textSize="30sp"
            tools:text="title:" />
        <!-- 单向绑定 -->
        <TextView
            android:id="@+id/tvTitle2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{title}"
            android:textSize="30sp"
            tools:text="title:" />
         <...></...>
    </LinearLayout>
</layout>

这里支持import操作,类似java的import导包,导入之后就能在@{} 中使用引入之后的函数和类. 如果想双向绑定,就使用@={}。Varilable标签是用来定义数据的,name随意,字符串即可。type必须是封装类型的全类名,支持泛型实例。

Java/kotlin 代码层面:

数据的绑定支持几乎所有类型,包括jdk,sdk提供的类,或者可以自定义类:

class UserModel  {

    val user = User("hank001", "man")

}

对,这里命名为UserModel,但是它和androidX里面的抽象类ViewModel没有半毛钱关系。

Activity中,需要使用DataBindingUtil将当前activity与布局文件绑定。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.title = "asfdsaf"

        val map = ObservableArrayMap<String, Any>().apply { put("count", 0) }
        binding.map = map

        binding.userBean = UserBean()

        Thread {
            for (i in 0..100) {
                binding.title = "asfdsaf$i" // 数据变更时,UI会发生变更
                map["count"] = "count$i"

                Thread.sleep(10)
            }
        }.start()

    }
}

上面的代码,如果运行起来,

GIF.gif

可以看到我并未主动去使用textview的引用去操控它的text属性。这些工作都是在databinding框架中完成的。至于更具体更复杂的用法,本文不再赘述。网上很多骚操作。

核心原理

核心功能是 数据绑定,也就是说,只要知道了databinding是如何在数据变化时,通知到view让它改变属性的,databinding的秘密就算揭开。直接从代码进入源码。这一切的源头,都是由于我们使用了DataBindingUtil来进行绑定引起的。那么就从它开始。

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.title = "asfdsaf"
image.png

注释的大意是:将Activity的内容View设置给 指定layout布局,并且返回一个关联之后的binding对象。指定的layout资源文件不能是merge布局。

随后该函数调用到了:

image.png

这里首先使用 activity.setContentView,将layoutId设置进去,常规操作。然后,拿到activitydecorView,进而拿到contentView,随后调用bindToAddViews

image.png

继续追踪bind函数:

image.png

目标转移到了sMapper.getDataBinder(),进去看了之后发现是抽象类,找到他的实现类:

image.png

结果发现了我自己的包名,看到这里应该有些明白了,我并没有写这个DataBindingMapperImpl类,它只能是as帮我们自动生成的。

image.png

所以,绑定View和数据的具体代码应该在这个类里面有答案,经过追踪,发现代码走到了这一行:

image.png

回到一开始,

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

这句话,饶了一大圈,最终得到了一个ActivityMainBindingImpl对象,随后我们用这个对象去操作view引用来绑定数据

binding.title = "asfdsaf"

那就直接从这个title看起,上面是kotlin的setTitle写法,直接看setTitle方法:

image.png

其实它就是把xml布局文件中的 title属性值设置为 传入的形参值。然后 notifyPropertyChanged(BR.title): 通知 ID为 BR.title的属性值发生了改变。

image.png 直接进到了观察者模式Observable接口的一个实现类BaseObservable,由于as的原因,代码无法继续索引(它会直接跳到xml文件),但是经过debug,我发现,当title发生变化时, image.png

从上面的命名可以看出,DataBinding框架应该是给每一个xml中定义的变量variable都建立了一个独立的监听器,在variable发生变化时,这个监听器会在 variable 发生改变时,通知界面元素发生属性变更。,查找这个监听器的调用位置 executeBinding()函数,结果有了意外发现,“双向数据绑定”的原理也被揭开。

image.png

这里传了3个参数,BeforeTextChanged,OnTextChanged,AfterTextChanged,刚好对应了TextWatcher接口中的3个方法。进入看一眼上面的setTextWatcher():

image.png

在 textView的内容发生变更的时候,也会执行到监听器的onChange函数,进行数据变更。

上面的追踪仅仅是针对

<variable
            name="title"
            type="java.lang.String" />

这一种定义databinding变量的方式。如果是map类型,map的内部元素发生变更,UI也是可以随之更新的,又是怎么回事呢?

        <variable
            name="map"
            type="ObservableMap<String, Object>" />

经过一番追踪,发现有点不同。map的用法有点不同:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(map.count)}"
    android:textSize="30sp"
    tools:text="userBean:" />

当我在activity中变更map的元素值的时候,

image.png

会执行到setMap方法:

image.png

map的某一个元素值发生变化时,会执行到handlerFieldChange

image.png

随后 onFieldChange函数,

image.png

如果确认发生变更,就会requestRebind()重新去绑定最新的map对象。

补充说明一点:

binding.lifecycleOwner = this

这句代码,

image.png

如果一个DataBinding对象的mLifeCycleOwner不是空,那么:

image.png

在绑定数据的时候,就会去判定当前mLifeCycleOwner是不是STARTED之后,如果不是,数据的绑定都不会执行

总结

综合以上所有小结论,总结一下:

LiveData

时代发展了,DataBinding的替代品来的很快。随着Kotlin的兴起,DataBinding可以减少findViewById的优势不复存在,而LiveData的发布,又为MVVM提供了一种新的可能。

LiveData, 意为: 存活的数据。我们还是从权威的官方注释开始:

image.png

大意翻译为:

LiveData是数据的持有者类,它可以被给定的"lifeCycleOwner"来观察。

这意味着,一个"observer"可以连同一个 lifeCycleOwner 被添加到一个pair中, 并且,当且仅当lifeCycleOwner 处于存活状态时,观察者将会被通知到封装数据的变动。当 lifeCycleOwner 的状态时 STARTED或者RESUMED时,它会被认为时存活状态。 通过observerForever(observer)添加进去的观察者,会被认为总是处于存活状态,这样它就会永远被通知数据的变动。对于这样的观察者,我们应该手动调用 removeObserver(observer)

连同Lifecycle被添加进去的观察者,如果相关的lifecycle转移到DESTORY状态时,将会被自动移除。这对于Activity/Fragment将会十分有用,他们可以安全地观察LiveData,并且不用担心泄露。

另外,LiveData拥有 onActive()和onInactive()方法来使得当存活的观察者数量在0和1之间变化时被通知到。这将允许LiveData在它没有任何存活观察者的时候释放重量级资源。

这个类被设计用来持有ViewModel的个别数据字段,但是它也可以被用来实现一个application中多个module之间的解耦的共享数据。

关键词重点:

之所以选择用LiveData来实现MVVM,是因为 LiveData的代码足够纯粹,纯java/kotlin代码,不再像databinding那样黏乎乎,并且 在ViewModel抽象类的官方注释上,也推荐了LiveData的标准写法,写法足够简单实用。

核心功能

LiveData,其实他本身就是数据,一个可以被观察的活着的数据。它的唯一职责,就是提供观察接口给View,让View可以感知它的变化

基本用法

LiveData作为给View层暴露ViewModel的一种方式,它一般要配合ViewModel来组成MVVM架构。

但是它也可以单独使用,下面的案例用纯粹的方式(不涉及到MVVM)来展示LiveData的用法:

// javabean
class User(var name: String, var sex: String) {

    override fun toString(): String {
        return "[name:$name,sex:$sex]"
    }
}
// Livedata的管理类,虽然名字也叫xxModel,但是和ViewModel没有任何关系
class UserModel {

    val userLiveData = MutableLiveData<User>() // LiveData除了可以发送数据之外,还可以缓存数据(参见setValue getValue)

    private var seri = 0

    init {
        userLiveData.postValue(User("hank$seri", "male"))
    }

    fun loadUser() {
        userLiveData.postValue(User("hank${++seri}", "male"))//post一次之后便会缓存起来
    }
}
// View层Activity 
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val user = UserModel()
        // 让界面元素通过观察者来感知数据的变化
        user.userLiveData.observe(this, Observer<User> {
            Log.d("hanTag", "MainActivity:侦测到User发生变化$it")
            textView.text = it.toString()
        })
        
        // 通过textView的点击事件来主动触发数据的变化
        textView.setOnClickListener {
            Log.d("hanTag", "MainActivity:主动触发User的变化,可能是触发网络请求")
            user.loadUser()
        }

    }
}

以上代码仅作示例。

以上代码有3个部分,一个javaBean,一个定义LiveData的管理类UserModel(虽然名字是xxModel,但是和ViewModel没有任何关系),还有一个是View层Activity.

运行效果:

GIF-1591150885936.gif

核心原理

LiveData的核心功能,是把数据的变化通知给观察者,也就是这行代码

val userLiveData = MutableLiveData<User>()
userLiveData.postValue()// 入口代码

那么进入源码:

image.png

它实际上调用的是父类的postValue()

image.png

这段注释的意思大概是:

发送一个任务给主线程去设置给定的value。如果你在主线程中执行了如下代码:

liveData.postValue("a")

liveData.setValue("b")

这个value值 b 将会被首先设置,并且随后主线程将会用a覆盖它。

如果你在主线程执行post任务之前多次调用这个方法,那么只有最后一个value才会被分发。

接着往下看,方法内容:

image.png

这里进行了一系列判定,规避了无需执行任务的情况.

ArchTaskExecutor是一个单例类,用来在主线程中执行任务,细节无需关心。

来看看 mPostValueRunnable

image.png

这个runnable,其实就做了两件事,1,传递刚刚更新的mPendingDatanewValue,然后还原mPendingData。2,将newValue值继续往下传输。

setValue做了什么

image.png

发现了重点,这个函数的注释上说明,如果存在存活状态的观察者,将会把这个value值分发给他们。

image.png

继续观察considerNotify()方法:

image.png

可以看到,一个observer本身就有是否存活的状态值 mActive. 如果判定存活,就继续往下走程序,

这里我得到几个关键信息:

接下来的重点就转移到了ObserverWrapper这个类,它是 LiveData的内部抽象类:

image.png

这个类,在我们去注册观察者的时候其实就用到了:

image.png image.png

那它作为一个观察者,他自己的存活状态mActive是由什么决定的呢?

image.png

是ObserverWrapper自己的 activeStateChanged(). 这个函数的调用有4处,但是经过Debug追踪,最终锁定一处:

image.png

LifecycleBoundObserver类的onStateChange函数中,这里已经说的很明白了,上面的代码解读一下:

OK.到此为止,作结论:

总结

LiveData之所以可以仅在观察者存活的时候通知到多个观察者,是因为:

LiveDataBus

LiveDataBus基于LiveData, 是一种在application全局范围内的事件总线的新方案,当然这个并不是谷歌直接给的,在此之前,可能存在EventBusRxBus这种写法,但是他们都有内存泄漏的问题。而使用基于LiveDataLiveDataBus,将不会有类似的问题。

必须提到的是,原始的MutableLiveData存在多次通知的问题,解决方式也不止一种,后文再细讲。

核心功能

负责Activty/Fragment之间,多个Activity之间,以及FragmentActivity之间的数据通信。

基本用法

由于它不是谷歌提供的,是开发者根据需求创造的,所以这里我给出参考源码,实际开发中每个人的写法都可能不同:

package com.zhou.baselib.bus

import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean

class LiveDataBus {

    /**
     * 只有当前存活的owner才能收到消息, 即,尚未创建成功的owner将不会收到消息 ,
     * 即使它进行了监听
     */
    private val mapOfAliveOwner: HashMap<String, AliveOwnerMutableLiveData<Any?>> = HashMap()

    /**
     * 全部owner都能收到消息,就算将要跳转的Activity并没创建成功的情况
     */
    private val mapOfAllOwner: HashMap<String, MutableLiveData<Any?>> = HashMap()

    /**
     *
     */
    private val mapOfSingleEventOwner: HashMap<String, SingleLiveData<Any?>> = HashMap()

    // 内部类的单例写法,静态成员天生就是线程安全的
    class SingleHolder {
        companion object {
            val DATA_BUS = LiveDataBus()
        }
    }

    companion object {
        // 提供给外界一个get方法来获取单例对象
        fun get(): LiveDataBus {
            return SingleHolder.DATA_BUS
        }
    }

    /**
     * 获取消息通道, 仅支持当前存活的 lifeCycleOwner
     *
     * @key 消息通道的key
     */
    fun getAliveOwnerChannel(key: String): MutableLiveData<Any?> {
        if (!mapOfAliveOwner.containsKey(key)) {
            mapOfAliveOwner[key] = AliveOwnerMutableLiveData()
        }
        return mapOfAliveOwner[key]!!
    }

    /**
     * 获取默认消息通道,  支持所有lifeCycleOwner,包括目前还没创建成功的
     */
    fun getDefaultChannel(key: String): MutableLiveData<Any?> {
        if (!mapOfAllOwner.containsKey(key)) {
            mapOfAllOwner[key] = MutableLiveData()
        }
        return mapOfAllOwner[key]!!
    }

    fun getSingleEventChannel(key: String): SingleLiveData<Any?> {
        if (!mapOfSingleEventOwner.containsKey(key)) {
            mapOfSingleEventOwner[key] = SingleLiveData()
        }
        return mapOfSingleEventOwner[key]!!
    }
}

/**
 * 只有当前存活的lifeCycleOwner才会收到 消息, 重写它的observer的mLastVersion
 */
class AliveOwnerMutableLiveData<T> : MutableLiveData<T>() {

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, observer)
        hook(observer)
    }

    private fun hook(observer: Observer<in T>) {
        val classLiveData: Class<LiveData<*>> = LiveData::class.java
        val fieldObservers = classLiveData.getDeclaredField("mObservers")
        fieldObservers.isAccessible = true
        val mObservers = fieldObservers[this]
        val classObservers: Class<*> = mObservers.javaClass

        val methodGet = classObservers.getDeclaredMethod("get", Any::class.java)
        methodGet.isAccessible = true
        val objectWrapperEntry = methodGet.invoke(mObservers, observer)
        val objectWrapper =
            (objectWrapperEntry as Map.Entry<*, *>).value!!
        val classObserverWrapper: Class<*>? = objectWrapper.javaClass.superclass

        val fieldLastVersion =
            classObserverWrapper!!.getDeclaredField("mLastVersion")
        fieldLastVersion.isAccessible = true
        val fieldVersion = classLiveData.getDeclaredField("mVersion")
        fieldVersion.isAccessible = true
        val objectVersion = fieldVersion[this]
        fieldLastVersion[objectWrapper] = objectVersion
    }

}

/**
 * 如果希望创建一个消息通道,只允许通知一次,那就使用SingleLiveEvent
 *
 * @param <T> the data type
 */
class SingleLiveData<T> : MutableLiveData<T>() {
    private val mPending = AtomicBoolean(true)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.d(
                "SingleLiveEvent",
                "Multiple observers registered but only one will be notified of changes."
            )
        }
        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {// 获取bool值,并且在获取之后将它的值设置成false
                observer.onChanged(t)
            }
        })
    }

}

以Activity之间的通信为例:

核心原理

使用上述LiveDataBus类,只需要做两件事,

其一:获得消息通道 LiveDataBus.get().getXXXChannel("xxxx") 然后 observer(this,Observer{...})进行监听注册

其二:获得消息通道 LiveDataBus.get().getXXXChannel("xxxx") ** 然后 postValue(xxx) 发送消息

上面的LiveDataBus类提供了3种消息通道,

LiveDataBus本身的设计,就是基于 订阅,发布者模式,和Rxjava如出一辙,但是由于利用到了LiveData,LifeCycle,它纯天然就带有生命周期感知机制,无需我们操心去做更多防止内存泄漏的工作。但是,LiveDataBusRxJava有一个共同之处,那就是支持粘性消息,即使 订阅者对象尚未创建,待到创建成功之后,也能收到。

进入源码, 本次探索源码,分析的目标是:

为何使用默认的MutableLiveData时,它支持粘性消息呢?

从发布者入手:

LiveDataBus.get().getDefaultChannel("Main2Activity").postValue("发送给第二个Activity的消息")

postValue之后,追踪到LiveData类的mPostValueRunnable...

image.png

继续往下,进入到setValue

image.png image.png image.png

observer.mActive值是由 Activity/Fragment的状态决定的(前面LiveData的核心原理已经说明),

如果我想阻止消息的通知,想在这里执行return不现实。所以唯一的解决方案就锁定在 下图中的observer.mLastVersion >= mVersion 的判断上。只要我们能够让判断成立,那么这里就会return。onChange就不会执行,消息也就不会具备粘性。

image.png

一向严谨的谷歌工程师居然对这里的 mVersion变量加注释,但是根据代码逻辑可以判定:

只有在消息的版本mVersion 大于 订阅者的版本mLastVersion 时,才执行消息 通知.

这个也好理解,毕竟不能让订阅者收到过时的消息。

LiveData类的mVersion属性,唯一一处和当前情形有关的变动,就是:

image.png

而它的初始值是 START_VERSION -1

observer.mLastVersion 的初始值也是 START_VERSION -1

那么就很有意思了。 LiveData的mVersion, 可以理解为消息通道的版本号。而 observer.mLastVersion可以理解为 消息订阅者的版本号。前者,会随着每一次的postValue->调用到 setValue,从而 mVersion++, 那后者呢?

image.png

在消息确认要发送之后,把消息的版本号赋值给 订阅者的版本号,防止重复发送。

OK,问题基本确定了。先总结一波:

LiveDataBus基于 发布/订阅者 模型,发布者是 MutableLiveData,订阅者是Observer,两者都存在一个版本号,并且初始值都是-1。

当发布者发布消息的时候,它自身的版本号+1。

此时,判定 发布者的版本号是不是大于订阅者版本号,如果大于,那么消息就是有效消息,进行通知。否则,就认定为过时消息,不通知订阅者。

所以,要解决粘性消息,就只需要针对订阅者的版本号进行hook。hook的思路是:

在订阅者Activity获取了消息通道,然后注册监听的时候,把订阅者Observer自身的版本号设置得和 消息发布者的版本号一样,让 if(observer.mLastVersion>=mVersion){return;} 语句满足判定,执行return.

于是就看到了本节开头LiveDataBus代码中的,hook方法, 在注册监听的时候,把LiveData消息通道的mVersion赋值给observermLastVersion

class AliveOwnerMutableLiveData<T> : MutableLiveData<T>() {

        override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
            super.observe(owner, observer)
            hook(observer)
        }

        private fun hook(observer: Observer<in T>) {
            // 获得 当前 ObserverWrapper
            val classLiveData: Class<LiveData<*>> = LiveData::class.java
            val fieldObservers = classLiveData.getDeclaredField("mObservers")
            fieldObservers.isAccessible = true
            val mObservers = fieldObservers[this]
            val classObservers: Class<*> = mObservers.javaClass // 拿到了 map 对象

            //
            val methodGet = classObservers.getDeclaredMethod("get", Any::class.java)
            methodGet.isAccessible = true
            val objectWrapperEntry = methodGet.invoke(mObservers, observer)
            val objectWrapper =
                (objectWrapperEntry as Map.Entry<*, *>).value!!// 拿到ObserverWrapper
            val classObserverWrapper: Class<*>? = objectWrapper.javaClass.superclass //

            val fieldLastVersion =
                classObserverWrapper!!.getDeclaredField("mLastVersion")// 取得ObserverWrapper 的 mLastVersion属性
            fieldLastVersion.isAccessible = true
            val fieldVersion = classLiveData.getDeclaredField("mVersion")// 拿到 LiveData的mVersion属性
            fieldVersion.isAccessible = true
            val objectVersion = fieldVersion[this]
            fieldLastVersion[objectWrapper] = objectVersion
        }

    }

而由于有时候粘性消息也同样有作用,比如Activity跳转的数据传递。所以,LiveDataBus中我保留了原有的MutableLiveData通道map. 要不要使用粘性消息,视情况而定。

总结

LiveDataBus作为RxJava或者EventBus的替代品,在防止内存泄漏方面确实给开发者省了不少心,而且 View层内部的数据通信,也完全可以用LiveDataBus来替代传统的 全局变量方式,接口方式,Bundle封装方式等,写法上更加简洁一致,唯一需要管理的可能就是 消息通道的key值,如果用一个统一的类来管理,会更加严谨。

案例实操

Demo地址:https://github.com/18598925736/MvvmStandartDemo

上述有关MVVM的几个重要组件,在MVVM模式开发中会经常用到。但是个人不会去采用DataBinding,读者如果有需求可以自己查阅其他资料。

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

主要业务 : 一个简单的登录逻辑。从简单的案例入手,更容易感受到框架的本质。

完成核心功能

image.png

如上图所示,Demo中,存在明显的三层架构,

M-VM-M 三者之间的引用关系,现在是一条直线.V直接持有VM,VM直接持有M。

相比之前MVP,P和V层的对象之间实际上是存在互相引用的关系,只是用接口隔离了,为了不泄漏内存,还要在适当的时机断开引用。

但是,现在,内存泄漏的问题不用操心了。

image.png

详细代码如下:

V层代码, 略微使用接口约束:

interface LoginView {
    fun getUserName(): String
    fun getPassword(): String
    fun showResult(res: String)
}

/**
 * MVVM中View层依然是纯粹的视图层,不要涉及到任何业务逻辑,它只负责调用VM层的业务,
 * 与MVP相比,P必须持有V的引用,然后驱动UI的更新,最后还要写释放引用的代码。
 *
 * 但是MVVM中,可以直接用 观察者回调来实现
 *
 */
class LoginActivity : AppCompatActivity(), LoginView {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // 获取ViewModel
        val userModel = ViewModelProvider(this).get(LoginActivityViewModel::class.java)

        // 点击事件触发,出发VM的业务逻辑
        btnLogin.setOnClickListener {
            showLoading() // 开始请求数据显示加载中
            userModel.doLogin(getUserName(), getPassword())
        }

        // 定义数据回调逻辑
        userModel.userLiveData.observe(this, Observer {
            hideLoading() // 得到数据返回,隐藏加载中
            showResult(it.toString())// 处理数据
        })
    }

   //...省略N行
}

M层代码:

/**
 * 约束业务数据的接口
 */
interface UserModelInterface {
    fun login(userName: String, password: String, callback: HttpCallback<UserBean>)
}

/**
 * 纯粹的M层, 只有数据获取的代码
 */
class UserModel : UserModelInterface {
    override fun login(userName: String, password: String, callback: HttpCallback<UserBean>) {
        HttpRequestManager.doLogin(userName, password, callback)
    }

}

VM层的代码:

/**
 * 业务逻辑转移到这里
 *
 * 原则上,一个界面(Activity或者Fragment)对应一个 UserModel
 *
 */
class LoginActivityViewModel : ViewModel() {
    val userLiveData: MutableLiveData<UserBean> by lazy {
        MutableLiveData<UserBean>()
    }

    fun doLogin(userName: String, password: String) {

        // 发送网络请求,并且执行回调
        UserModel().login(userName, password, object : HttpCallback<UserBean> {
            override fun onSuccess(result: UserBean?) {
                userLiveData.postValue(result)
            }

            override fun onFailure(e: Exception?) {
                userLiveData.postValue(UserBean())// 失败,则手动构建一个userBean
            }
        })
    }

}

运行效果和MVP没有区别:

GIF-1591271895932.gif

接口/基类抽取

设计一个框架,除了面向对象思想的封装继承和多态之外,还需要了解设计模式注解技术泛型约束反射原理,有可能还需要了解一些 数据结构来追求高效。

提取基类,M和V层其实还是一样,需要BaseModel和BaseView来约束基本行为,就算是接口是空,也必须有,空接口可以作为类型标记。

BaseModel

interface BaseModel {
}

后续的Model层子接口以及实现类,都要以它为父类:

image.png

BaseView

// View层基类,
interface BaseView {
    fun showLoading()// 展示加载中
    fun hideLoading()// 隐藏加载中
}

所有View层类都是它的子类:

image.png

ViewModel不用提取

至于VM层,个人习惯,一般不用接口约束,因为它不再像MVPP层一样持有View的引用。提取不提取都问题不大。

image.png

BaseActivity/BaseFragment

MVP中,一个V,对应一个P,同样,MVVM中,一个V对应一个VM,所以,BaseActivity中,VM可以用泛型来进行约束。 抽离VM泛型之后,BaseActivity如下:

open abstract class BaseActivity<T : ViewModel> : AppCompatActivity() {
    /**
     * 布局ID
     */
    abstract fun getLayoutId(): Int
    private fun getViewModelClz(): Class<T> {
        return analysisClassInfo(this)
    }

    fun getViewModel(): T {
        return ViewModelProvider(this).get(getViewModelClz())
    }

    /**
     * 获取对象的参数化类型,并且把第一个参数化类型的class对象返回出去
     *
     * @param obj
     * @return
     */
    private fun analysisClassInfo(obj: Any): Class<T> {
        val type = obj.javaClass.genericSuperclass //
        val param = (type as ParameterizedType?)!!.actualTypeArguments //  获取参数化类型
        return param[0] as Class<T>
    }

    /**
     * 界面元素初始化
     */
    abstract fun init()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        init()
    }
}

这里有个难点

由于通过ViewModelProvider需要传入具体的ViewModelClass对象,所以要从BaseActivity的泛型参数中将泛型的具体类型解析出来,也就是这里的 analysisClassInfo()方法 . 具体的,这里不是重点,详情就略过了。

在使用了这个BaseActivity之后,LoginActivity就变成了如下模样:

image.png

BaseFragment也是类似:

open abstract class BaseFragment<T : ViewModel> : Fragment() {
    /**
     * 布局ID
     */
    abstract fun getLayoutId(): Int

    private fun getViewModelClz(): Class<T> {
        return analysisClassInfo(this)
    }

    fun getViewModel(): T {
        return ViewModelProvider(this).get(getViewModelClz())
    }

    /**
     * 获取对象的参数化类型,并且把第一个参数化类型的class对象返回出去
     *
     * @param obj
     * @return
     */
    private fun analysisClassInfo(obj: Any): Class<T> {
        val type = obj.javaClass.genericSuperclass //
        val param = (type as ParameterizedType?)!!.actualTypeArguments //  获取参数化类型
        return param[0] as Class<T>
    }

    /**
     * 界面元素初始化
     */
    abstract fun init()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(getLayoutId(), container, false)
    }


    /**
     * onViewCreated之后,才能用kotlin的viewId去操作view
     */
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        init()
    }
}

使用BaseFragment也是一样:

image.png

基类统一管理

将基类移动到基础lib模块中,

image.png

解决Bug,提升兼容性和使用体验

Demo只是Demo,实际使用MVVM的过程中,还会遇到不可预知的问题。

LiveData成员属性膨胀

一般我们认定,一个Activity/Fragment对应一个ViewModel,但是一个ViewModel中可以认定多个数据源,也就是LiveData, 一旦业务代码积累过多,就可能会出现一个ViewModel中N个liveData成员属性 :

class LoginActivityViewModel : ViewModel() {
    private val userLiveData: MutableLiveData<UserBean> by lazy { MutableLiveData<UserBean>() }
    private val noticeLiveData: SingleLiveData<String> by lazy { SingleLiveData<String>() }

    // 业务积累
    private val noticeLiveData: SingleLiveData2<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData2: SingleLiveData3<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData3: SingleLiveData4<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData4: SingleLiveData5<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData5: SingleLiveData6<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData6: SingleLiveData7<String> by lazy { SingleLiveData<String>() }
    //.....
}

而且多个ViewModel还有可能公用同一个类型的 LiveData,比如有一个Fragment也在用 NoticeLiveData:

class NoticeFragmentViewModel : ViewModel() {
    val noticeLiveData: SingleLiveData<String> by lazy { SingleLiveData<String>() }
}

那每个地方都去写一遍,并不是最理想的写法。

解决方案:提炼出LiveDataHolder类,让每一个ViewModel子类都拥有一个自己的LiveDataHolder对象,从holder中去获取LiveData, 去执行数据的发送和监听.

import androidx.lifecycle.MutableLiveData

// 三种LiveData的类别
enum class EventType {
    SINGLE,// 单次通知
    ALIVE,// 只通知存活的lifeCycleOwner
    DEFAULT// 默认,粘性消息
}

/**
 * 这个函数是为了简化一个ViewModel中存在N个LiveData定义的情况,其实只需要一个类,去获取就行了
 *
 * 每个ViewModel应该有自己的LiveDataHolder,应该是每一个ViewModel都有一个LiveDataHolder, 用来获取LiveData
 */
class LiveDataHolder {

    companion object {
        fun get(): LiveDataHolder {
            return LiveDataHolder()
        }
    }

    private val map: HashMap<Class<out Any?>, MutableList<MutableLiveData<out Any?>>> = HashMap()
    fun <T> getLiveData(key: Class<T>): MutableLiveData<T> {
        return getLiveData(key, EventType.DEFAULT)
    }

    /**
     *
     * 获得一个指定的LiveData
     *
     * @param key 指定返回值类型
     * @param eventType 消息通道的类别
     * @see  EventType
     */
    fun <T> getLiveData(key: Class<T>, eventType: EventType): MutableLiveData<T> {
        val liveDataClz: Class<MutableLiveData<T>> = when (eventType) {
            EventType.SINGLE -> {
                SingleLiveData<T>().javaClass
            }
            EventType.ALIVE -> {
                AliveOwnerMutableLiveData<T>().javaClass
            }
            else -> {
                MutableLiveData<T>().javaClass
            }
        }

        if (map[key] == null) {
            map[key] = ArrayList()
        }
        val currentList = map[key]!!
        for (a in currentList) {
            if (liveDataClz.isInstance(a)) {
                return a as MutableLiveData<T>
            }
        }
        val newLiveData = liveDataClz.getConstructor().newInstance()
        currentList.add(newLiveData)
        return newLiveData
    }
}

为了防止在每一个ViewModel子类中都去定义一次LiveDataHolder,我把它提炼到 ViewModel基类中去:

// ViewModel基类
abstract class BaseViewModel : ViewModel() {
    // 一个ViewModel可以存在多个LiveData,所以使用LiveDataHolder管理所有的LiveData
    val liveDataHolder = LiveDataHolder.get()
}

经过此次优化,所有ViewModel子类中不再需要去存放LiveData成员属性,要使用的时候直接如下这样写

class LoginActivityViewModel : BaseViewModel() {
    
    /**
     * 触发业务
     */
    fun getMsg() {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.postValue(NoticeModel().getNotice())
    }

    /**
     * 监听业务
     *
     * 为了不向外界暴露LiveData成员,提供一个注册监听的函数
     */
    fun observerGetMsg(lifecycleOwner: LifecycleOwner, observer: Observer<String>) {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.observe(lifecycleOwner, observer)
    }

    /**
     * 触发登录业务
     */
    fun doLogin(userName: String, password: String) {
        // 发送网络请求,并且执行回调
        UserModel().login(userName, password, object : HttpCallback<UserBean> {
            override fun onSuccess(result: UserBean?) {
                liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE).postValue(result)
            }

            override fun onFailure(e: Exception?) {
                liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE)
                    .postValue(UserBean())
            }
        })
    }

    /**
     * 监听登录业务
     */
    fun observerDoLogin(lifecycleOwner: LifecycleOwner, observer: Observer<UserBean>) {
        liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE)
            .observe(lifecycleOwner, observer)
    }

}

多个Fragment公用所在Activity的ViewModel

同一个Activity上,可能多个Fragment都会共用Activity的viewModel来达到数据同步的目的。如果按照常规方式,我们只能进行类型强转,然后调用 activity的viewModel的方法。

image.png

但是这种写法耦合性很高,不方便后期维护。

解决方案:使用泛型,改变一下BaseFragment的泛型约束,改变之后如下:

image.png

之后,使用BaseFragment的地方就应该写成如下这样:

image.png

如果存在另外一个相同的Fragment,也在使用父ActivityViewModel,他们就能够达成数据同步,如下图这样:

image.png

效果:

GIF-1591345588827.gif

MVVM优缺点

优点

缺点

结语

Demo地址:https://github.com/18598925736/MvvmStandartDemo

本文讲述了MVVM开发中可能用到的4个组件以及各自的核心原理,基本用法,并提供了基本的架构思路。时间有限,遇到真正的大型项目,还是要继续提炼基类,使用泛型,注解,反射,甚至数据结构,设计模式,去搭建一个完整项目架构。但是本文可以作为入门MVVM的完整攻略。

本人上一篇 漫谈MVP,结合 本文漫谈MVVM, 目前主流的开发机构模式基础知识应该完整了。

~开发工程师活到老学到老,共勉!

上一篇下一篇

猜你喜欢

热点阅读