Android官方架构组件LiveData: 观察者模式领域二三事

本文是 《Android Jetpack 官方架构组件》 系列文章, LiveData本身很简单,但其代表却正是 MVVM 模式最重要的思想,即 数据驱动视图(也有叫观察者模式、响应式等)——这也是摆脱 顺序性编程思维 的重要一步。

回顾LiveData:从处境尴尬到咸鱼翻身

我们都知道Google在去年的 I/O 大会非常隆重地推出了一系列的 架构组件,本文的主角,LiveData 正是其中之一,和LifecycleViewModelRoom比较起来,LiveData可以说是最受关注的组件也不为过,遗憾的是,在发布的最初,关注点是因为它饱含争议,相当一部分的开发者认为——LiveData 实在太 鸡肋 了!

2017年的 Android 技术领域,RxJava无疑是炙手可热的名词之一,其 观察者模式链式调用 所表现出来的 API 优秀地设计,使得它位于很多 Android项目技术选型中的 第一序列

这时 Google 隆重推出了具有类似功能的 LiveData (其本质就是观察者模式),可以说是有点初生牛犊不怕虎的感觉,开发者们不由自主将LiveDataRxJava 进行了对比,结论基本出奇的一致—— LiveData所提供的功能,RxJava完全足以胜任,而后者却同时具有庞大的生态圈,这是LiveData短时间内难以撼动(替代)的。

时至今日,LiveData的使用者越来越多,最主要的原因当然和Google的强力支持不无关系,但是LiveData本身优秀的设计和轻量级也吸引了越来越多开发者的青睐。

现在我们需要去了解它了,我们都知道,LiveData 本质是 观察者模式 的体现,可关键的问题是:

观察者模式到底是啥?!

讨论这个问题之前,我们先看看 LiveData 的用法,这实在没什么技术难度,比如,你可以这样实例化一个LiveData并使用它:

为了保证代码简洁可读,示例代码我使用了Kotlin

如你所见,LiveData实际上就像一个 容器, 本文中它存储了一个String类型的引用,每当这个容器内 String的数据发生变化,我们都能在回调函数中进行对应的处理,比如 Toast

这似乎和我们日常用到的 Button 控件的 setOnClickListener() 非常相似,实际上点击事件的监听也正是 观察者模式 的一种体现,对于观察者来说,它并不关心观察对象 数据是如何过来的,而只关心数据过来后 进行怎样的处理

这也就是说,事件发射的上游接收事件的下游 互不干涉,大幅降低了互相持有的依赖关系所带来的强耦合性。

我依然坚持学习原理比学习如何应用的优先级更高,因此我们先来一一探究LiveData本身设计中存在的那些闪光点背后的故事。

LiveData是如何避免内存泄漏的

我们都知道,RxJava在使用过程中,避免内存泄漏是一个不可忽视的问题,因此我们一般需要借助三方库比如RxLifecycleAutoDispose来解决这个问题。

而反观LiveData,当它被我们的Activity订阅观察,这之后Activity如果finish()掉,LiveData本身会自动“清理”以避免内存泄漏。

这是一个非常好用的特性,它的实现原理非常简单,其本质就是利用了Jetpack 架构组件中的另外一个成员—— Lifecycle

让我们来看看LiveData被订阅时内部的代码:

源码中的逻辑非常复杂,我们只关注核心代码:

  • 1.首先我们在调用LiveData.observer()方法时,传递的第一个参数Acitivity实际被向上抽象成为了 LifecycleOwner,第二个参数Obserser实际就是我们的观察后的回调。

这里我们需要注意的是,执行LiveData.observer()方法时 必须处于主线程,否则会因为断言失败而抛出异常。

  • 2.方法内部实际上将我们传入的2个参数包装成了一个新的 LifecycleBoundObserver对象,它实现了 Lifecycle 组件中的LifecycleObserver接口:

这里就解释了为什么LiveData能够 自动解除订阅而避免内存泄漏 了,因为它内部能够感应到Activity或者Fragment的生命周期。

这种设计非常巧妙——在我们初识 Lifecycle 组件时,总是下意识认为它能够对大的对象进行有效生命周期的管理(比如 Presenter),实际上,这种生命周期的管理我们完全可以应用到各个功能的基础组件中,比如大到吃内存的 MediaPlayer(多媒体播放器)、绘制设计复杂的 自定义View,小到随处可见的LiveData,都可以通过实现LifecycleObserver接口达到 感应生命周期并内部释放重的资源 的目的。

关于上述代码中注释了 更新LiveData的活跃状态 的源码,我们先跳过,稍后我们会详细探讨它。

    1. 我们继续回到上上一个源码片段的第三步中,对于一个可观察的LiveData来讲,当然存在多个观察者同时订阅观察的情况,因此考虑到这一点,Google的工程师们为每一个LiveData配置了一个Map存储所有的观察者。
  • 4.到了这一步,我们将第2步包装生成的对象交给我们传入的 Activity,让它在不同的生命周期事件中去逐一通知其所有的观察者,当然也包含了我们的LiveData

数据更新后如何通知到回调方法?

LiveData原生的API提供了2种方式供开发者更新数据, 分别是 setValue()postValue(),官方文档明确标明:setValue()方法必须在 主线程 进行调用,而postValue()方法更适合在执行较重工作 子线程 中进行调用(比如网络请求等)——在所有情况下,调用setValue()postValue()都会 触发观察者并更新UI

柿子挑软的捏,我们先看setValue()方法的实现原理:

通过保留最终的核心代码,我们很清晰了解了setValue()方法为什么能更新LiveData的值,并且通知到回调函数中的代码去执行,比如更新UI。

但是我们知道,普遍情况下,Android不允许在子线程更新UI,但是postValue()方法却可以在子线程更新LiveData()的数据,并通知更新UI,这是如何实现的呢?

其实答案已经呼之欲出了,就是通过 Handler

现在你已经对LiveData整体了一个基本的了解了,接下来让我们开始去探究更细节的闪光点。

看完源码,你告诉我才算入门?

LiveData本身非常简单,毕竟它本身的源码一共也就500行左右,也许你要说 准备面试粗读一遍源码就够了,很遗憾,即使是粗读了源码,也很难说能够完全招架更深入的提问...

让我们来看一道题目:在下述Activity完整的生命周期中,Activity一共观察到了几次数据的变更——即 一共打印了几条Log ?(补充纠正,onStop()方法中值应该为 "onStop")

公布答案:

意外的是,livedata.observer()的本次观察并没有观察到 onCreateonStoponDestroy 的数据变更。

为什么会这样?

还记得上文提到过2次的 LiveData的活跃状态(Active) 相关代码吗?实际上,LiveData内部存储的每一个LifecycleBoundObserver本身都有shouldBeActive的状态:

现在我们明白了,原来并不是只要在onDestroy()之前为LiveData进行更新操作,LiveData的观察者就能响应到对应的事件的。

虽然我们明白了这一点,但是如果更深入的思考,你会又多一个问题,那就是:

  • 既然LiveData已经能够实现在onDestroy()的生命周期时自动解除订阅,为什么还要多此一举设置一个Active的状态呢?

仔细想想,其实也不难得到答案,Activity并非只有onDestroy()一种状态的,更多时候,新的Activity运行在栈顶,旧的Activity就会运行在 background——这时旧的Activity会执行对应的onPause()onStop()方法,我们当然不会关心运行在后台的Activity所观察的LiveData对象(即使数据更新了,我们也无从进行对应UI的更新操作),因此LiveData进入 InActive(待定、非活跃)状态,return并且不去执行对应的回调方法,是 非常缜密的优秀设计

当然,有同学提出,我如果希望这种情况下,Activity在后台依然能够响应数据的变更,可不可以呢?当然可以,LiveData此外还提供了observerForever()方法,在这种情况下,它能够响应到任何生命周期中数据的变更事件:

除此之外,源码中处处都是优秀的细节,比如对于observe()方法和observerForever()方法对应生成的包装类,后者方法生成的是AlwaysActiveObserver对象,统一抽象为ObserverWrapper

这种即使只有2种不同场景,也通过代码的设计,将公共业务进行向上抽离为抽象类的严谨,也非常值得我们学习。

小结,与更深入的思考

本来写了更多,篇幅所限,最终还是决定删除了相当一部分和 RxJava 有关的内容,这些内容并非是将 LiveDataRxJava 进行对比一决高下—— 例如,Google官方提供了 LiveDataRxJava 互相进行转换的工具类:

https://developer.android.com/reference/android/arch/lifecycle/LiveDataReactiveStreams

值得玩味的是,官方的工具类中,LiveDataRxJava的转换方法,返回值并非是一个Flowable,而是一个Publisher接口:

正如我在注释中标注的,这个工具方法返回的是一个接口,很大程度上限制了我们对RxJava众多强大操作符的使用,这是否是来自Google的恶意

当然不是,对于这种行为,我的理解是Google对于LiveData本身严格的约束——它只应该用于进行数据的观察,而不是花哨的操作;转换为Flowable当然非常简单,但是这种行为是否属于LiveData本身职责的逾越,更准确来说,是否属于不必要的过度设计?这些是我们需要去细细揣度的。

我无从验证我的理解是否正确,但是我的这个理由已经足够说服我自己,再往下已不再是LiveData的范畴,关于这一点我将会专门起一篇文章去进行更深入的探讨,欢迎关注。

--------------------------广告分割线------------------------------

系列文章

争取打造 Android Jetpack 讲解的最好的博客系列

Android Jetpack 实战篇


关于我

Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的个人博客或者Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

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

推荐阅读更多精彩内容