安卓现代化开发系列——从状态保存到SavedState

news2024/11/23 15:50:13

由于安卓已经诞生快二十载,其最初的开发思想与现代的开发思想已经大相径庭,特别是Jetpack库诞生之后,项目中存在着新老思想混杂的情况,让许多的新手老手都措手不及,项目大步向屎山迈进。为了解决这个问题,开发者必须弄懂新旧两种开发模式,这就是《安卓现代化开发系列》诞生的意义,本系列并不会包含隐晦难懂的代码,一切的文字都是以理解本质为主,起到一个抛钻引玉的作用。

1、为什么需要状态保存?

说「状态保存」之前,我们先讲一讲为什么需要状态保存:

常见的window、linux系统不同的是,移动端的操作系统拥有的内存更少,因此这类系统更容易面临内存不足的情况,如何最大限度利用较少的内存是移动端操作系统比较重要的问题。

对于安卓系统来说,一个Activity不可见时,即这时已经跳转到了另外一个Activity或者整个App都处于处于后台的情况下,同时它的生命周期处于「Stoped」。在这之后,一旦出现内存不足的情况,Android系统就会考虑销毁这些用户不可见的Activity,这样就可以释放它们占用的内存,给予用户目前正在交互的Activity更多的内存,避免彻底的OOM(out of momory)异常出现。

此刻就出现了一个问题,如果只是单纯的把Activity销毁了,那么之前用户操作的信息就全部丢失了,可以想象的一个场景是:用户正在编辑一段日记的时候,来了一个电话,当通话结束之后(假设此刻处于后台的编辑日记的Activity由于内存不足被销毁了),那么返回到App的时候,用户会发现花了很多时间编辑的日记已经全部丢失,这样的App逻辑是无法接受的。因此我们需要一种机制:在即将被销毁的时候保存Activity的状态,页面重建之后根据之前保存的状态恢复页面,这种“机制”就是标题所谓的「状态保存」。

2、状态在安卓中意味着什么

在安卓中,当我们提到「状态保存」的时候,开发者保存的状态其实就是某些「成员变量」。

因此,读者可以简单的理解为,当一个变量存在于View中,即此变量为View的成员变量时,此变量可能会由于View的重建而丢失,因为View此时是一个全新的实例。同理,当Activity与Fragment也会存在类似的场景丢失他们的成员变量。因此开发者需要处理这些可能会由于实例的替换导致丢失成员变量的场景,这个处理的过程就是安卓的「状态保存」。

下面结合代码理解一下:

2.1、View的实例状态

根据上文所述View中的那些成员变量就是「View的实例状态」,这里展示一个按钮案例,常见的按钮就有”选中“和”未选中“两个状态,因此开发者会用一个布尔值来存储这个状态,但是由于重建机制的存在,View会被一个新的实例代替,那么此时的View就丢失了状态了。

2.2、Activity的实例状态

一个Activity中存在着View,在View的内部存在着「View的实例状态」,同时它旁边也存在着一些Activity的成员变量,这些成员变量和View内部的状态共同组成了「Activity的实例状态」。

同样,当遇到重建的场景时,Activity会同时丢失自身的状态与View内部的实例状态(在View没有实现状态保存的情况下)。

2.3、Fragment的实例状态

Activity几乎类似,Fragment也同样存在着View与自身的成员变量,因此「View的实例状态」与这些成员变量共同组成了「Fragment的实例状态」。

需要注意的是:由于Fragment的特殊性,Fragment的生命周期与FragmentView的生命周期是不一致的,一个Fragment在自身的生命周期内可能会跨越多个View的重建,这也导致了Fragment的状态保存分裂为「成员变量的保存」与「View的实例状态的保存」,这两者在Activity中是同时发生的,而Fragment中并不一定同时。

2.4、实例状态的包含关系

由于View是依附于组件中的,因此「组件的实例状态」除了组件本身的变量,还包括了「View的实例状态」,因此当我们说组件的状态保存的时候,其实还包括了保存View的状态。

也许读者此时会联想到,Fragment也可以存在于父Fragment或者父Activity,那么它们之间的实例状态也是包含关系吗?

答案是对的,当Activity保存自身状态的时候,同时也会让它所包含的Fragment保存实例状态。

下面这张图可以展示状态关系:

3、图示状态保存与恢复

下面援引自 The Real Best Practices to Save 的几张图可以很好阐述状态保存时发生的事情:

当Activity需要保存实例状态的时候,它会先遍历所有的View让他们各自保存自己的状态,然后打包放在自己的实例状态中的某个地方,和自身的其他业务状态保存在一起。

相反,当Activity需要恢复状态的时候,它会从实例状态中找出所有View之前保存的状态,然后将他们恢复给所有的View,同时恢复自身的业务状态。

对于Fragment来说整个过程是类似的,这里就不展示了。

4、实操状态保存与恢复

4.1、View

View的状态保存与恢复核心方法是onSaveInstanceState()onRestoreInstanceState()

开发者只需要为当前的ViewonSaveInstanceState()onRestoreInstanceState()中实现图中的操作即可。

需要注意的是:

View能够实现状态保存与恢复的前提是:必须在UI树中存在唯一的ID。

换句话说,这要求了开发者必须在布局的xml中为该View赋予唯一的ID,或者动态添加的时候生成一个唯一ID。

这并不难理解,状态保存的本质是将状态缓存在某个容器中,需要恢复的时候从容器中取出来,而ID则是取的Key,如果没有Key那又如何保存状态呢?

4.2、Activity

Activity的状态保存与View类似,也是一对onSaveInstanceState()onRestoreInstanceState()方法,但是开发者大多数选择在onCreate()中恢复状态,这取决于实际的需要。

4.3、Fragment

Fragment的状态保存也与Activity类似,下面直接看图即可:

依然需要注意的是:在2.3中提到,由于Fragment的实例与UI的分离的设计模式,因此会发生只保存UI状态的情况,因此上图中的onSaveInstanceState()是不会调用的,我们从方法名中也可以看出这是保存实例状态。

5、状态保存与恢复的时机?

5.1、Activity

Activity被意外销毁时,需要保存状态,并在Activity重新恢复显示时恢复状态。

对于Activity来说,除了用户手动从当前Activity退出以外(这种情况无需状态保存),还有以下两种情况会导致Activity会被系统销毁:

  1. 配置发生变化(用户修改了手机的语言、暗夜模式等)。
  2. Activity处于「停止」状态时因系统限制(内存不足)而被销毁。

为什么用户主动按下返回按钮导致Activity销毁不需要状态保存而后两种情况需要状态保存呢?

主要的原因是前者是**「用户意料之内的行为」,而后两种情况属于「用户意料之外的行为」**。当一个用户旋转一个页面时,亦或者用户从页面A跳转到B,并稍后从B返回到A时,这两种情况用户并不希望页面的信息丢失了,否则就会出现上文出现的「编辑一半的日记被来电清空」的特殊情况,这对于用户来说是不可以接受的。

下面结合一张图来展示Activity生命周期与状态保存的关系:

由图中可见,Activity的状态保存与恢复发生在onSaveInstanceState()onRestoreInstanceState()中,具体的细节下文会解释,这里读者记住它发生的时机即可。(在安卓9之后,保存状态发生在onStop()之后,这与安卓9之前的版本有细微的差异)。

5.2、Fragment

Fragment保存状态的时机相对复杂,有好几种情况。同时保存业务状态和保存View的状态的时机并不一定是一致的。

下面援引官方文档的一句结论:

注意:仅当 fragment 的宿主 activity 调用自己的 onSaveInstanceState(Bundle) 时,系统才会调用 onSaveInstanceState(Bundle)

实际上这个结论并不能完全概括Fragment保存状态的所有时机,只是阐述了其中一种由Activity发生状态保存的时候,顺便保存其子Fragment状态的情况,而Fragment保存状态的情况还有两种,笔者下文会讲,这里先从Activity发生状态保存时开始讲起。

5.2.1、Activity状态保存时,顺便保存Fragment的状态,恢复状态同理

这种情况属于自动发生的情况,上文讲Activity的状态保存时提到,Activity的实例状态其实也包含了Fragment的实例状态,因此Activity保存状态中也包含了Fragment的状态。

通过图示来理解这个过程:

这种情况就是官方文档中提到的「宿主Activity」调用FragmentonSaveInstanceState()的时候保存状态的情况。

5.2.2、主动保存与恢复Fragment的状态

有时候Fragment并不一定要跟随Activity进行状态保存,在Activity的生命周期期间,其内部的Fragment也会主动保存与恢复状态,这暗示着这些Fragment存在着需要销毁实例的情况。

下面我们讲讲如何主动保存与恢复这些Fragment的状态:

首先我们看FragmentManager(1.5.5版本)的源码,其中存在着一个saveFragmentInstanceState(Fragment)的方法,它是public的因此开发者可以使用这个方法「主动保存一个Fragment的状态」,随后就可以抛弃掉这个Fragment实例。

还可以看到,保存的状态为SavedState,随后我们可以根据这个状态去为新创建的Fragment实例恢复刚才的状态。

那么我们应该如何为新创建的Fragment实例恢复呢?Fragment专门为这种情况提供了一个方法:

可以看到这个方法单纯就是为了初始化一个Fragment的状态,唯一需要注意的是:

setInitialSavedState(SavedState)只能在FragmentFragmentManager纳入管理之前调用。

这一对API的意义是什么呢?目的只有一个就是节省内存,因为开发者经常会遇到这种场景:

Fragment暂时不可见,希望回收它的实例但是保存状态,稍后新建一个类型相同的Fragment实例,然后用刚才保存的状态将「新建的实例」恢复成「旧的实例」的状态。

如果你熟悉ViewPager2,那么你一定了解它的一个offscreenPageLimit的机制,FragmentStateAdapter这个适配器会将那些离视窗范围太远的Fragment销毁掉,这个场景就是上述的提到的。那么它又是如何在重新回到被销毁的Fragment的位置的时候将其状态恢复的呢?

答案就是上文提到的主动恢复状态的方法:setInitialSavedState(SavedState)

虽然旧的Fragment实例被销毁了,但是ViewPager2通过保存它的状态的方式,稍后使用了一个新的Fragment与之前保存的状态恢复到了当初的样子。

虽然开发者很少会实现自己的「跨Fragment实例的状态保存恢复机制」,但是理解其本质有利于理解ViewPager等框架的基础原理。

5.2.3、Fragment进入回退栈的时候

下面引用一段来自 Fragment Lifecycle in Android - GeeksforGeeks 的图阐述一下Fragment特殊的生命周期:

在图中可以看出,从onCreate()onDestroy()的生命周期中,Fragment还可能进入一个从onCreateView()onDestroyView()的循环,这个循环的次数可能是大于1次的。

换句话说,在Fragment实例被创建到被销毁的期间,它的View也许会经历1次或以上的重新创建。

那么什么情况下会发生「只销毁View而不销毁实例」的情况呢?答案如标题所述,就是Fragment进入回退栈的时候。

当开发者使用FragmentManager执行replace操作并调用了addToBackStack()的时候,意味着「使用了一个新的Fragment替换掉了旧的Fragment」,但是这个操作是可逆的,因为操作添加进了回退栈,也就意味着,用户按返回键的时候,会返回到之前那个被替换的Fragment

这意味着,旧的Fragment只是暂时被压到了一个栈中,待会仍然可以通过退栈的方式重新回到用户的屏幕中,这个旧的Fragment会经历onPause()->onStop()->onDestroyView()的过程,但是仅此而已。它的实例没有被销毁,只是View被销毁了而已。

但是开发者仍然不需要担心View被销毁后,View中的实例状态丢失了,因为Fragment考虑到了这种情况,在FragmentManager检测到这种场景的时候,会主动让Fragment保存其contentView的状态并存放在FragmentManager中。

然而对于开发者来说,并不需要额外花心思在如何处理FragmentcontentView的状态如何被保存,因为这个本质是属于View层面的东西,了解这个机制的含义更多是解决一些开发中的隐性问题:

Fragment的实例和UI的生命周期实则是分离的,不能将两者等同,例如不能简单的使用Fragment的生命周期回调对UI进行一些操作而是使用其contentView的生命周期,否则将会发生越界访问(UI销毁了仍尝试访问)或者内存泄漏。

Fragment的UI初始化应该写在onCreateView()中而不是onCreate(),这样能避免在生命周期期间发生UI销毁,导致UI没有被重新初始化等问题。

6、古法状态保存的问题

上述分点讨论了ViewActivityFragment的古法状态保存,不知道读者是否感觉到了他们有一些设计上的缺陷呢?笔者这里总结了几点:

我们以Activity为例,回顾一下它的状态保存:

6.1、不同类型的状态之间混合在一起

如果我们将一个页面中不同业务的状态,都通过同一种方式(key-value)全部缓存在onSaveInstanceState(Bundle)提供的bundle中,那么维护起来将十分的复杂。

6.2、上层主动保存状态而不是状态持有者本身

另外还要注意的是:Activity本质上需要在自身的基础上,通过重写方法的方式来保存和恢复一些状态,然后提供这些状态给别的组件使用。

这样的问题是:状态的使用者往往不是Activity而是Activity的一些附属的组件,例如一些成员变量、View(这里的情况下把View的状态上升到了Activity来维护,也是开发中常见的一种方式)等。

如果全部的状态都通过Activity亲自来维护和恢复,如果后续需要保存的状态多起来的话,将会为Activity的开发提高了很大的负担。再者这也是违背单一权责的,单个方法中需要管理的不同业务的状态太多了。

6.3、缺乏统一的管理层

Activity、Dialog、Fragment等不同场景均依赖自身的方法,缺乏统一的代码。数据的维护可能需要团队的代码规范。

6.4、总结

古法状态保存由于历史的原因,设计的缺陷非常的大,开发者很难在复杂的业务中精准、高效地保存页面状态。

7、走向SavedState库

谷歌为了解决上述的状态保存的问题,提出了SavedState库。

让我们看看「SavedState」库的整体脉络:

图中可以看出,状态是缓存在SavedStateRegistry中的,而该Registry又通过不同的SavedStateProvider来保存不同类型的状态,达到了分类管理的效果。

单纯一幅图是没法完全理解这个库的,下面进行分点讲解:

7.1、关键类解析

7.1.1、SavedStateRegistryOwner

可以看到,SavedStateRegistryOwner是一个非常简单的接口,它的目的是对外提供一个SavedStateRegistry,这是一个集中管理状态的管理器,下文会提到这里略过。

还需要注意的是,该接口是LifecycleOwner的子类,因此它拥有感知生命周期的能力。不难理解,毕竟需要状态保存与恢复需要发生在组件恰当的生命周期中。

7.1.2、SavedStateProvider

SavedStateProvider是一个接口,它的含义是「状态提供器」,实现该接口的类本质上就是定义了如何保存一类状态的方式。当需要保存状态时,SavedStateRegistry就会让它管理的所有Provider按定义保存所有状态。

7.1.3、SavedStateRegistry

SavedStateRegistry是一个管理器,管理着一系列的SavedStateProvider,当其拥有者重建时,Registry也会重新创建一个新的实例。当Registry的拥有者(例如Activity)被创建的时候,Registry就会尝试恢复之前保存的状态。

让我们总体概览一下SavedStateRegistry的代码, 不用为复杂的代码感到担心,下面会详解:

7.1.3.1、注册与反注册「状态提供者」

上文中提到,SavedStateRegistry是一个管理一系列SavedStateProvider的容器,因此它提供了一对方法用于注册和解绑这些StateProvider,稍后这些Provider在保存与恢复状态中起到了关键作用。

7.1.3.2、恢复与消费状态

SavedStateRegistry分别提供了performRestore(Bundle?)consumeRestoredStateForKey(String?)来实现状态的恢复与消费。

从代码中可见,「恢复状态」只是从外部的Bundle中抓取一部分存放到Registry内部,并没有去执行取值的操作。如果需要从恢复后的状态中取值,则再次多次调用consumeRestoredStateForKey(String?)来取状态。

那么为什么「恢复状态」之后还要「消费状态」呢?

这里笔者的结论是:存在「恢复状态后,还不能立即消费状态」的场景。因此谷歌在设计该Api的时候,把状态的消费单独分离出来,适配更多场景。

需要注意的是:消费状态必须要在状态保存发生之后,可以使用SavedStateRegistry.isRestored来判断,否则会异常。

7.1.3.3、保存状态

保存状态的代码也非常简洁,就是将SavedStateRegistry中的所有SavedStateProvider集中打包放到外部的bundle中。

7.1.4、SavedStateRegistryController

这个类并没有什么特殊的,他只是SavedStateRegistry之间的包装类,结合SavedStateRegistryOwner做了一些生命周期上的工作,本质还是使用performRestore(Bundle?)performSave(Bundle?)两个方法与Registry进行沟通:

因为Controller多了与生命周期的监听,因此实际开发中,直接使用SavedStateRegistry还是比较少的,大多数使用SavedStateRegistryController来间接控制。

7.1.5、总结

让我们重新回到这张图中,根据刚才的解析总结一下各组件的功能:

  • SavedStateRegistryOwner:SavedStateRegistry的提供者。
  • StateRegistryController:间接控制SavedStateRegistry
  • SavedStateRegistry:状态的管理者。
  • SavedStateProvider:状态的提供者。

7.3、谷歌眼中的SavedState

我们结合谷歌提供的AndroidX代码来理解一下SavedState库是如何被使用的。

7.3.1、ComponentDialog

ComponentDialog中,存在着SavedState库的核心代码,我们抽取出来看看:

可见,该Dialog实现了SavedStateRegistryOwner接口,因此它可以对外提供SavedStateRegistry,上文中提到,由于SavedStateController包含的能力更多,因此都是直接使用SavedStateController来间接操控SavedStateRegistry

onSaveInstanceState()中,使用了Controller来保存状态,而在onCreate(Bundle?)方法中,使用了Controller来恢复状态。

还有一点值得注意的是,在initViewTreeOwners()方法中,将当前的SavedStateRegistryOwner绑定在了Dialog所在的DecorView中,这样给该Dialog下面的所有View提供了访问该DialogSavedStateRegistry的能力。

关于ViewTreeStateRegistryOwner的设计,在生命周期那一章已经简单阐述过类似的概念,不懂的读者可以回去阅读生命周期章。

7.3.2、ComponentActivity

同时我们再看看ComponentActivity,本质上和Dialog也相似,关键代码已经全部截取出来了,读者结合Dialog的代码自行领会即可。

Fragment的几乎也一样,这里就不展示了。

8、SavedState的最佳实践——SavedStateHandle

那么开发中如何使用SavedState呢,实际上开发者并不需要在项目中亲自试用SavedState,例如在Activity中直接使用SavedStateRegistry,而是配合ViewModel与其配套的SavedStateHandle一起使用。

为什么会这样呢?因为直接在Activity、Fragment中声明变量已经不适合现代mvvm等开发模式了,而是将状态和逻辑写在ViewModel中,而ActivityFragment等只做数据的订阅载体。

因此ViewModel就需要一种访问其组件上的缓存的状态的能力,这里就引出本篇文章的主角——SavedStateHandle,我们直接先看看它是如何被使用的吧:

只需要在ViewModel的构造函数中添加SavedStateHandle这个参数即可,开发者通过SavedStateHandle可以读取到ActivitygetIntent()的值,亦或者是读取到FragmentgetArguments()的值。相反的,也可以通过SavedStateHandle往里面写入值。

这种用法有什么用呢?作用是两点:

  1. 读取Activity或者Fragment的入参。
  2. 写入与读取状态,这些状态可以被状态保存机制保存起来。

第一点就不细说了,这个可以让ViewModel读取到当前所在组件的入参,做一些逻辑上的初始化工作。

这里重点是第二点,如果你了解ViewModel,那么你肯定知道ViewModel在配置更新导致的组件重建的时候,是不会销毁的,然而一旦遇到非配置更新导致的重建的情况(例如处于Stoped状态的Activity由于内存不足被系统回收),ViewModel就会被销毁了。

为了解决ViewModel在上述情况被销毁导致状态丢失的问题,开发者可以通过SavedStateHandle来写入和读取一些值,这个值会在发生状态保存的时候被写入到组件的Bundle中,并在组件组件重建的时候重新回到SavedStateHandle中,这让ViewModel拥有了读写状态的能力。

8.1、SavedStateHandle如何做到的

也许你一定很好奇SavedStateHandle是如何能够与Activity或者Fragment发生联系的,如果上述关于ComponentActivity等代码你还有印象,那么你肯定能猜到:

SavedStateHandle访问了组件的SavedStateRegistry,并在上面读取和写了状态。

让我们通过代码来看看SavedStateHandle做了什么事:

首先,ViewModel的构建都是通过工厂类反射得到的,因此我们使用了一个带参的ViewModel,那么必定存在一个对应的工厂类,这个工厂类在SavedState库中已经实现好了:

可以看到,这个工厂类在构建方法中允许传入一个外部的SavedStateRegistryOwner来获取其SavedStateRegistry,同时还传入了一个defaultArgs,还记得上面说的吗?这个参数在Activity中是getIntent().getExtras(),在Fragment中是getArguments()。我们直接在ComponentActivity的代码中验证下:

验证成功,关于Fragment读者可以亲自验证下,同样是getDefaultViewModelProviderFactory()方法。

综上我们可以得出以下结论:

  1. ViewModel默认可以使用带SaveStateHandle的参数的构造函数,因为工厂方法已经默认提供了。
  2. SavedStateHandleViewModel提供了读取组件入参、读取写入状态的能力。
  3. SavedStateHandle的能力的基础源自工厂类拥有组件的SaveStateRegistry,因此SavedStateHandle被构建时同时也传入了组件的SaveStateRegistry

下面用一张图简单描述一下它们的关系:

8.2、状态保存的思路转变

如果都通过ViewModel来保存业务中的状态,那么View又如何保存呢,毕竟View是没法直接访问ViewModel的,其实这陷入了一种思维的误区。

进入MVVM时代之后,开发者更聚焦于状态本身,通过改变状态来让UI自动发生响应,因此View本身的状态可以「上升」到ViewModel中,组件发生销毁之后,ViewModel仍可以安全的保存状态,因此重新走一遍订阅状态的流程又可以让View恢复状态了。

因此,并不是View不保存状态了,而是保存的位置迁移到了ViewModel

这里用一张新的图来阐述这种变化:

9、引入SavedStateHandle后,状态保存走向何方?

上文中提到,在引入MVVM开发思想以及对应的实现工具ViewModel之后,我们应该在ViewModel中结合SavedStateHandle来实现状态保存,但我们需要保存ViewModel中所有的属性吗?答案是不必要。

首先,基于ViewModel的视角去看一看SavedStateHandle

可以看到ViewModel存在着两种类型的属性:

  • 由SavedStateHandle直接管理的、ViewModel实例销毁时不会丢失的属性
  • 直接编码在ViewModel自身的临时变量

刚才提到,虽然开发者可以将一切变量都通过SavedStateHandle保存在状态中,避免ViewModel销毁后丢失,但是这是不必要的,为什么呢?下面从一个实际场景出发来解释下:

假设页面A是一个列表,页面入参是用户的ID,从网络中加载用户相关的推荐房间数据。

使用者进入到页面后,页面开始加载数据,同时使用者也在页面中勾选了一些筛选之类的选项。

紧接着使用者收到了来电,APP进入了后台,页面也随即进入Stoped状态。

不久之后,用户没有返回APP,而是使用了其他的APP,这导致了手机内存不足,原来的列表页面被系统回收

这个时候我们就要开始考虑哪些是需要保存的状态了:

  • 对于入参ID,我们可以得知,所有入参是存在于Intent().getExtra()中的,在开发过程中不用担心这块数据因组件以外销毁而丢失。
  • 对于筛选选项之类的,笔者认为这一块是需要保存的,最好使用SavedStateHandle处理一下。
  • 对于加载的列表,笔者认为这一块是不需要保存的,如果长时间没有回来APP,即时性比较强的列表数据其实是没有恢复的必要,重新为用户加载一份并不是特别糟糕的体验。(注:个人观点,仍需要结合实际开发场景)

下面用代码来复现上述这种场景:

代码很多,但是并不复杂,我们分别从ActivityViewModel两部分讲解:

Activity

定义了一个uid的Key值以及一个伴生方法,用于其他页面跳转到当前Activity时在Intent的Extras中附带一个用户id的参数;

添加了一个生命周期观察器,用于在进入Created时,调用ViewModel的方法去抓取数据。

ViewModel

使用savedStateHandle去读取Activity的Intent里面的Extra,用来获取用户id。

定义了一个userFilter的Key值,用于通过savedStateHandle去读写Activity的Intent,用于避免开启筛选的状态在重建组件时丢失;

定义了dataList用于缓存根据用户id请求的结果,但是此结果并不会保存在Intent中,因此会在组件因内存不足被系统销毁时丢失。

最后笔者还是要说一句,决定哪些数据需要保存哪些数据可以放弃,这个要视乎实际项目的需要。

10、总结

本章中,我们从最古早的方法回调的方式了解如何保存与恢复状态,发现出许多旧版方式存在的缺陷,然后从SavedState库着手,以一种新的方式完成状态保存。可以看出近些年来谷歌在努力着手解决安卓整体框架的缺陷。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1203241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux时间同步

搭建集群时,都会先设置时间同步,否则会出现多种问题。 方式一: 1.安装ntp软件 yum install -y ntp 2.更新时区 删除原有时区:sudo rm -f /etc/localtime 加载新时区:sudo ln -s /usr/share/zoneinfo/Asia/Shangh…

杂乱知识点记录

杂乱知识点记录 1 目标检测评估指标2 visual grounding3 分割4 VLM经典框架5 RCNN系列RCNNFast RCNNFaster RCNNMask RCNN 6 GIOU7 DETR系列DETRDeformable DETRDAB-DETRDN-DETRDINO 8 COCO20149 COCO评价指标 maxDets[1,10,100]10 FCOS:anchor-free11 ATSS 1 目标检…

公司让我开发一个管理系统,有了它,So easy!

目录 一、前言 二、低代码如何快速开发? 1.可视化开发 2.预构建的组件和模板 3.集成的开发和测试工具 4.跨平台兼容性 5.可伸缩性和可扩展性 三、前后端分离的开发框架 技术架构 一、前言 长期以来,常规软件开发是一项艰苦而详尽的工作。开发人员编写代表…

CMT2300A超低功耗127-1020MHz Sub-1GHz全频段SUB-1G 射频收发芯片

CMT2300A超低功耗127-1020MHz Sub-1GHz全频段SUB-1G 射频收发芯片 Sub-1GHz,是指小于1GHz频率的统称。Sub-1GHz无线电频段应用的主要特点:(1)频率较低波长较长,传输距离远,穿透性强;&#xff0…

阿里云国际站:专有网络vpc

文章目录 一、阿里云专有网络的概念 二、专有网络的组成部分 三、专有网络的优势 一、阿里云专有网络的概念 专有网络VPC是阿里云用户在云上创建的私有网络,用户自己掌控,可以自定义IP地址段、创建交换机、配置路由表和网关等操作。用户可以在自己的专…

假冒 Skype 应用程序网络钓鱼分析

参考链接: https://slowmist.medium.com/fake-skype-app-phishing-analysis-35c1dc8bc515 背景 在Web3世界中,涉及假冒应用程序的网络钓鱼事件相当频繁。慢雾安全团队此前曾发表过分析此类网络钓鱼案例的文章。由于Google Play在中国无法访问,许多用户…

个推「数据驱动运营增长」上海专场:携程智行火车票分享OTA行业的智能用户运营实践

近日,以“数据增能,高效提升用户运营价值”为主题的个推「数据驱动运营增长」城市巡回沙龙上海专场圆满举行。携程智行火车票用户运营负责人王银笛分享OTA行业的智能用户运营实践。 ▲ 王银笛 携程智行火车票用户运营负责人 负责智行业务线用户运营。从0…

竞赛 题目:基于FP-Growth的新闻挖掘算法系统的设计与实现

文章目录 0 前言1 项目背景2 算法架构3 FP-Growth算法原理3.1 FP树3.2 算法过程3.3 算法实现3.3.1 构建FP树 3.4 从FP树中挖掘频繁项集 4 系统设计展示5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 基于FP-Growth的新闻挖掘算法系统的设计与实现…

短剧软件APP开发方案

一、项目概述 短剧软件APP是一款集创作、拍摄、观看短剧于一体的移动应用。用户可以随时随地创作自己的短剧,也可以观看其他用户创作的短剧。本方案将详细介绍短剧软件APP的开发流程。 二、需求分析 在开发短剧软件APP之前,需要进行详细的需求分析。通…

MS321V/358V/324V低压、轨到轨输入输出运放

MS321V/MS358V/MS324V 是单个、两个和四个低压轨到轨输 入输出运放,可工作在幅度为 2.7V 到 5V 的单电源或者双电源条件 下。在低电源、空间节省和低成本应用方面是最有效的解决方案。 这些放大器专门设计为低压工作( 2.7V 到 5V )…

采集标准Docker容器日志:部署阿里云Logtail容器以及创建Logtail配置,用于采集标准Docker容器日志

文章目录 引言I 预备知识1.1 LogtailII 查询语法2.1 具体查询语法2.2 查询示例2.3 设置token时间(登录过期时间)see also引言 I 预备知识 1.1 Logtail Logtail是日志服务提供的日志采集Agent,用于采集阿里云ECS、自建IDC、其他云厂商等服务器上的日志。本文介绍Logtail的功…

飞天使-django概念之urls

urls 容易搞混的概念,域名,主机名,路由 网站模块多主机应用 不同模块解析不同的服务器ip地址 网页模块多路径应用 urlpatterns [ path(‘admin/’, admin.site.urls), path(‘’, app01views.index), path(‘movie/’, app01views.movi…

记一次线上问题引发的对 Mysql 锁机制分析

背景 最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题,当时从监控可以看到数据库活跃连接数飙升,导致应用层数据库连接池被打满,后续所有请求都因获取不到连接而失败 整体业务代码精简逻辑如下: Transaction p…

探索向量数据库 | 重新定义数据存储与分析

随着大模型带来的应用需求提升,最近以来多家海外知名向量数据库创业企业传出融资喜讯。 随着AI时代的到来,向量数据库市场空间巨大,目前处于从0-1阶段,预测到2030年,全球向量数据库市场规模有望达到500亿美元&#xff…

软文推广中媒体矩阵的优势在哪儿

咱们日常生活中是不是经常听到一句俗语,不要把鸡蛋放在同一个篮子里,其实在广告界这句话也同样适用,媒介矩阵是指企业在策划广告活动时,有目的、有计划的利用多种媒体进行广告传播,触达目标用户。今天媒介盒子就来和大…

管理压力:打工人不难为打工人

写在前面 让时间回到2018年7月末: 事件地点:中国平安办公室 事件经过: 平安产品经理提出一个需求,要求APP开发人员根据用户手机壳自动调整颜色的主题。这个需求被程序员认为是不合理的。双方开始争论,情绪激动&…

私域电商:构建商业新模式的必要性

随着互联网的快速发展,传统的电子商务模式已经无法满足企业对于个性化、精准化服务的需求。在这样的背景下,私域电商应运而生,为企业提供了新的商业机会和增长点。本文将探讨私域电商的必要性及其构建商业新模式的影响。 一、私域电商的概念 …

【Python基础】网络编程之Epoll使用一(符实操:基于epoll实现的实时聊天室)

🌈欢迎来到Python专栏 🙋🏾‍♀️作者介绍:前PLA队员 目前是一名普通本科大三的软件工程专业学生 🌏IP坐标:湖北武汉 🍉 目前技术栈:C/C、Linux系统编程、计算机网络、数据结构、Mys…

轻盈创新,气膜体育馆

气膜体育馆采用高强度、高柔性的薄膜材料为主要构建元素。其制作过程包括将膜材的外沿固定在地面基础或屋顶结构周边,并搭配智能化的机电设备,通过吹气实现室内空间的密闭。利用密闭空间内的气压支撑原理,当室内气压大于外部气压时&#xff0…