【背上Jetpack之Fragment】从源码的角度看Fragment 返回栈 附多返回栈demo

标签: Jetpack

系列文章

【背上Jetpack】Jetpack 主要组件的依赖及传递关系

【背上Jetpack】AdroidX下使用Activity和Fragment的变化

【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势

【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇

前言

上一篇 我们介绍了 OnBackPressedDispather ,那么今天我们来正式地从源码的角度看看 fragment 的返回栈吧。由于其主流程和生命周期差不多,因此本文将详细地分析返回栈相关的源码,并插入大量源码。建议将生命周期流程熟悉后阅读本文。文末提供单返回栈和多返回栈的 demo

如果您对 activity 对任务栈和返回栈不是很了解,可以移步 Tasks and the Back Stack

小问号你是否有很多朋友?

在分析源码之前,我们先来思考几个问题。

  • 返回栈中的元素是什么?
  • 谁来管理 fragment 的返回栈?
  • 如何返回?

返回栈中的元素是什么?

返回栈,顾名思义,是一个栈结构。所以我们要搞清楚,这个栈结构到底存的是什么。

我们都知道,使用 fragment 的返回栈需要调用 addToBackStack("") 方法

从源码角度看 Fragment 生命周期 一文中,我们提到了 FragmentTransaction ,它是一个「事务」的模型,事务可以回滚到之前的状态。所以当触发返回操作时,就是将之前提交的事务进行回滚。

FragmentTransaction 的实现类为 BackStackRecord ,所以 fragment 的返回栈其实存放的就是 BackStackRecord

作为返回栈的元素,BackStackRecord 实现了FragmentManager.BackStackEntry 接口

BackStackRecord

BackStackRecord 的定义我们可以发现 BackStackRecord 有三种身份

  • 继承了 FragmentTransaction,即是事务,保存了整个事务的全部操作
  • 实现了 FragmentManager.BackStackEntry ,作为回退栈的元素
  • 实现了OpGenerator ,可以生成 BackStackRecord 列表,后文详细介绍

谁来管理 fragment 的返回栈?

我们已经知道 fragment 的返回栈其实存放的是 BackSrackRecord , 那么谁来管理 fragment 的返回栈?

FragmentManager 用于管理 fragment ,所以 fragment 返回栈也应该由 FragmentManager 管理

//FragmentManager.java
ArrayList<BackStackRecord> mBackStack;

其实触发 fragment 的返回逻辑有两种途径

  • 开发主动调用 fragment 的返回方法

  • 用户按返回键触发

后文我们会从这两个角度分析一下 fragment 中的返回栈逻辑究竟是怎样的

如何返回?

我们已经知道返回栈中的元素是 BackStackRecord ,也清楚了是 FragmentManager 来管理返回栈。那么如果让我们来实现「返回」逻辑,应该如何做?

首先我们要清楚所谓的「返回」是对事务的回滚,即 对 commit 事务的内部逻辑执行相应的「逆操作」

例如

addFragment←→removeFragment

showFragment←→hideFragment

attachFragment←→detachFragment

有的小伙伴可能会疑惑 replace 呢?

expandReplaceOps 方法会把 replace 替换(目标 fragment 已经被 add )成相应的 remove 和 add 两个操作,或者(目标 fragment 没有被 add )只替换成 add 操作

popBackStack 系列方法

FragmentManager 中提供了popBackStack 系列方法

popBackStack系列方法

是否觉得很眼熟?提交事务也有类似的api,commit 系列方法

commit系列方法

这里分别提供了同步和异步的方法,可能有读者会疑惑,同样是对事务的操作,一个为提交,一个为回滚,为什么一个封装到了 FragmentManager 中,一个却在 FragmentTransaction 中。既然都是对事务的操作,应该都放在FragmentManager 中。我认为可能为了api使用的方便,使得 FragmentManager 开启事务的链式调用一气呵成。各位有什么想法欢迎在评论区留言。

这里主要介绍一下 popBackStack(String name, int flag)

name 为 addToBackStack(String name) 的参数,通过 name 能找到回退栈的特定元素,flag可以为 0 或者FragmentManager.POP_BACK_STACK_INCLUSIVE,0 表示只弹出该元素以上的所有元素,POP_BACK_STACK_INCLUSIVE 表示弹出包含该元素及以上的所有元素。这里说的弹出所有元素包含回退这些事务。如果这么说比较抽象的话,看图

//flag 传入0,弹出 ♥2 上的所有元素
childFragmentManager.popBackStack("♥", 0)

flag为0

//flag 为 POP_BACK_STACK_INCLUSIVE 弹出包括该元素及及以上的元素
childFragmentManager.popBackStack("♥",  androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)

flag为1

走进源码

1. popBackStack() 逻辑

在分析返回栈源码之前我们回顾一下 FragmentManager 提交事务到 fragment 各个生命周期的流程

异步

commitNow

下面我们看看 popBackStack 的源码

popBackStack源码

等等,这个 enqueueAction 有些眼熟…

commit

commitInternal

看来提交事务和回滚事务的流程基本是相同的,只是传递的 action 不同

enqueueAction

OpGenerator

由源码可知,OpGenerator 是一个接口,其内只有一个 generateOps 方法,用于生成事务列表以及对应的该事务是否是弹出的。有两个实现类

OpGenerator实现类

由此可见 commit 调用的为 BackStackRecordgenerateOps 方法,popBackStack 调用的是 PopBackStackState 中的 generateOps

前者的逻辑很简单,向 records list 中添加数据, isRecordPop list 全部传入 false

records.add(this);
isRecordPop.add(false);

后者的逻辑稍微复杂些,其内部调用了 popBackStackState 方法

如果是 popBackStack 方法 ,则将 FragmentManager 的返回栈列表(mBackStack)的栈顶移除, isRecordPop list 全部传入 true

int last = mBackStack.size() - 1;
records.add(mBackStack.remove(last));
isRecordPop.add(true);

如果传入的 name 或 id 有值,且 flag 为 0,则找到返回栈(倒序遍历)中第一个符合 name 或 id 的位置,并将该位置上方的所有 BackStackRecord 并添加到 record list 中,同时 isRecordPop list 全部传入 true

index = mBackStack.size() - 1;
while (index >= 0) {
    BackStackRecord bss = mBackStack.get(index);
    if (name != null && name.equals(bss.getName())) {
        break;
    }
    if (id >= 0 && id == bss.mIndex) {
        break;
    }
    index--;
}

for (int i = mBackStack.size() - 1; i > index; i--) {
  records.add(mBackStack.remove(i));
  isRecordPop.add(true);
}

如果传入的 name 或 id 有值,且 flag 为 POP_BACK_STACK_INCLUSIVE,则在上一条获取位置的基础上继续遍历,直至栈底或者遇到不匹配的跳出循环,接着出栈所有 BackStackRecord

//index 操作与上方相同,先找到返回栈(倒序遍历)中第一个符合 name 或 id 的位置
if ((flags & POP_BACK_STACK_INCLUSIVE) != 0) {
    index--;
    // 继续遍历 mBackStack 直至栈底或者遇到不匹配的跳出循环
    while (index >= 0) {
        BackStackRecord bss = mBackStack.get(index);
        if ((name != null && name.equals(bss.getName()))
                || (id >= 0 && id == bss.mIndex)) {
            index--;
            continue;
        }
        break;
    }
}
//后续出栈逻辑与上方相同

可以配合上面的动图理解

入栈和出栈后续的逻辑大体是相同的,只是根据 isPop 的正负出现了分支,出栈调用的是 executePopOps

上文我们有提到,「返回」逻辑实际上就是执行提交事务内部操作逻辑的「逆操作」

那么接下的逻辑就很清晰了,根据不同的 mCmd 执行相应的逆操作

void executePopOps(boolean moveToState) {
    for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
        final Op op = mOps.get(opNum);
        Fragment f = op.mFragment;
        switch (op.mCmd) {
            case OP_ADD:
                mManager.removeFragment(f);
                break;
            case OP_REMOVE:
                mManager.addFragment(f);
                break;
            case OP_HIDE:
                mManager.showFragment(f);
                break;
            case OP_SHOW:
                mManager.hideFragment(f);
                break;
            case OP_DETACH:
                mManager.attachFragment(f);
                break;
            case OP_ATTACH:
                mManager.detachFragment(f);
                break;
            case OP_SET_PRIMARY_NAV:
                mManager.setPrimaryNavigationFragment(null);
                break;
            case OP_UNSET_PRIMARY_NAV:
                mManager.setPrimaryNavigationFragment(f);
                break;
            case OP_SET_MAX_LIFECYCLE:
                mManager.setMaxLifecycle(f, op.mOldMaxState);
                break;
            default:
                throw new IllegalArgumentException("Unknown cmd: " + op.mCmd);
        }
        if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) {
            mManager.moveFragmentToExpectedState(f);
        }
    }
    if (!mReorderingAllowed && moveToState) {
        mManager.moveToState(mManager.mCurState, true);
    }
}

后面的逻辑就完全一样了

popBackStack

2. fragment 是怎样拦截 activity 的返回逻辑的?

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇 一文中我们介绍了 OnBackPressedDispatcher

ComponetActivity

activity 的 onBackPressed 的逻辑主要分为两部分,判断所有注册的 OnBackPressedCallback 是否有 enabled 的,如果有则拦截,不执行后续逻辑;

fragment 拦截返回逻辑

否则着执行 mFallbackOnBackPressed.run() ,其内部逻辑为调用 ComponentActivity 父类的 onBackPressed 方法

所以我们只需看 mOnBackPressedCallbacks(ArrayDeque<OnBackPressedCallback) 是怎样被添加的以及 isEnabled 何时赋值为 true

经过查找我们发现它是在 FragmentManager 的 attachController 调用 addCallback

 mOnBackPressedDispatcher.addCallback(owner,mOnBackPressedCallback)

进而执行了


mOnBackPressedCallback 在初始化时 enabled 赋值为 false

mOnBackPressedCallback

isEnadbled 会在返回栈数量大于 0 且其 mParent 为 PrimaryNavigation 时赋值为true

而返回栈(mBackStack)的赋值在 BackStackRecordgenerateOps 方法中,且是否添加到返回栈由 mAddToBackStack 这个布尔类型的属性控制

mAddToBackStack 的赋值在 addToBackStack 方法中,这也解释了为何调用 addToBackStack 方法就能将事务加入返回栈

我们来总结一下,fragment 拦截 activity 返回栈是通过 OnBackPressedDispatcher 实现的,如果开启事务调用了 addToBackStack 方法,则 mOnBackPressedCallbackisEnabled 属性会赋值为 true,进而起到拦截 activity 返回逻辑的作用。拦截后执行 popBackStackImmediate 方法

而 popBackStack系列方法会调用 popBackStackState 构造 recordsisRecordPop 列表,isRecordPop 的内部元素的值均为true 后续流程和提交事务是一样的,根据 isRecordPop 值的不同选择执行 executePopOpsexecuteOps 方法

单返回栈和多返回栈的实现

Ian LakeFragments: Past, Present, and Future (Android Dev Summit '19)

有提到未来会提供多返回栈的 api

那么以现有的 api 如何实现多返回栈呢?

首先我要弄清楚怎样才会有多返回栈,根据上文我们知道 FragmentManager 内部持有mBackStack list,这对应着一个返回栈,如果想要实现多返回栈,则需要多个 FragmentManager,而多 FragmentManager 则对应多个 fragment

因此我们可以创建多个宿主 frament 作为导航 fragment 这样就可以用不同的宿主 fragment 的 独立的FragmentManager 分别管理各自的返回栈,如果这样说比较抽象,可以参考下图

多返回栈

图中有四个返回栈,其中最外部有一个宿主 fragment ,内部有四个负责导航的 fragment 管理其内部的返回栈,外部的宿主负责协调各个返回栈为空后如何切换至其他返回栈

单返回栈就很容易了,我们只需在同一个 FragmentManager 上添加返回栈即可

单返回栈

详情参照 demo


关于我


我是 Fly_with24

版权声明:本文为fly_with_24原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fly_with_24/article/details/104922634

智能推荐

2018.8.27

2018.8.27...

HTML 表单元素的基本样式

HTML 表单元素的基本样式 原创 ixygj197875 发布于2018-02-22 17:48:53 阅读数 2296 收藏 更新于2018-05-20 15:35:58 分类专栏: 揭秘 CSS 揭秘 CSS 收起 表单元素主要包括 label、input、textarea、select、datalist、******、progress、meter、output等,以及对表单元素进行分组的 ...

php输出语句

php输出语句 常见的输出语句 echo(): 可以一次输出多个值,多个值之间用逗号分隔。echo是语言结构(language construct),而并不是真正的函数,因此不能作为表达式的一部分使用。 print(): 函数print()打印一个值(它的参数),如果字符串成功显示则返回true,否则返回false。 print_r(): 可以把字符串和数字简单地打印出来,而数组则以括起来的键和值...

工厂模式

简介 常见的实例化对象模式。 用工厂方法替代new操作的一种模式。 当我们使用new操作实例化对象时,调用构造函数完成初始化。若初始化仅是进行赋值等简单的操作,写入构造函数即可。但如果初始化时需要执行一长串复杂的代码,将多个工作装入一个方法,是不妥的。 创建实例与使用实例分离。将创建实例所需的大量初始化工作从基类的构造函数中分离出去。 简单工厂模式、工厂方法模式针对的是一个产品等级结构;而抽象工厂...

B1105 Spiral Matrix (画图)

B1105 Spiral Matrix (25分) //第一次只拿了21分 矩阵的长和宽,求最大因子,从sqrt(num)开始枚举. 每次循环一次,s++,t--,d--,r++ 测试点四运行超时,是因为输入一个数字的时候,需要直接输出这个数字。//1分 测试点二运行超时,最后一个数字不必再while循环一次,直接输出即可。//3分 最后一个测试点卡了好久/(ㄒoㄒ)/~~ 螺旋矩阵...

猜你喜欢

Java基础=>String,StringBuffer与StringBuilder的区别

字符串常量池 什么是字符串常量池? JVM为了减少字符串对象的重复创建,其维护了一块特殊的内存,这段内存被称为字符串常量池(存储在方法区中)。 具体实现 当代码中出现字符串时,JVM首先会对其进行检查。 如果字符串常量池中存在相同内容的字符串对象,如果有,则不再创建,直接返回这个对象的地址返回。 如果字符串常量池中不存在相同内容的字符串对象,则创建一个新的字符串对象并放入常量池,并返回新创建的字符...

java调用其他java项目的Https接口

项目中是这样的: 用户拿出二维码展示,让机器识别二维码, 机器调用开门的后台系统接口, 然后开门的后台系统接口需要调用管理系统的接口, 管理系统需要判断能不能开门.这两个系统是互相独立的.当时使用http调用是没有问题的.当时后来要求必须用https.废话不说,直接代码: 我的项目中调用的是 HttpsUtils.Get(utlStr) 这个接口 开门系统接口如下图:   管理系统的接口...

Hadoop1.2.1全分布式模式配置

一 集群规划 主机名            IP                               安装的软件 &nbs...

Go语言gin框架的安装

尝试安装了一下gin,把遇到的一些小问题来记录一下 安装步骤 首先来看看官方文档,链接点这里 可以看到安装步骤很简单,就一句话 在命令行中输入这句话运行等待就好。 问题来了,因为墙的问题,go get会很慢,所以命令行里面半天什么反应也没有,不要急,慢慢等着就会看到gin-gonic/gin这个目录出现 这个时候命令行还是没有结束,表示还在下一些东西。有的时候可能心急的人就停了(比如我),然后写个...

uni-app表单组件二

input(输入框) 属性名 类型 说明 平台差异 value String 输入框的初始内容 type String input 的类型 password Boolean(默认false) 是否是密码类型 placeholder String 输入框为空时占位符 placeholder-style String 指定 placeholder 的样式 placeholder-class Strin...