ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-11-28 13:30:22  阅读:234  来源: 互联网

标签:返回 onBackPressed Fragment Jetpack private OnBackPressedDispatcher Activity Andro


================================

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) {
OnBackPressed

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

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

DispatcherOwner 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、刷新信息流按一次先刷新信息流,再按一次退出小红书、今日头条等

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.startBackPressActivity() {
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();
}
}

这段代码的逻辑并不复杂,我们主要通过 OnBackPressedDispatcher#addCallback() 添加了一个回调对象,从而干预了返回键事件的逻辑:“首次点击返回键弹出提示,两秒内再次点击返回键退出应用”。

另外,需要解释下这句代码: private val binding by viewBinding(ActivityBackpressBinding::bind)。这里其实是使用了 ViewBinding + Kotlin 委托属性的视图绑定方案,相对于传统的 findViewById、ButterKnife、Kotlin Synthetics 等方案,这个方案从多个角度上表现更好。具体分析你可以看我之前写过的一篇文章:Android | ViewBinding 与 Kotlin 委托双剑合璧

4.4 优化:兼容 Fragment 返回栈

上一节基本能满足需求,但考虑一种情况:页面内有多个 Fragment 事务加入了返回栈,点击返回键时需要先依次清空返回栈,最后再走 “再按一次返回键退出” 逻辑。

此时,你会发现上一节的方法不会等返回栈清空就直接走退出逻辑了。原因也很好理解,因为 Activity 的回退对象的加入时机比 FragmentManagerImpl 中的回退对象加入时机更早,所以 Activity 的回退逻辑优先处理。解决方法就是在 Activtiy 回退逻辑中手动弹出 Fragment 事务返回栈。完整演示代码如下:

BackPressActivity.kt

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

private val binding by viewBinding(ActivityBackpressBinding::bind)

/**

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

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

addFragmentToStack()
onBackPressedDispatcher.addCallback(this, onBackPress)

binding.ivBack.setOnClickListener {
onBackPressed()
}
}

private fun addFragmentToStack() {
// 提示:为了聚焦问题,这里不考虑 Activity 重建的场景
for (index in 1…5) {
supportFragmentManager.beginTransaction().let { it ->
it.add(
R.id.container,
BackPressFragment().also { it.text = “fragment_KaTeX parse error: Expected 'EOF', got '}' at position 8: index" }̲, "fragment_index”
)
it.addToBackStack(null)
it.commit()
}
}
}

/**

  • @return true:没有Fragment弹出 false:有Fragment弹出
    */
    private fun popBackStack(): Boolean {
    // 当 Fragment 状态以保存,不弹出返回栈
    return supportFragmentManager.isStateSaved
    || supportFragmentManager.popBackStackImmediate()
    }

private val onBackPress = object : OnBackPressedCallback(true) {

)
it.addToBackStack(null)
it.commit()
}
}
}

/**

  • @return true:没有Fragment弹出 false:有Fragment弹出
    */
    private fun popBackStack(): Boolean {
    // 当 Fragment 状态以保存,不弹出返回栈
    return supportFragmentManager.isStateSaved
    || supportFragmentManager.popBackStackImmediate()
    }

private val onBackPress = object : OnBackPressedCallback(true) {

标签:返回,onBackPressed,Fragment,Jetpack,private,OnBackPressedDispatcher,Activity,Andro
来源: https://blog.csdn.net/m0_64319112/article/details/121590162

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有