Android _ Jetpack 处理回退事件的新姿势 —— OnBackPressedDispatcher

目录

Android _ Jetpack 处理回退事件的新姿势 —— OnBackPressedDispatcher_第1张图片


前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~

  • 你真的懂 Fragment 吗?AndroidX Fragment 核心原理分析

1. 概述

  • OnBackPressedDispatcher 解决了什么问题: 在 Activity 里可以通过回调方法 onBackPressed() 处理,而 Fragment / View 却没有直接的回调方法。现在,我们可以使用 OnBackPressedDispatcher 替代 Activity#onBackPressed(),更优雅地实现回退逻辑。

  • OnBackPressedDispatcher 的整体处理流程: 分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。

  • OnBackPressedDispatcher 与其他方案对比: 在 OnBackPressedDispatcher 之前,我们只能通过 “取巧” 的方法处理回退事件:

  • 1、在 Fragment 中定义回调方法,从 Activity#onBackPressed() 中传递回调事件(缺点:增加了 Activity & Fragment 的耦合关系);

  • 2、在 Fragment 根布局中设置按键监听 setOnKeyListener(缺点:不灵活 & 多个 Fragment 监听冲突)。


2. OnBackPressedDispatcher 有哪些 API?

主要有以下几个,其他这几个 API 都比较好理解。其中 addCallback(LifecycleOwner, callback) 会在生命周期持有者 LifecycleOwner 进入 Lifecycle.State.STARTED 状态,才会加入分发责任链,而在 LifecycleOwner 进入 Lifecycle.State.STOP 状态时,会从分发责任链中移除。

1、添加回调对象
public void addCallback(OnBackPressedCallback onBackPressedCallback)

2、添加回调对象,关联到指定生命周期持有者
public void addCallback(LifecycleOwner owner, OnBackPressedCallback onBackPressedCallback)

3、判断是否有启用的回调
public boolean hasEnabledCallbacks()

4、回退事件分发入口
public void onBackPressed()

5、构造器(参数为最终回调)
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
mFallbackOnBackPressed = fallbackOnBackPressed;
}


3. OnBackPressedDispatcher 源码分析

OnBackPressedDispatcher 源码不多,我直接带着问题入手,帮你梳理 OnBackPressedDispatcher 内部的实现原理:

3.1 Activity 如何将事件分发到 OnBackPressedDispatcher?

答:ComponentActivity 内部组合了分发器对象,返回键回调 onBackPressed() 会直接分发给 OnBackPressedDispatcher#onBackPressed()。另外,Activity 本身的回退逻辑则封装为 Runnable 交给分发器处理。

androidx.activity.ComponentActivity.java

private final OnBackPressedDispatcher mOnBackPressedDispatcher =
new OnBackPressedDispatcher(new Runnable() {
@Override
public void run() {
// Activity 本身的回退逻辑
ComponentActivity.super.onBackPressed();
}
});

@Override
@MainThread
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}

@NonNull
@Override
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
return mOnBackPressedDispatcher;
}

3.2 说一下 OnBackPressedDispatcher 的处理流程?

答:分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。

OnBackPressedDispatcher.java

// final 回调:Activity#onBackPressed()
@Nullable
private final Runnable mFallbackOnBackPressed;

// 责任链
final ArrayDeque mOnBackPressedCallbacks = new ArrayDeque<>();

// 构造器
public OnBackPressedDispatcher() {
this(null);
}

// 构造器
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
mFallbackOnBackPressed = fallbackOnBackPressed;
}

// 判断是否有启用的回调
@MainThread
public boolean hasEnabledCallbacks() {
Iterator iterator = mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
if (iterator.next().isEnabled()) {
return true;
}
}
return false;
}

入口方法:责任链上的每个回调方法仅在前面的回调处于未启用状态(unEnabled)才能调用。
如果如果都没有启用,最后会回调给 mFallbackOnBackPressed
@MainThread
public void onBackPressed() {
Iterator iterator = mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
OnBackPressedCallback callback = iterator.next();
if (callback.isEnabled()) {
callback.handleOnBackPressed();
// 消费
return;
}
}
if (mFallbackOnBackPressed != null) {
mFallbackOnBackPressed.run();
}
}

3.3 回调方法执行在主线程还是子线程?

答:主线程,分发器的入口方法 Activity#onBackPressed() 执行在主线程,因此回调方法也是执行在主线程。另外,添加回调的 addCallback() 方法也要求在主线程执行,分发器内部使用非并发安全容器 ArrayDeque 存储回调对象。

3.4 OnBackPressedCallback 可以同时添加到不同分发器吗?

答:可以。

3.5 加入返回栈的Fragment 事务,如何回退?

答:FragmentManager 也将事务回退交给 OnBackPressedDispatcher 处理。首先,在 Fragment attach 时,会创建一个回调对象加入分发器,回调处理时弹出返回栈栈顶事务。不过初始状态是未启用,只有当事务添加进返回栈后,才会修改回调对象为启用状态。源码体现如下:

FragmentManagerImpl.java

// 3.5.1 分发器与回调对象(初始状态是未启用)
private OnBackPressedDispatcher mOnBackPressedDispatcher;
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
execPendingActions();
if (mOnBackPressedCallback.isEnabled()) {
popBackStackImmediate();
} else {
mOnBackPressedDispatcher.onBackPressed();
}
}
};

// 3.5.2 添加回调对象 addCallback
public void attachController(@NonNull FragmentHostCallback host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
if (mHost != null) throw new IllegalStateException(“Already attached”);

// Set up the OnBackPressedCallback
if (host instanceof OnBackPressedDispatcherOwner) {
OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);
mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();
LifecycleOwner owner = parent != null ? parent : dispatcherOwner;
mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);
}

}

// 3.5.3 执行事务时,尝试修改回调对象状态
void scheduleCommit() {

updateOnBackPressedCallbackEnabled();
}

private void updateOnBackPressedCallbackEnabled() {
if (mPendingActions != null && !mPendingActions.isEmpty()) {
mOnBackPressedCallback.setEnabled(true);
return;
}

mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0 && isPrimaryNavigation(mParent));
}

// 3.5.4 回收
public void dispatchDestroy() {
mDestroyed = true;

if (mOnBackPressedDispatcher != null) {
// mOnBackPressedDispatcher can hold a reference to the host
// so we need to null it out to prevent memory leaks
mOnBackPressedCallback.remove();
mOnBackPressedDispatcher = null;
}
}

如果你对 Fragment 事务缺乏清晰的概念,务必看下我之前写的一篇文章:你真的懂 Fragment 吗?AndroidX Fragment 核心原理分析

讨论完 OnBackPressedDispatcher 的使用方法 & 实现原理,下面我们直接通过一些应用场景来实践:


4. 再按一次返回键退出

再按一次返回键退出是一个很常见的功能,本质上是一种退出挽回。网上也流传着很多不全面的实现方式。其实,这个功能看似简单,却隐藏着一些优化细节,一起来看看~

4.1 需求分析

首先,我分析了几十款知名的 App,梳理总结出 4 类返回键交互:

分类 描述 举例
1、系统默认行为 返回键事件交给系统处理,应用不做干预 微信、支付宝等
2、再按一次退出 是否两秒内再次点击返回键,是则退出 爱奇艺、高德等
3、返回首页 Tab 按一次先返回首页 Tab,再按一次退出 Facebook、Instagram等
4、刷新信息流 按一次先刷新信息流,再按一次退出 小红书、今日头条等

Android _ Jetpack 处理回退事件的新姿势 —— OnBackPressedDispatcher_第2张图片

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

fun Context.startB

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

浏览器打开:qq.cn.hn/FTe 开源分享

ackPressActivity() {
startActivity(Intent(this, BackPressActivity::class.java))
}

class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) {

// ViewBinding + Kotlin 委托
private val binding by viewBinding(ActivityBackpressBinding::bind)

/**

  • 上次点击返回键的时间
    */
    private var lastBackPressTime = -1L

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 添加回调对象
onBackPressedDispatcher.addCallback(this, onBackPress)

// 返回按钮
binding.ivBack.setOnClickListener {
onBackPressed()
}
}

private val onBackPress = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (popBackStack()) {
return
}
val currentTIme = System.currentTimeMillis()
if (lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000) {
// 显示提示信息
showBackPressTip()
// 记录时间
lastBackPressTime = currentTIme
} else {
//退出应用
finish()
// android.os.Process.killProcess(android.os.Process.myPid())
// System.exit(0) // exitProcess(0)
// moveTaskToBack(false)
}
}
}

private fun showBackPressTip() {
Toast.makeText(this, “再按一次退出”, Toast.LENGTH_SHORT).show();
}
}

// 显示提示信息
showBackPressTip()
// 记录时间
lastBackPressTime = currentTIme
} else {
//退出应用
finish()
// android.os.Process.killProcess(android.os.Process.myPid())
// System.exit(0) // exitProcess(0)
// moveTaskToBack(false)
}
}
}

private fun showBackPressTip() {
Toast.makeText(this, “再按一次退出”, Toast.LENGTH_SHORT).show();
}
}

你可能感兴趣的:(程序员,面试,android,移动开发)