Jetpack LiveData 的设计理念及改进

一、架构指南

在日常的开发中,我们经常会讲到 MVC、MVP、MVVM 等多种开发模式,这其实都是应用架构的不同呈现方式,你目前又是使用的什么应用架构呢?

一个好的架构,其至少应该遵循两个原则

  • 关注点分离。关注点分离指的是架构中的每一层应只专注于实现某一特定目的。一种常见的错误就是在 ActivityFragment 中编写所有代码(例如,直接在界面层完成网络请求),这种基于界面的类应仅包含与系统和用户交互的逻辑,你应该使这些类尽可能保持精简,这样可以避免许多与生命周期相关的问题
  • 通过模型驱动界面。模型是指负责处理应用数据的组件,模型应独立于应用的界面层和其它应用组件,这样才能不受应用的生命周期以及相关的关注点的影响

关于第一点。对于一个移动设备来说,其拥有的资源是固定且极其有限的,系统可能会随时终止某些应用进程以便为前台进程腾出内存空间。而且,即使是对于前台进程来说,我们也并非拥有ActivityFragment的完全所有权,系统也可能会随时因为内存空间不足或者系统配置更改等意外情况而销毁它们。做到关注点分离,就是将各个层次的职责划分开,避免将用户数据和界面的生命周期强绑定,这样当意外情况发生时用户数据才不会随之一起被销毁

关于第二点。通过模型驱动界面,即界面对模型来说应该是相当于一个观察者而独立存在,界面不应该直接持有数据。界面通过观察数据的变化来驱动自身,模型也可以通过改变自身来驱动界面更改。这里指的模型最好是持久性模型,即模型的生命周期需要比界面甚至应用进程更加长,典型代表即 Jetpack 中的 ViewModel 和 Room。这样当有意外情况发生时,用户也不会丢失数据,我们可以在随后就为用户恢复数据

Google 推荐的应用架构图如下所示

当中,每个组件仅依赖于其下一级的组件。ViewModel 就是关注点分离原则的一个具体实现,是作为用户数据的承载体处理者而存在的,Activity/Fragment 仅依赖于 ViewModel,ViewModel 就用于响应界面层的输入和驱动界面层变化,Repository 用于为 ViewModel 提供一个单一的数据来源及数据存储域,Repository 可以同时依赖于持久性数据模型和远程服务器数据源

二、LiveData 的优势

本文想要讨论的就是 ViewModel 所包含的 LiveData

从 Google 官方推荐的应用架构图可以看到,LiveData 是包含在 ViewModel 中的。LiveData 是一种可观察的数据存储器,Activity/Fragment 是观察者,LiveData 是被观察者。LiveData 具有生命周期感知能力,当我们向 LiveData 注册了一个和 LifecycleOwner 相绑定的 Observer 时,如果 LifecycleOwner 的生命周期处于STARTEDRESUMED 状态,则认为该观察者当前处于活跃状态,此时 LiveData 才会向观察者发送事件通知,非活跃状态的观察者不会收到任何事件通知。且当 LifecycleOwner 的状态变为DESTROYED时,LiveData 会自动解除和观察者之间的绑定关系,以防止内存泄漏和过多的内存消耗。所以说,LiveData 具有生命周期感知能力,Activity/Fragment 无需和 LiveData 创建明确且严格的依赖路径

ViewModel 和 LiveData 可以看做是对关注点分离通过模型驱动界面原则的一个共同实现,ViewModel 提供了让用户数据独立于界面而存在的能力,LiveData 提供了安全地通知并驱动界面变化的能力

三、LiveData 的缺陷

LiveData 的设计初衷就决定了其具有以下三点缺陷(或者说特性):

  1. 只在 Observer 至少处于 STARTED 状态时才能收到事件通知。Activity 只有在 onStart 后和 onStop 前才能收到事件通知
  2. LiveData 是黏性的。假设存在一个静态的 LiveData 变量,且已经包含了数据,对其进行监听的 Activity 都会收到其当前值的回调通知,即收到了黏性消息。这个概念就类似于 EventBus 中的 StickyEvent
  3. 中间值可以被新值直接掩盖。当 Activity 处于后台时,如果 LiveData 先后接收到了多个值,那么当 Activity 回到前台时也只会收到最新值的一次回调,中间值直接被掩盖了,Activity 完全不会感受到中间值的存在

以上三点特性都是由于界面层的特点来决定的:

  • 当界面处于后台时,此时就完全没有必要更新界面,因为此时界面对用户来说完全不可见,且界面有可能再也没有机会回到前台了,所以只有当界面回到前台时更新操作才是有意义的
  • 当界面被意外销毁后,我们需要根据已有的数据来进行界面重建,所以 LiveData 被设计为黏性的
  • 对于 LiveData 所代表的界面状态值来说,我们往往需要的只是其最新状态,不需要处理中间值,所以 LiveData 的中间值可以被新值直接掩盖

四、LiveData 作为消息通知组件

如果将 LiveData 单纯地作为界面层状态更新的载体来看待的话,那么以上三点特性就挺合情合理的了。但如果我们是将 LiveData 作为应用全局的消息通知组件的话,这三个特性就会给我们带来困扰了

相信很多开发者都尝试过将一个 LiveData 实例声明为静态变量,然后多个 Activity 通过同时监听该 LiveData 来实现数据通信。这种方式的优点是:能够以非常简单的方式来实现跨页面通信,同时也保障了生命周期安全。缺点是:在 Activity 处于 onStop 状态时无法收到通知,且会收到黏性消息这种脏数据

在 Activity 处于 onStop 状态时收到通知有什么意义呢?收到了黏性消息又会导致什么问题呢?这可以通过假设一个需求来说明

假设当前你的 App 包含一个圈子列表页面,每个圈子 item 包含了一个按钮用于改变对此圈子的关注状态,当点击关注后就会向用户展示一个几百毫秒的动画效果。点击 item 可以跳转到圈子详情页,在详情页也包含一个按钮用于改变圈子的关注状态

现在,产品要求两个页面间的关注状态是要能够实时统一的,即在圈子详情页改变关注状态后,圈子列表页面也要跟着一起改变关注状态。为了实现这个效果,可以声明一个全局的静态变量来实现跨页面通知圈子关注状态的变化

object FocusRepository {

    //String 表示圈子ID,Boolean 表示对该圈子的关注状态
    val focusLiveData = MutableLiveData<Pair<String, Boolean>>()

}

class CircleListActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_list)
        //建立监听
        FocusRepository.focusLiveData.observe(this, Observer {

        })
    }

}

class CircleDetailsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_details)
        onCircleFocusStateChanged("100", true)
    }

    private fun onCircleFocusStateChanged(circleId: String, focused: Boolean) {
        FocusRepository.focusLiveData.value = Pair(circleId, focused)
    }

}

这种方式就会导致三个问题:

  • 当用户在 CircleDetailsActivity 改变了圈子的关注状态后返回,CircleListActivity 从后台回到前台后才会收到 focusLiveData 的事件通知,此时才会触发执行动画。而在这种情况下我们并不希望用户看到动画效果,而是希望能够在 CircleListActivity 改变关注状态的同时就实时在触发动画了。此时使用 LiveData 就无法满足我们的需求了,LiveData 不支持在 Activity 处于 onStop 状态时下发通知
  • 如果在 CircleDetailsActivity 先后改变了多个圈子的关注状态的话,那么就会导致另一个问题:中间值被最新值直接掩盖了。这也是由于LiveData 不支持在 Activity 处于 onStop 状态时下发通知导致的
  • 在 focusLiveData 已经有值的情况下,当用户第一次打开 CircleListActivity 时,就会收到 focusLiveData 的回调通知。而此时 CircleListActivity 的数据会从服务器获取,可以保证是最新的,并不需要本地值的回调通知,此时 focusLiveData 就相当于脏数据了。这种情况下,LiveData 也会给我们带来困扰,其黏性消息其实就相当于脏数据了

五、EventLiveData

考虑到 LiveData 不那么适合用做应用全局的消息通知组件,所以我就基于其源码实现了一个改良版的 EventLiveData,以此来解决 LiveData 的缺陷。EventLiveData 在使用上基本 LiveData 一样,我只是对其进行了功能扩展

发送消息:

val eventLiveData = EventLiveData<String>()

//主线程调用
eventLiveData.setValue("leavesC")
//子线程调用
eventLiveData.postValue("leavesC")
//任意线程都可以调用,内部会自动判断线程
eventLiveData.submitValue("leavesC")

不接收黏性消息:

//不接收黏性消息
//在 onResume 之后和 onDestroy 之前均能收到 Observer 回调
eventLiveData.observe(LifecycleOwner, Observer {

})

//不接收黏性消息
//在 onCreate 之后和 onDestroy 之前均能收到 Observer 回调
eventLiveData.observe(LifecycleOwner, Observer {

}, false)

接收黏性消息:

//接收黏性消息
//在 onResume 之后和 onDestroy 之前均能收到 Observer 回调
eventLiveData.observeSticky(LifecycleOwner, Observer {

})

//接收黏性消息
//在 onCreate 之后和 onDestroy 之前均能收到 Observer 回调
eventLiveData.observeSticky(LifecycleOwner, Observer {

}, false)

不和生命周期绑定:

//不接收黏性消息
eventLiveData.observeForever(Observer {

})

//接收黏性消息
eventLiveData.observeForeverSticky(Observer {

})

六、实现原理

EventLiveData 是基于 LiveData 的源码来改造实现的,在理解了 LiveData 的设计理念和实现原理后来进行自定义其实就非常简单了,这里就简单说下我的实现思路

LiveData 内部包含一个 mVersion 变量用来标记当前值的版本,即值的新旧程度,当外部传递了新值时(不管是 setValue 还是 postValue),mVersion 均会递增 +1

@MainThread
private fun setValue(value: T) {
    assertMainThread(
        "setValue"
    )
    mVersion++
    mData = value
    dispatchingValue(null)
}

同时 ObserverWrapper 内部包含一个 mLastVersion 用于标记 Observer 内最后一个被回调的 value 的新旧程度

private abstract class ObserverWrapper {

    //外部传进来的对 LiveData 进行数据监听的 Observer
    final Observer<? super T> mObserver;

    //用于标记 mObserver 是否处于活跃状态
    boolean mActive;

    //用于标记 Observer 内最后一个被回调的 value 的新旧程度
    int mLastVersion = START_VERSION;

    ObserverWrapper(Observer<? super T> observer) {
        mObserver = observer;
    }

}

considerNotify 方法会根据 mLastVersion 的大小来决定是否需要向 Observer 回调值,那么我们只要控制 Observer 的 mLastVersion 的初始值大小不就可以避免旧值的通知了吗?

private void considerNotify(ObserverWrapper observer) {
    ···
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

再然后,LifecycleBoundObserver 的 shouldBeActive() 方法就限制了只有当 Lifecycle 的当前状态是 STARTED 或者 RESUMED 时才进行数据回调,那么我们只要改变此限制条件,就可以增大 Observer 的有效生命周期范围了

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }

    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

}

七、引入依赖

EventLiveData 已托管到 jitpack,可以直接远程依赖。GitHub 地址:https://github.com/leavesCZY/EventLiveData

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    implementation 'com.github.leavesCZY:EventLiveData:1.0.2'
}

八、参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵...
    tse1y阅读 3,010评论 0 2
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,495评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,551评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,716评论 1 1
  • 在妖界我有个名头叫胡百晓,无论是何事,只要找到胡百晓即可有解决的办法。因为是只狐狸大家以讹传讹叫我“倾城百晓”,...
    猫九0110阅读 3,255评论 7 3