前言

相信很多人在职业生涯的面试过程中都被问过一个问题?

面试官:我看你简历上有写 LiveData,那你能说说 LiveData 是粘性的吗?

这确实是一个值得深入思考的知识点,今天就让我们站在Google设计者的角度来深入学习一下LiveData

LiveData是粘性的吗?

关于这个问题,我们首先应该知道,粘性是什么意思?

不知道你对EventBus熟不熟悉,我第一次接触粘性这个概念,就来自于EventBus粘性事件

粘性事件:相对比普通事件,粘性事件支持先发送事件,再去注册订阅者。一旦完成订阅动作,这个订阅者就会接收到该粘性事件。

所以粘性其实就可以理解为观察者模式的升级,让观察者与被观察者对象之间更加的粘合。正常情况下,我们需要先注册观察者对象,然后再去更改被观察者对象,这样观察者对象才能接收到这个观察事件。而粘性,则支持先去触发更改被观察者对象,产生观察事件,然后再去注册观察者对象,注册成功后,该观察者对象还是可以接收到该观察事件,执行对应的观察动作。

来个例子吧!看看是不是粘性的

我们利用LiveData来做APP的全局状态管理

object GlobalState {val jcTestNumberLd: MutableLiveData<Int> = MutableLiveData<Int>()}

然后在Fragment以及Activity中观察该jcTestNumberLd

/** 观察 GlobalState 的 Activity */
class JcTestActivity : BaseVmDbActivity<MainViewModel, ActivityJcTestBinding>() {override fun initView() {viewBinding.incButton.setOnClickListener {GlobalState.jcTestNumberLd.value =if (GlobalState.jcTestNumberLd.value == null) {1} else {GlobalState.jcTestNumberLd.value!!.toInt().inc()}}}override fun initObserve() {GlobalState.jcTestNumberLd.observe(this, {Log.e(TAG, "initObserve: jctestNumber = $it")})}........
}/** 观察 GlobalState 的 Fragment */
class EventFragment : BaseVmDbFragment<EventViewModel, FragmentEventBinding>() {override fun setObservers() {GlobalState.jcTestNumberLd.observe(viewLifecycleOwner, {Log.e(TAG, "setObservers: jctestNumber = $it", )})}........
}

注意:这里例子中的 EventFragment 并不是关联到 JcTestActivity 的。用户会先进入到 JcTestActivity,然后由用户控制进入到另一个Activity中,加载EventFragment

我们来执行一下以下五步操作,来看一下输出的日志。

  1. 当我们第一次进入 JcTestActivity 时,注册了观察者,没有接收到观察事件,所以也就不会执行观察动作。
  2. 然后我们点击自增按钮为jcTestNumberLd 赋予新值,接收到观察事件,执行观察动作,输出 1
  3. 再次点击自增按钮,有观察事件,执行观察动作,输出 2
  4. 再次点击自增按钮,有观察事件,执行观察动作,输出 3
  5. 然后我们到 EventFragment 中,注册新的观察者,发现直接接收到观察事件,执行观察动作,输出 3

输出结果:

E/JcTestActivity: initObserve: jctestNumber = 1
E/JcTestActivity: initObserve: jctestNumber = 2
E/JcTestActivity: initObserve: jctestNumber = 3E/EventFragment: setObservers: jctestNumber = 3

这就是粘性事件!所以说,LiveData是粘性的

LiveData 是怎么实现粘性的呢?

在知道LiveData是粘性后,我不经问自己:它是怎么实现粘性的呢?

这里我们先来回顾一下EventBus粘性事件的实现原理。

EventBus在发送粘性事件时,会将这粘性事件存到一个叫做stickyEvents的集合中,然后等注册订阅新的观察者对象时,会去遍历该集合中的粘性事件,如果有找到对应的粘性事件,就将该粘性事件发送给该观察者。(如果你对EventBus粘性事件不熟悉,可以点击EventBus 源码解析(很细 很长)进一步了解学习。)

LiveData是不是也是以同样的原理来实现粘性的呢?

public LiveData(T value) {mData = value;mVersion = START_VERSION + 1;
}/*** Creates a LiveData with no value assigned to it.*/
public LiveData() {mData = NOT_SET;mVersion = START_VERSION;
}

LiveData 的构造函数中可以发现有一个 mVersion 参数,它代表着 LiveData 的版本号,每当我们进行setValue时,都会让mVersion进行自增。

另外,ObserverWrapper 这个观察者包装类中也有一个int mLastVersion = START_VERSION 版本号。

这两个版本号分别是被观察者对象与观察者对象的版本号,那这二者之间又有什么关系呢?

在判断是否通知观察者的 considerNotify(ObserverWrapper observer) 方法中,会对这两个版本号进行比较。

private void considerNotify(ObserverWrapper observer) {...省略代码...//如果观察者的版本号 >= LiveData的版本号,就说明该观察者已经接收过该观察事件,也就不再分发。if (observer.mLastVersion >= mVersion) {return;}//反之,分发观察事件给该观察者,让其执行对应的观察动作,并更新观察者的版本号observer.mLastVersion = mVersion;observer.mObserver.onChanged((T) mData);
}

概括一下:根据比对观察者对象的版本号与LiveData的版本号来判断是否分发当前版本的数据给该观察者。如果观察者对象的版本号大于等于LiveData的版本号,也就说明该观察者已经接收过当前版本的数据了,也就不需要再次分发了(等待下一次数据更新)。反之,则分发当前版本的数据给该观察者,让其执行对应的观察动作,并更新观察者的版本号,也就是更新为LiveData的版本号。

利用Hook验证一下

我们利用 Hook 分别拿到 LiveDatamVersion 以及 ObserverWrappermLastVersion 来看一下。

/** 主动 hook 检测版本号 */
dataBinding.hookCheckVersionButton.setOnClickListener {GlobalState.jcTestNumberLd.hook()
}fun LiveData<Int>.hook() {//get liveData mVersionval mVersion = this.javaClass.superclass.getDeclaredField("mVersion")mVersion.isAccessible = trueval mVersionValue = mVersion.get(this)Log.e(TAG, "hook: LiveData mVersion = $mVersionValue")val mObservers = this.javaClass.superclass.getDeclaredField("mObservers")mObservers.isAccessible = true//SafeIterableMap<Observer<? super T>, ObserverWrapper>val mObserversValue = mObservers.get(this)Log.e(TAG, "hook: mObserversValue = $mObserversValue")val methodGet = mObserversValue.javaClass.getDeclaredMethod("get", Any::class.java)methodGet.isAccessible = true//myObserver就是自定义的Observer,即通过Observer这个key来拿到SafeIterableMap的值,//这里也就是LifecycleBoundObserverval objectWrapperEntry = methodGet.invoke(mObserversValue, myObserver)val objectWrapper = (objectWrapperEntry as Map.Entry<*, *>).value//ObserverWrapper mLastVersionval mLastVersion = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion")mLastVersion.isAccessible = trueval mLastVersionValue = mLastVersion.get(objectWrapper)Log.e(TAG, "hook: observerWrapper mLastVersion = $mLastVersionValue")
}val myObserver = Observer<Int> {Log.e(TAG, "initObserve: jctestNumber = $it")dataBinding.testNumberTv.text = it.toString()
}

接着,我们来分析一下上面所介绍的那五个步骤:

  1. 当我们第一次进入 JcTestActivity 时,注册了观察者,没有接收到观察事件,所以也就不会执行观察动作。Hook一下,此时 LiveData mVersionValue = -1observerWrapper mLastVersionValue = -1
  2. 然后我们点击自增按钮为jcTestNumberLd 赋予新值,接收到观察事件,执行观察动作,输出 1
  3. 再次点击自增按钮,有观察事件,执行观察动作,输出 2
  4. 再次点击自增按钮,有观察事件,执行观察动作,输出 3。Hook一下,此时 LiveData mVersionValue = 2observerWrapper mLastVersionValue = 2
  5. 然后我们退出重新进到 JcTestActivity,注册新的观察者,发现直接接收到观察事件,执行观察动作,输出 3。Hook一下,此时 LiveData mVersionValue = 2observerWrapper mLastVersionValue = -1

输出的 Log信息 如下:

//第一次进入JcTestActivity
E/JcTestActivity: LiveData hook: mVersionValue = -1
E/JcTestActivity: hook: observerWrapper mLastVersionValue = -1//执行两次Inc
E/JcTestActivity: initObserve: jctestNumber = 1
E/JcTestActivity: initObserve: jctestNumber = 2
E/JcTestActivity: initObserve: jctestNumber = 3
E/JcTestActivity: LiveData hook: mVersionValue = 2
E/JcTestActivity: hook: observerWrapper mLastVersionValue = 2//退出JcTestActivity后,重新进入JcTestActivity
E/JcTestActivity: initObserve: jctestNumber = 3
E/JcTestActivity: LiveData hook: mVersionValue = 2
E/JcTestActivity: hook: observerWrapper mLastVersionValue = -1

这里最后一步重新进入了JcTestActivity 拿到 observerWrapper mLastVersionValue = -1 ,是因为重新进入后,这个观察者是新创建的,其mLastVersionValue 初始值就是-1。

新观察者创建成功后,触发considerNotify()方法,进行版本号对比,此时,LiveData.mVersion -> 2 大于 observer.mLastVersion -> -1,所以LiveData会将最新数据分发给当前观察者。

通过 Hook 分别拿到 LiveDatamVersion 以及 ObserverWrappermLastVersion 来进行对比,进一步证实了LiveData是粘性的

为什么Google要将LiveData设计成粘性的

LiveData可观察的数据存储器类,这样也就意味着存储在LiveData中的数据是会更新的,既然是会更新的,那必定就会存在状态,即最新数据状态。

所以,当数据状态发生改变时(数据发生了更新),LiveData需要告诉所有处于活跃状态的观察者, 让其同步更新数据。这应该很好理解了,因为这就是普通事件,先注册观察者,再去更新被观察者对象,触发观察事件。

那这时,你再去新注册一个观察者对象,你认为它需不需要知道此时LiveData最新的数据呢?

答案是:需要。

因为所有的观察者,都只需要知道LiveData中存储的数据,而且是最新数据。不管我是新注册的观察者,只要你LiveData有了最新数据,就需要告诉我。而关于有无新数据,从代码上体现出来的就是,LiveData.mVersion > Observer.mLastVersion

这也就是粘性事件,先更新被观察者对象,触发观察事件,再去注册观察者,观察者会直接接收到该观察事件,执行对应的观察动作。

它的功能属性导致其只能是粘性的。

总结

本篇文章,我们先是通过EventBus的粘性事件来了解了什么是粘性?通过案例来初步推出LiveData是粘性的,接着通过探索源码来发现LiveData实现粘性的原理,并通过反射hook,来进一步证实,最后站在Google设计者的角度来思考为什么要将LiveData设计成粘性的。相信你通过本篇文章,肯定对LiveData有了进一步的了解。

为什么Google要将LiveData设计成粘性的相关推荐

  1. Google Friendly的网站设计

    转载自chedong的blog,一篇很好的文章,居然刚刚从简朴生活的blog上看到 版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明 http://www.chedo ...

  2. 为什么RedisCluster会设计成16384个槽呢?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! Redis Cluster 是Redis的集群实现,内置数 ...

  3. Spring 为啥默认把 bean 设计成单例的?

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:在滴滴和头条干了 2 年后端开发,太真实-个人原创100W+访问量博客:点击前往,查看更多 熟悉Spring开发 ...

  4. String的内存模型,为什么String被设计成不可变的

    String是Java中最常用的类,是不可变的(Immutable), 那么String是如何实现Immutable呢,String为什么要设计成不可变呢? 前言 关于String,收集一波基础,来源 ...

  5. 这样设计是否更好些~仓储接口是否应该设计成基础操作接口和扩展操作接口

    前言 我们进行linq to sql和ef时代后,底层的实现基本使用的是repository模块,即仓储模式,事实上就是把ORM实体的最基本操作进行封闭,对外层不公开操作实现的细节. 面向接口的编程 ...

  6. 为什么ui框架设计成单线程_评估UI设计的备忘单

    为什么ui框架设计成单线程 Whether you're evaluating your design proposals or giving feedback to a colleague duri ...

  7. 支付渠道参数如何设计成路由化配置

    转载自  支付渠道参数如何设计成路由化配置 今天我们来探讨在搭建支付系统时一个比较关键的问题:渠道参数路由化配置如何设计? 在开发支付系统的时候,我们经常会涉及到对接多个支付渠道,除常见的支付宝.微信 ...

  8. java语言特点 字符串不变_面试必问:Java中String类型为什么设计成不可变的?

    这几天在各大平台上都看到过这样一些帖子,全都是关于String类型对象不可变的问题,当然现在也是找工作的准备时期,因此花了一部分时间对其进行整理一下. 想要完全了解String,在这里我们需要解决以下 ...

  9. Spring为啥默认把bean设计成单例的

    熟悉Spring开发的朋友都知道Spring提供了5种scope分别是singleton.prototype.request.session.global session.而且默认情况下是single ...

  10. 为什么jdk中把String类设计成final

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 为什么j ...

最新文章

  1. C++泛型编程:template模板
  2. GAN的基本原理与入门应用!
  3. iOS Swift UISearchController的取消按钮
  4. java和php设置的cookies_php带cookie访问下载文件 header设置
  5. 【转】ArcGIS API for Silverlight/WPF 2.1学习笔记(二)
  6. HBase Shell 的基本操作
  7. mysql where 1 作用_MYSQL where 1=1 的作用
  8. 百会CRM教你在大数据平台中做精准营销
  9. ide 日志 乱码_IDE日志分析方法pt。 1个
  10. DevExpress v15.1:ASP.NET MVC功能升级(一)
  11. synchronized实现
  12. Android Context简单说明
  13. c++ 中引用()的用法和应用实例
  14. 键盘连接计算机接口,终于明白电脑如何连接键盘
  15. 2018 网易校招 骰子游戏
  16. ZBrush中的法线贴图你知道吗?
  17. ns3--TapBridge, TapNetDevice,FdNetDevice等
  18. 拓嘉辰丰电商:拼多多所属哪种电商模式
  19. 拆解进口美国产飞机仪表!看看USA做工!
  20. 循环冗余校验码例题[转帖]

热门文章

  1. C# GDAL读、写shape中文乱码
  2. PHP implode和explode用法
  3. 山东济南计算机比赛,第十二届齐鲁软件大赛及首届济南市计算机科技奖颁奖盛典举行...
  4. 领导者激励团队的最佳方法
  5. Global.asax 文件是什么
  6. 从BlueSky.h和BlueSky.cpp到BlueSky.out的那些事儿
  7. STC系列51单片机延时程序汇总
  8. 方舟手游服务器怎么发位置,方舟手游怎么把自己号传到服务器 | 手游网游页游攻略大全...
  9. 【周六福利来了~】优才安卓公开课:程序员到架构师之路
  10. What Is Harmony OS? Huawei’s New Operating System Explained