⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。
这篇文章是 Android Jetpack 系列文章的第 8 篇文章,完整目录可以移步至文章末尾~
前言
- 从 androidx.activity 1.0.0 开始,Google 引入 OnBackPressedDispatcher API 来处理回退事件,旨在优化回退事件处理:你可以在任何位置定义回退逻辑,而不是依赖于 Activity#onBackPressed();
- 在这篇文章里,我将介绍 OnBackPressedDispatcher 的使用方法 & 实现原理 & 应用场景。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
- 本文相关代码可以从 DemoHall·HelloAndroidX 下载查看。
目录
- 概述
- OnBackPressedDispatcher 解决了什么问题: 在 Activity 里可以通过回调方法 onBackPressed() 处理,而 Fragment / View 却没有直接的回调方法。现在,我们可以使用 OnBackPressedDispatcher 替代 Activity#onBackPressed(),更优雅地实现回退逻辑。
- OnBackPressedDispatcher 的整体处理流程: 分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。
- OnBackPressedDispatcher 与其他方案对比: 在 OnBackPressedDispatcher 之前,我们只能通过 “取巧” 的方法处理回退事件:
+ 1、在 Fragment 中定义回调方法,从 Activity#onBackPressed() 中传递回调事件(缺点:增加了 Activity & Fragment 的耦合关系);
+ 2、在 Fragment 根布局中设置按键监听 setOnKeyListener(缺点:不灵活 & 多个 Fragment 监听冲突)。
- OnBackPressedDispatcher 有哪些 API?
主要有以下几个,其他这几个 API 都比较好理解。其中 addCallback(LifecycleOwner, callback) 会在生命周期持有者 LifecycleOwner 进入 Lifecycle.State.STARTED 状态,才会加入分发责任链,而在 LifecycleOwner 进入 Lifecycle.State.STOP 状态时,会从分发责任链中移除。
1 | java复制代码1、添加回调对象 |
- OnBackPressedDispatcher 源码分析
OnBackPressedDispatcher 源码不多,我直接带着问题入手,帮你梳理 OnBackPressedDispatcher 内部的实现原理:
3.1 Activity 如何将事件分发到 OnBackPressedDispatcher?
答:ComponentActivity 内部组合了分发器对象,返回键回调 onBackPressed() 会直接分发给 OnBackPressedDispatcher#onBackPressed()。另外,Activity 本身的回退逻辑则封装为 Runnable 交给分发器处理。
androidx.activity.ComponentActivity.java
1 | java复制代码private final OnBackPressedDispatcher mOnBackPressedDispatcher = |
3.2 说一下 OnBackPressedDispatcher 的处理流程?
答:分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。
OnBackPressedDispatcher.java
1 | java复制代码// final 回调:Activity#onBackPressed() |
3.3 回调方法执行在主线程还是子线程?
答:主线程,分发器的入口方法 Activity#onBackPressed() 执行在主线程,因此回调方法也是执行在主线程。另外,添加回调的 addCallback() 方法也要求在主线程执行,分发器内部使用非并发安全容器 ArrayDeque 存储回调对象。
3.4 OnBackPressedCallback 可以同时添加到不同分发器吗?
答:可以。
3.5 加入返回栈的Fragment 事务,如何回退?
答:FragmentManager 也将事务回退交给 OnBackPressedDispatcher 处理。首先,在 Fragment attach 时,会创建一个回调对象加入分发器,回调处理时弹出返回栈栈顶事务。不过初始状态是未启用,只有当事务添加进返回栈后,才会修改回调对象为启用状态。源码体现如下:
FragmentManagerImpl.java
1 | java复制代码// 3.5.1 分发器与回调对象(初始状态是未启用) |
如果你对 Fragment 事务缺乏清晰的概念,务必看下我之前写的一篇文章:Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析
讨论完 OnBackPressedDispatcher 的使用方法 & 实现原理,下面我们直接通过一些应用场景来实践:
- 再按一次返回键退出
再按一次返回键退出是一个很常见的功能,本质上是一种退出挽回。网上也流传着很多不全面的实现方式。其实,这个功能看似简单,却隐藏着一些优化细节,一起来看看~
4.1 需求分析
首先,我分析了几十款知名的 App,梳理总结出 4 类返回键交互:
分类 | 描述 | 举例 |
---|---|---|
1、系统默认行为 | 返回键事件交给系统处理,应用不做干预 | 微信、支付宝等 |
2、再按一次退出 | 是否两秒内再次点击返回键,是则退出 | 爱奇艺、高德等 |
3、返回首页 Tab | 按一次先返回首页 Tab,再按一次退出 | Facebook、Instagram等 |
4、刷新信息流 | 按一次先刷新信息流,再按一次退出 | 小红书、今日头条等 |
4.2 如何退出 App?
交互逻辑主要依赖于产品形态和具体应用场景,对于我们技术同学还需要考虑不同的退出 App 的方式的区别。通过观测以上 App 的实际效果,我梳理出以下 4 种退出 App 的实现方式:
- 1、系统默认行为: 将回退事件交给系统处理,而系统的默认行为是 finish() 当前 Activity,如果当前 Activity 位于栈底,则将 Activity 任务栈转入后台;
- 2、调用 moveTaskToBack(): 手动将当前 Activity 所在任务栈转入后台,效果与系统的默认行为类似(该方法接收一个 nonRoot 参数:true:要求只有当前 Activity 处于栈底有效、false:不要求当前 Activity 处于栈底)。因为 Activity 实际上并没有销毁,所以用户下次返回应用时是热启动;
- 3、调用 finish(): 结束当前 Activity,如果当前 Activity 处于栈底,则销毁 Activity 任务栈,如果当前 Activity 是进程最后一个组件,则进程也会结束。需要注意的时,进程结束后内存不会立即被回收,将来(一段时间内)用户重新启动应用为温启动,启动速度比冷启动更快;
- 4、调用 System.exit(0) 杀死应用 杀死进程 JVM,将来用户重新启动为冷启动,需要花费更多时间。
那么,我们应该如何选择呢?一般情况下,“调用 moveTaskToBack()” 表现最佳,两个论点:
- 1、两次点击返回键的目的是挽回用户,确认用户真的需要退出。那么,退出后的行为与无拦截的默认行为相同,这点 moveTaskToBack() 可以满足,而 finish() 和 System.exit(0) 的行为比默认行为更严重;
- 2、moveTaskToBack() 退出应用并没有真正销毁应用,用户重新返回应用是热启动,恢复速度最快。
需要注意,一般不推荐使用 System.exit(0) 和 Process.killProcess(Process.myPid) 来退出应用。因为这些 API 的表现并不理想:
- 1、当调用的 Activity 不位于栈顶时,杀死进程系统会立即重新启动 App(可能是系统认为 前台 App 是意外终止的,会自动重启);
- 2、当 App 退出后,粘性服务会自动重启(Service#onStartCommand() 返回 START_STICKY 的 Service),粘性服务会一致运行除非手动停止。
分类 | 应用返回效果 | 举例 |
---|---|---|
1、系统默认行为 | 热启动 | 微信、支付宝等 |
2、调用 moveTaskToBack() | 热启动 | QQ 音乐、小红书等 |
3、调用 finish() | 温启动 | 待确认(备选爱奇艺、高德等) |
4、调用 System.exit(0) 杀死应用 | 冷启动 | 待确认(备选爱奇艺、高德等) |
Process.killProcess(Process.myPid) 和 System.exit(0) 的区别? todo
4.3 具体代码实现
BackPressActivity.kt
1 | kotlin复制代码fun Context.startBackPressActivity() { |
这段代码的逻辑并不复杂,我们主要通过 OnBackPressedDispatcher#addCallback() 添加了一个回调对象,从而干预了返回键事件的逻辑:“首次点击返回键弹出提示,两秒内再次点击返回键退出应用”。
另外,需要解释下这句代码: private val binding by viewBinding(ActivityBackpressBinding::bind)
。这里其实是使用了 ViewBinding + Kotlin 委托属性的视图绑定方案,相对于传统的 findViewById、ButterKnife、Kotlin Synthetics 等方案,这个方案从多个角度上表现更好。具体分析你可以看我之前写过的一篇文章:Android Jetpack 开发套件 #4 ViewBinding 与 Kotlin 委托双剑合璧
4.4 优化:兼容 Fragment 返回栈
上一节基本能满足需求,但考虑一种情况:页面内有多个 Fragment 事务加入了返回栈,点击返回键时需要先依次清空返回栈,最后再走 “再按一次返回键退出” 逻辑。
此时,你会发现上一节的方法不会等返回栈清空就直接走退出逻辑了。原因也很好理解,因为 Activity 的回退对象的加入时机比 FragmentManagerImpl 中的回退对象加入时机更早,所以 Activity 的回退逻辑优先处理。解决方法就是在 Activtiy 回退逻辑中手动弹出 Fragment 事务返回栈。完整演示代码如下:
BackPressActivity.kt
1 | kotlin复制代码class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) { |
4.5 在 Fragment 中使用
TestFragment.kt
1 | kotlin复制代码class TestFragment : Fragment() { |
4.6 其他 finish() 方法
另外,finish() 还有一些类似的 API,可以补充了解下:
- finishAffinity():关闭当前 Activity 任务栈中,位于当前 Activity 底下的所有 Activity(例如 A 启动 B,B 启动 C,如果 B 调用 finishAffinity(),则会关闭 A 和 B,而 C 保留)。该 API 在 API 16 后引入,最好通过 ActivityCompat.finishAffinity() 调用。
- finishAfterTransition():执行转场动画后 finish Activity,需要通过 ActivityOptions 定义转场动画。该 API 在 API 21 后引入,最好通过 ActivityCompat.finishAfterTransition() 调用。
- 总结
关于 OnBackPressedDispatcher 的讨论就先到这里,给你留两个思考题:
- 1、如果 Activity 上弹出一个 Dialog,此时点返回键是先关闭 Dialog,还是会分发给 OnBackPressedDispatcher?如果弹出的是 PopupWindow 呢?
- 2、Activity 的 WebView 中弹出了一个浮层,怎么实现点击返回键先关闭浮层,再次点击才回退页面?
参考资料
- Jetpack 应用架构指南 —— 官方文档
- 提供自定义返回导航 —— 官方文档
- Fragment 的过去、现在和将来 —— 谷歌开发者
推荐阅读
Android Jetpack 系列文章目录如下(2023/07/08 更新):
- #1 Lifecycle:生命周期感知型组件的基础
- #2 为什么 LiveData 会重放数据,怎么解决?
- #3 为什么 Activity 都重建了 ViewModel 还存在?
- #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
- #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
- #6 ViewBinding 与 Kotlin 委托双剑合璧
- #7 AndroidX Fragment 核心原理分析
- #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
- #9 食之无味!App Startup 可能比你想象中要简单
- #10 从 Dagger2 到 Hilt 玩转依赖注入(一)
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
本文转载自: 掘金