您当前的位置:首页 > 互联网教程

Android 多返回栈技术详解

发布时间:2025-05-11 21:16:46    发布人:远客网络

Android 多返回栈技术详解

一、Android 多返回栈技术详解

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈(back stack)。多返回栈即一堆"返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01和 Fragment 1.4.0-alpha01中开始的。本文将为您展开多返回栈的技术详解。

无论您在使用 Android全新的手势导航还是传统的导航栏,用户的"返回"操作是 Android用户体验中关键的一环,把握好返回功能的设计可以使应用更加贴近整个生态系统。

在最简单的应用场景中,系统返回按钮仅仅 finish您的 Activity。在过去您可能需要覆写 Activity的 onBackPressed()方法来自定义返回操作,而在 2021年您无需再这样操作。我们已经在 OnBackPressedDispatcher中提供了针对自定义返回导航的 API。实际上这与 FragmentManager和 NavController中已经添加的 API相同。

这意味着当您使用 Fragments或 Navigation时,它们会通过 OnBackPressedDispatcher来确保您调用了它们返回栈的 API,系统的返回按钮会将您推入返回栈的页面逐层返回。

多返回栈不会改变这个基本逻辑。系统的返回按钮仍然是一个单向指令——"返回"。这对多返回栈 API的实现机制有深远影响。

在 surface层级,对于多返回栈的支持貌似很直接,但其实需要额外解释一下"Fragment返回栈"到底是什么。FragmentManager的返回栈其实包含的不是 Fragment,而是由 Fragment事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API的事务组成的。

这就意味着当您调用 commit()提交了一个调用过 addToBackStack()方法的 Fragment事务时, FragmentManager会执行所有您在事务中所指定的操作(比如替换操作),从而将每个 Fragment转换为预期的状态。然后 FragmentManager会将该事务作为它返回栈的一部分。

当您调用 popBackStack()方法时(无论是直接调用,还是通过系统返回键以 FragmentManager内部机制调用),Fragment返回栈的最上层事务会从栈中弹出--比如新添加的 Fragment会被移除,隐藏的 Fragment会显示。这会使得 FragmentManager恢复到最初提交 Fragment事务之前的状态。

也就是说 popBackStack()变成了销毁操作:任何已添加的 Fragment在事务被弹出的时候都会丢失它的状态。换言之,您会失去视图的状态,任何所保存的实例状态(Saved Instance State),并且任何绑定到该 Fragment的 ViewModel实例都会被清除。这也是该 API和新的 saveBackStack()方法之间的主要区别。 saveBackStack()可以实现弹出事务所实现的返回效果,此外它还可以确保视图状态、已保存的实例状态,以及 ViewModel实例能够在销毁时被保存。这使得 restoreBackStack() API后续可以通过已保存的状态重建这些事务和它们的 Fragment,并且高效"重现"已保存的全部细节。太神奇了!

而实现这个目的必须要解决大量技术上的问题。

虽然 Fragment总是会保存 Fragment的视图状态,但是 Fragment的 onSaveInstanceState()方法只有在 Activity的 onSaveInstanceState()被调用时才会被调用。为了能够保证调用 saveBackStack()时 SavedInstanceState会被保存,我们还需要在 Fragment生命周期切换的正确时机注入对 onSaveInstanceState()的调用。我们不能调用得太早(您的 Fragment不应该在 STARTED状态下保存状态),也不能调用得太晚(您需要在 Fragment被销毁之前保存状态)。

这样的前提条件就开启了需要解决 FragmentManager转换到对应状态的问题,以此来保障有一个地方能够将 Fragment转换为所需状态,并且处理可重入行为和 Fragment内部的状态转换。

在 Fragment的重构工作进行了 6个月,进行了 35次修改时,发现 Postponed Fragment功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态——既没有被提交也并不是未被提交。之后的 65个修改和 5个月的时间里,我们几乎重写了 FragmentManager管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment:使用新的状态管理器》。

随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack()和 restoreBackStack()。

如果您不使用这些新增 API,则一切照旧:单个 FragmentManager返回栈和之前的功能相同。现有的 addToBackStack()保持不变——您可以将 name赋值为 null或者任意 name。然而,当您使用多返回栈时, name的作用就非常重要了:在您调用 saveBackStack()和之后的 restoreBackStack()方法时,它将作为 Fragment事务的唯一的 key。

举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace操作:

也就是说我们的 FragmentManager会变成这样:

比如说我们希望将 profile页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack()并且紧跟一个新的事务:

现在我们添加 ProfileFragment的事务和添加 EditProfileFragment的事务都保存在"profile"关键字下。这些 Fragment已经完全将状态保存,并且 FragmentManager会随同事务状态一起保持它们的状态。很重要的一点:这些 Fragment的实例并不在内存中或者在 FragmentManager中——存在的仅仅只有状态(以及任何以 ViewModel实例形式存在的非配置状态)。

替换回来非常简单:我们可以在"notifications"事务中同样调用 saveBackStack()操作,然后调用 restoreBackStack():

维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时, FragmentManager和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment返回栈的最后一个事务。

这些 API都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment的视图状态、已保存的实例状态、非配置的状态。

当然了,如果您不希望在这些 API之上构建您的框架,那么可以使用我们所提供的框架进行开发。

Navigation Component最初是作为通用运行时组件进行开发的,其中不涉及 View、Fragment、Composable或者其他屏幕显示相关类型及您可能会在 Activity中实现的"目的地界面"。然而,NavHost接口的实现中需要考虑这些内容,通过它添加一个或者多个 Navigator实例时,这些实例确实清楚如何与特定类型的目的地进行交互。

这也就意味着与 Fragment的交互逻辑全部封装在了 navigation-fragment开发库和它其中的 FragmentNavigator与 DialogFragmentNavigator中。类似的,与 Composable的交互逻辑被封装在完全独立的 navigation-compose开发库和它的 ComposeNavigator中。这里的抽象设计意味着如果您希望仅仅通过 Composable构建您的应用,那么当您使用 Navigation Compose时无需任何涉及到 Fragment的依赖。

该级别的分离意味着 Navigation中有两个层次来实现多返回栈:

仍需特别注意那些尚未更新的 Navigator,它们无法支持保存自身状态。底层的 Navigator API已经整体重写来支持状态保存(您需要覆写新增的 navigate()和 popBackStack() API的重载方法,而不是覆写之前的版本),即使 Navigator并未更新, NavController仍会保存 NavBackStackEntry的状态(在 Jetpack世界中向后兼容是非常重要的)。

如果您仅仅在应用中使用 Navigation,那么 Navigator这个层面更多的是实现细节,而不是您需要直接与之交互的内容。可以这么说,我们已经完成了将 FragmentNavigator和 ComposeNavigator迁移到新的 Navigator API的工作,使其能够正确地保存和恢复它们的状态,在这个层面上您无需再做任何额外工作。

如果您正在使用 NavigationUI,它是用于连接您的 NavController到 Material视图组件的一系列专用助手,您会发现对于菜单项、 BottomNavigationView(现在叫 NavigationRailView)和 NavigationView,多返回栈是默认启用的。这就意味着结合 navigation-fragment和 navigation-ui使用就可以。

NavigationUI API是基于 Navigation的其他公共 API构建的,确保您可以准确地为自定义组件构建您自己的版本。保证您可以构建所需的自定义组件。启用保存和恢复返回栈的 API也不例外,在 Navigation XML中通过 NavOptions上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack()的重载方法可以帮助您指定 pop操作保存状态或者指定 navigate操作来恢复之前已保存的状态。

比如,在 Compose中,任何全局的导航模式(无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的形式)都可以使用我们在与底部导航栏集成所介绍的相同的技术,并且结合 saveState和 restoreState属性一起调用 navigate():

对用户来说,最令人沮丧的事情之一便是丢失之前的状态。这也是为什么 Fragment用一整页来讲解保存与 Fragment相关的状态,而且也是我非常乐于更新每个层级来支持多返回栈的原因之一:

如果您希望了解更多使用该 API的示例,请参考 NavigationAdvancedSample(它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions代码)。

对于 Navigation Compose的示例,请参考 Tivi。

如果您遇到任何问题,请使用官方的问题追踪页面提交关于 Fragment或者 Navigation的 bug,我们会尽快处理。

二、移动开发基础教程:Android是什么 安卓开发

Android是基于Linux内核的操作系统,是谷歌公司在2007年11月5日公布的手机操作系统,早期由谷歌开发,后由开放手持设备联盟(Open Handset Alliance)开发。它采用了软件堆层(software stack,又名以软件叠层)的架构,主要分为三部分。底层Linux内核只提供基本功能;其他的应用软件则由各公司自行开发,部分程序以Java编写。

BlackBerry和iPhone都提供了受欢迎的、高容量的移动平台,但是却分别针对两个不同的消费群体。BlackBerry是企业业务用户的不二选择。但是,作为一种消费设备,它在易用性和“新奇特性”方面难以和iPhone抗衡。Android则是一个年轻的、有待开发的平台,它有潜力同时涵盖移动电话的两个不同消费群体,甚至可能缩小工作和娱乐之间的差别。

如今,很多基于网络或有网络支持的设备都运行某种Linux内核。这是一种可靠的平台:可经济有效地进行部署和提供支持,并且可直接作为面向部署的良好的设计方法。这些设备的UI通常是基于HTML的,可通过PC或Mac浏览器查看。但并不是每个设备都需要通过一个常规的计算设备来控制。想象一下传统的家用电器,例如电炉、微波炉或面包机。如果您的家用电器由Android控制,并且有一个彩色触摸屏,会怎么样?如果电炉上有一个Android UI,那么操控者甚至可以烹饪点什么东西。

在Eclipse环境中开发Android应用程序需要了解Eclipse环境和Android平台的知识。了解以下术语会有助于用Eclipse插件开发Android应用程序。

这是一个由谷歌公司主导的组织,它由许多公共和私人组织组成。

这是Open Handset Alliance的主打产品。它是一种针对移动设备的开放源码操作环境。

模拟另一个系统的软件工具—这常常是在个人计算机(IBM、Mac、Linux)上运行的一个环境,它模拟另一个环境,比如移动计算设备。

一种开放源码的操作系统内核,许多计算平台都使用这种操作系统,包括服务器、桌面计算机、网络设备和移动计算设备。Android在Linux内核上运行。

Dalvik VM是Android产品组合中的一种操作环境,它在运行时解释应用程序代码。Dalvik VM与Java VM相似,但是两者不兼容。

Android平台是Open Handset Alliance的成果,Open Handset Alliance组织由一群共同致力于构建更好的移动电话的公司组成。这个组织由谷歌领导,包括移动运营商、手持设备制造商、零部件制造商、软件解决方案和平台提供商以及市场营销公司。从软件开发的观点看,Android正处在开源领域的中心位置。

市场上第一款支持Android的手机是由HTC制造并由T-Mobile供应的G1。这款设备从设想到推出花了大约一年的时间,惟一可用的软件开发工具是一些实行增量改进的SDK发行版。随着G1发行日的临近,Android团队发布了SDK V1.0,用于这个新平台的应用程序也浮出水面。

为了鼓励创新,谷歌举办了两届“Android Developer Challenges”,为优胜的参赛作品提供数百万美金的奖励。G1问世几个月之后,随后就发布了Android Market,它使用户可以浏览应用程序,并且可以将应用程序直接下载到他们的手机上。经过大约 18个月,一个新的移动平台进入公众领域。

Android平台被称为一个产品组合,因为它是一系列组件的集合,包括:

基于Linux内核的操作系统 Java编程环境工具集,包括编译器、资源编译器、调试器和模拟器用来运行应用程序的Dalvik VM

Android有丰富的功能,因此很容易与桌面操作系统混淆。Android是一个分层的环境,构建在Linux内核的基础上,它包括丰富的功能。UI子系统包括:

窗口视图用于显示一些常见组件(例如编辑框、列表和下拉列表)的小部件。

Android包括一个构建在WebKit基础上的可嵌入浏览器,iPhone的Mobile Safari浏览器同样也是以WebKit为基础。

Android提供多种连接选项,包括WiFi、蓝牙和通过蜂窝(cellular)连接的无线数据传输(例如GPRS、EDGE和3G)。Android应用程序中一项流行的技术是链接到谷歌地图,以便在应用程序中显示地址。Android软件栈还提供对基于位置的服务(例如GPS)和加速计的支持,不过并不是所有的Android设备都配备了必需的硬件。另外还有摄像支持。

过去,移动应用程序努力向桌面应用程序看齐的两个领域分别是图形/媒体和数据存储方法。Android通过提供对2D和3D图形的内置支持,包括OpenGL库,解决了图形方面的挑战。由于Android平台包括流行的开源SQLite数据库,因此缓解了数据存储的负担。图1显示一个简化的Android软件层次结构。

如前所述,Android运行在Linux内核上。Android应用程序是用Java编程语言编写的,它们在一个虚拟机(VM)中运行。需要注意的是,这个VM并非您想象中的JVM,而是Dalvik Virtual Machine,这是一种开源技术。每个Android应用程序都在Dalvik VM的一个实例中运行,这个实例驻留在一个由Linux内核管理的进程中,如下图所示。

Android应用程序由一个或多个组件组成:

具有可视UI的应用程序是用活动实现的。当用户从主屏幕或应用程序启动器选择一个应用程序时,就会开始一个动作。

服务应该用于任何需要持续较长时间的应用程序,例如网络监视器或更新检查应用程序。

可以将内容提供程序看作数据库服务器。内容提供程序的任务是管理对持久数据的访问,例如SQLite数据库。如果应用程序非常简单,那么可能不需要创建内容提供程序。如果要构建一个较大的应用程序,或者构建需要为多个活动或应用程序提供数据的应用程序,那么可以使用内容提供程序实现数据访问。

Android应用程序可用于处理一个数据元素,或者对一个事件(例如接收文本消息)做出响应。

Android应用程序是连同一个AndroidManifest.xml文件一起部署到设备的。AndroidManifest.xml包含必要的配置信息,以便将它适当地安装到设备。它包括必需的类名和应用程序能够处理的事件类型,以及运行应用程序所需的许可。例如,如果应用程序需要访问网络—例如为了下载一个文件—那么manifest文件中必须显式地列出该许可。很多应用程序可能启用了这个特定的许可。这种声明式安全性有助于减少恶意应用程序损害设备的可能性。

开始开发Android应用程序的最简捷的方式是下载AndroidSDK和EclipseIDE。Android开发可以在微软Windows、Mac OS X或Linux上进行。

本文假设您使用的是Eclipse IDE和用于Eclipse的Android Developer Tools插件。Android应用程序是用Java语言编写的,但是是在Dalvik VM(非Java虚拟机)中编译和执行的。在Eclipse中用Java语言编程非常简单;Eclipse提供一个丰富的Java环境,包括上下文敏感帮助和代码提示。Java代码通过编译后,Android Developer Tools可确保适当地将它打包,包括AndroidManifest.xml文件。

虽然没有Eclipse和Android Developer Tools插件也可以开发Android应用程序,但是那样就需要熟悉Android SDK。

AndroidSDK是作为一个ZIP文件发布的,可以将该文件解压到硬盘上的一个目录中。由于有多个SDK更新,建议有意识地组织开发环境,以便在不同的SDK安装之间轻松地切换。

Java归档文件,其中包含构建应用程序所需的所有的Android SDK类。

本地和网上提供的SDK文档。这些文档的主要形式为JavaDocs,以便于在SDK中导航大量的包。文档还包括一个高级开发指南和Android社区的链接。

Samples子目录包含各种应用程序的源代码,包括ApiDemo,该应用程序演示了很多API。这个示例应用程序可以作为Android应用程序开发的良好起点。

包含所有用于构建Android应用程序的命令行工具。最常用、最有用的工具是adb实用程序(Android Debug Bridge)。

该目录包含将开发环境连接到支持Android的设备(例如G1或Android Dev 1解锁开发手机)所需的驱动程序。只有Windows平台的开发人员才需要这些文件。

Android应用程序可以在实际的设备上运行,也可以在Android SDK附带的Android Emulator上运行。图 3显示Android Emulator的主屏幕。

adb实用程序支持一些可选命令行参数,以提供强大的特性,例如复制文件到设备或从设备复制文件。可以使用shell命令行参数连接到手机本身,并发送基本的shell命令。图 4显示在通过USB线连接到Windows笔记本电脑的一个实际设备上运行的adb shell命令。

显示网络配置,网络配置可显示多个网络连接。注意这多个网络连接:

tiwlan0是WiFi连接,该连接由本地DHCP服务器提供一个地址。

将目录改为/data/app,其中存放用户应用程序。

列出包含某个应用程序的目录。Android应用程序文件实际上是归档文件,可通过WinZip之类的软件查看。扩展名为apk。

发出ping命令,查看Google.com是否可用。

从相同的命令提示符环境中,还可以与SQLite数据库交互,启动程序以及执行许多其他系统级任务。想像一下您正在连接到电话,因此这是非常了不起的功能。

三、Android 开发中,有哪些坑需要注意

1.为Activity声明系统配置变更事件

系统配置变更事件是指转屏,区域语言发生变化,屏幕尺寸发生变化等等,如果Activity没有声明处理这些事件,发生事件时,系统会把Activity杀掉然后重启,并尝试恢复状态,Activity有机会通过onSaveInstanceState()保存一些基本数据到Bundle中,然后此Bundle会在Activity的onCreate()中传递过去。虽然这貌似正常,但是这会引发问题,因为很多其他的东西比如Dialog等是要依赖于具体Activity实例的。所以这种系统默认行为通常都不是我们想要的。

为了避免这些系统默认行为,就需要为Activity声明这些配置,如下二个是每个Activity必须声明的:

<activity android:configChanges="orientation|keyboardHidden">

几乎所有的Activity都要声明如上,为什么Android不把它们变成Default的呢?

这好像是废话,在Android上面开发不用Android API用什么?因为Android几乎支持Java SE所有的API,所以有很多地方Android API与Java SE的API会有重复的地方,比如说对于文件的操作最好使用Android里面Context封装的API,而不要直接使用File对象:

Context.openFileOutput(String);// no File file= new File(String)

原因就是API里面会考虑到Android平台本身的特性;再如,少用Thread,而多使用AsyncTask等。

3.要考虑到Activity和进程被杀掉的情况

如了通常情况退出Activity外,还有Activity因其他原因被杀的情况,比如系统内存过低,系统配置变更,有异常等等,要考虑和测试这种情况,特别是Activity处理重要的数据时,做好的数据的保存。

有些语言真的很啰嗦,中文或英文很简短就能表达的事情到了其他语言就变的死长死长的,所以如果是wrap_content就可能把其他控制挤出可视范围;如果是指定长度就可能显示不全。也要注意特殊语言比如那些从右向左读的语言。

一是组件的对象都比较大,实现接口比较浪费,而且让代码更不易读和理解;另外更重要的是导致多方引用,可能会引发内存泄露。

6.用getApplication()来取Context当参数

对于需要使用Context对象作为参数的函数,要使用getApplication()获取Context对象当参数,而不要使用this,除非你需要特定的组件实例!getApplication()返回的Context是属于Application的,它会在整个应用的生命周期内存在,远大于某个组件的生命周期,所以即使某个引用长期持有Context对象也不会引发内存泄露。

7.主线程只做UI控制和Frameworks回调相关的事。附属线程只做费时的后台操作。交互只通过Handler。这样就可以避免大量的线程问题。

8. Frameworks的回调不要做太多事情仅做必要的初始化,其他不是很重要的事情可以放到其他线程中去做,或者用Handler Schedule到稍后再做。

至少为hdpi, mdpi, ldpi准备图片和布局。元素的单位也尽可能的使用dip而不要用px。

几乎所有的Android手机都有BACK和MENU,它们的作用是返回和弹出菜单,所以就不要再在UI中设计返回按扭和菜单按扭。很多优秀的应用如随手记和微信都有返回键,他们之所以有是因为他们都是从iOS上移植过来的,为了保存体验的一致,所以也有了返回和菜单。但这不够Android化,一个纯正的Android是没有必须重复硬键的功能的。