我和 Google 的代码差在哪里?


/   今日科技快讯   /

近日,2021年小米春季新品发布会上,雷军提到了小米造车的情况。雷军透露,小米从2021年1月15日小米开始认真调研造车。对于造车需要消耗的资金,雷军表示非常了解,并且霸气表示:“造车,我们亏得起。”

/   作者简介   /

本篇文章来自cs Lesin同学投稿,讲解了关于分发 BackPress 自己的实现方案和 Google 官方的实现方案的区别,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

/   前言   /

之前在项目上遇到一个需要在 Fragment 里面监听 BackPress 的需求,在网上搜寻了一下,发现大多数解决方案都是使用 Fragment 的 RootView 去调用setOnKeyListener 的方法,然后在内部去拦截  KEYCODE_BACK 这个事件实现。

大概长这样:

说实话对 setOnKeyListener 这个 API 用之甚少,并且看到 requestFocus() 之类的方法,天然觉得不太靠谱,于是准备自己实现下。

/   实现思路   /

因为 Fragment 附属于 Activity,并且 Activity 可以响应到 BackPress,也就是我们熟悉的 onBackPressed() 方法,我们只需要重写这个方法,然后将 BackPress 分发到 Fragment 就好了。

我的实现方案

最终实现出的代码大概长这样:

首先定义了一个 BackPressObserver 接口, 表示我们的 BackPress 观察者,由 Fragment 实现,返回值代表是否消耗 BackPress 事件,消耗后,BackPress 不会再向下层 Fragment 传递。再看一下我们的 BackPressRegistry 类:

BackPressRegistry 类,则充当了一个注册和分发 BackPress 事件的角色,对应 registerBackPress() 和 dispatchBackPress() 方法。并且有两点需要特别说明一下:

1. 分发的时候,是从后向前遍历 BackPressObserver,原因是可能有多个 Fragment 注册 BackPress,对于这种情况,理论上,后注册的是应该优先响应的,这更符合直觉。

2. 注册的时候需要传入对应的 LifecycleOwner,这样可以在 Fragment 销毁后自动的移除 BackPressObserver,我们无需再去 Fragment 的 onDestroy() 里面手动移除。(放过 onDestroy() 吧!)

然后看一下我们的 BaseActivity 里面的实现:

主要是重写了 onBackPressed 方法,交给我们的 BackPressRegistry 去分发,如果一旦有某个 BackPressObserver 返回了 true,也就是消耗了 BackPress,则不会执行 super.onBackPressed(),也就是 Activity 不会被 finish 掉,这在某些场景很有用。

同时增加了一个 registerBackPress() 方法用于注册,内部调用 BackPressRegistry 同签名方法,相当于只是中转了下。

再看一下我们如何使用:

使用很简单,获取到 Activity 然后强转成 BaseActivity 然后调用我们的注册方法就可以了,然后在内部如果我们需要消耗 BackPress 则直接返回 true ,不需要消耗则返回 false,这样其它的 BackPressObserver 则有机会响应 BackPress,参考了 View 的分发事件。我的方案似乎没啥问题,也没遇到啥 bug。但是前两天使用 AndroidX 发现 ComponentActivity 居然也实现了类似的这个功能,并且核心思想都是一样的,于是赶紧看了下源码,感受下和 Google 的代码之间有什么差距。

/   Google的实现方案   /

首先官方是定义了一个 OnBackPressedDispatcherOwner 接口,由 Activity 实现,他将返回我们 OnBackPressedDispatcher,相当于我们的 BackPressRegistry 类,后面简称为 Dispacher,接口长这样:

同时这个接口继承了 LifecycleOwner,关于这一点,我还不太明白有啥用。然后我们看一下 OnBackPressedDispatcher 的实现。源码过多,这里先看下大概的结构:

主要有以下要素:

首先也用了一个容器来存放所有的 OnBackPressedCallback(等同于我的 BackPressObserver ),这个实现一会儿在看,只不过这里他用了一个 ArrayDeque 作为容器,但是我是采用的是 LinkedList,似乎前者性能更好?不过这个无所谓,一般一个 Activity 的 BackPress 注册个数应该是个位数,根本没有到谈性能的时候。

onBackPressd() 就相当于我们自己实现的 dispatchBackPress() 方法,负责分发 BackPress 事件。

有一个 mFallbackOnBackPressed 的 Runnable 属性,通过构造传入,当所有的 OnBackPressedCallback 都没有被分发到时,会回调这个 Runnable。

另外有三个 addCallback 的重载方法,应该就是注册我们的OnBackPressedCallback 了,后面再议。

还有两个内部类 OnBackPressedCancellable 以及LifecycleOnBackPressedCancellable,听名字和取消有关,一会儿再分析。

首先我们先看看 OnBackPressedCallback 长啥样:

虽然是叫 Callback,但是实际上是一个抽象类,竟然让我想到了面试八股文:说一下抽象类和接口的区别是什么?(发抖)

然后有如下要素:

  • 有一个 enable 属性决定该 Callback 是否可用,为 false 时,即使注册到 Dispacher,也不会回调自身的 handleOnBackPressed() 方法。

  • 使用一个线程安全的 CopyOnWriteArrayList 保存 Callcellable 集合,Callcellable 只有一个 cancel 方法,估计和上面两个内部类有关,一会再聊。

  • 内部有一个 remove() 方法,会调用所有 Cancellable 的 cancel 方法。

看上去这个 Callback 能自己设置自己的 enable 状态,还能取消自己。而我们自己实现的方案中 BackPressObserver 似乎仅仅只是一个回调而已。

下面我们分析 OnBackPressedDispatcher ,先看一下他的普通注册方法:

addCallback(OnOnBackPressedCallback onOnBackPressedCallback)

内部直接间接调用了 addCancellableCallback 方法。观察一下这个方法,第一行直接将我们的 callback 加入到 ArrayDeque 中,第二行构建了一个OnBackPressedCallcellable ,然后第三行加入到我们的 BackPressCallback 中,最后返回了出去,但是我们目前还没有用到这个返回值。

我们来看一下这个 OnBackPressedCallcellable 的实现:

构造时传入了我们的 BackPressCallback,然后在 cancel 的时候,移除了与 Dispatcher 的连接,并且移除了 BackPressCallback 对自身的持有。

还记得我们的 BackPressCallback 有一个 remove() 方法吗,会遍历的调用 cancel 方法,也就是说我们的 BackPressCallback 现在拥有了取消自身的能力,直接调用自身的 remove() 方法即可,即可取消与所有 Dispatcher 的连接。

接下来我们看一下另一个接受 LifecycleOwner 的 addCallback 方法。

这里并没有把 BackPressCallback 添加到 Dispacher 的 ArrayDeque 中,而是直接给 BackPressCallback 添加了一个 LifecycleOnBackPressedCancellable,然后这里不仅传入了我们的 BackPressCallback,还传入了 Lifecycle,肯定要围绕生命周期做文章了。

构造的时候就用 Lifecycle 添加了一个 Observer,我们看一下 onStateChanged 里面做了啥。

首先是在 onStart() 的时候,把 BackPressCallback 添加到了到 Dispacher,并且保存了返回的 Cancellable 方便在 onStop() 的时候取消。

而在 onDestroy() 的时候调用了自身的 cancel() 方法。移除了 Lifecycle 的observer (似乎没有必要?),然后移除了 BackPressCallback 对自身的持有。最后再调用了 mCurrentCancellable 的 cancel 方法,估计是为了保险起见。

根据这段代码我们发现,如果我们使用 Lifecycle 的方式去注册 BackPressCallback,只有在 onStart() 的时候,才会真正注册 BackPressCallback 到 Dispatcher,而 onStop() 的时候,会自动移除 BackPressCallback。

下面看一下 Activity 中怎么使用的 Dispacher。

直接在 onBackPressed() 里面调用了我们的 Dispacher 去分发。而 Dispacher 在构造函数里面实例了一个 Runnable,也就是当没有 BackPressCallback 响应BackPress 时,直接调用 super.onBackPressed() 方法,和我们实现的一样,只不过挪了一个地方。

下面看一看使用:

/   好在哪里?   /

写了这么多,变成源码分析了,不过下面讨论下官方代码更高明的地方。

提供 Owner 而不是直接提供方法

我的方案是直接在 BaseActivity 增加一个注册方法,而官方选择通过提供 Owner 让我们自己去访问 Dispacher 注册,其实可以看到官方很多地方都是这样设计的,比如我们的 LifecycleOwner 等等。事实证明这种 Owner 模式是很有好处的,他可以使外部可以直接和 Dispacher 打交道,不需要通过 Activity 这一层中转,这样一旦我们的 Dispacher 功能变得复杂,我们的 Activity 不会增加额外的中转方法。

同时,使用接口能更加清晰的让外部得知得知你拥有的功能。 

使用 DescendingIterator 倒叙遍历

也是第一次知道有这个 API,相比于我的倒叙 for 循环,使用倒叙迭代器无疑更加优雅。

一旦接管,全部接管

可以发现官方把 super.onBackPressed() 也封装在了一个 Runnable 中,这样的话,相当于完全接管 onBackPressed(),实现了统一性。

更加巧妙运用 Lifecycle

可以发现官方的代码通过 Lifecycle 将注册 OnBackPressedCallback 控制在了 onStart() 之后,即使你的注册代码是放在之前的一些方法里面,如 onCreate() 或者 onAttach() 。这样更加安全的避免了错误的发生。比如使用我的方案,我如果在 Fragment 的 onCreate()  或者 onAttach() 里面注册,那么可能会导致响应到 BackPress 时而 View 还没有创建好的情况,导致 NPE。 

结尾

其它的方面就夸不动了,不过官方还有两个地方确实打破了我的传统思维,第一个就是那命名为 Callback 就一定得是 Callback 吗?不,也可以是抽象类。第二个是接口方法返回的对象就一定是接口吗?也不一定,也可以直接返回某个具体的类。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

啥也不说了,Room真香!

Compose Android开发终极挑战赛: 写一个天气应用

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值