背景

在我们android开发中,如果需要actiivty/fragment等有状态的控件保存当前状态,由系统进行数据保存的恢复的时候(比如正常的暗黑模式切换/后台时低内存系统回收等等,都需要我们对当前的用户数据进行保存,不然下次重新恢复的时候,就会出现丢失数据的情况,给用户造成不太好的体验),一般都会重写onSaveInstanceState方法进行,在里面的Bundle对象进行数据的写入,然后会在onCreate阶段或者onRestoreInstanceState阶段进行数据的恢复!这套生命周期框架一直是深深嵌入在我们的开发习惯中!但是随着项目的迭代,也随着业务的发展,我们可以发现,在页面复杂度提高的同时,在onSaveInstanceState里面需要保存的数据也是越来越多这就带来了几个问题,仅举例

  1. onSaveInstanceState存储数据复杂,可能多人迭代就存在重复存取的现象,造成代码结构问题与隐藏bug的风险
  2. 不可复用,比如activity1需要存储的数据,刚好activity2也需要存储,这个时候就只能写两份代码,以此类推
  3. 没有统一的管理层,即数据的维护可能需要团队的代码规范

SavedState的登场

为了解决这些历史android的设计问题,也为了更方便广大开发者进行更好的代码结构解耦设计,所以google大哥在jetpack库中,推出了SavedState,它的定位是在Android开发中,编写可插入组件,以在进程终止时保存界面状态,并在进程重启时恢复界面状态。

虽然Savedstate推出来一段时间了,但是却一直处在“默默无闻”的状态,可能的原因有很多,比如日常开发很少接触状态保存,大部分app中真正需要保存状态的其实是很少一部分,很多app甚至是不写状态保存逻辑的,被系统回收就回收掉了,重建就是了(即使丢失了)。还有就是学习资料比较少,现在基本找不到SavedState的相关资料(区别于我们viewmodel常用的SavesStateHandle)。官方的demo例子也没有,所以Savedstate很长一段时间都是处于雪藏的状态。但是!为了让我们的app拥有更加好的体验,同时也提高我们的技术视野,学习Savedstate还是很有必要的,不然官方也不会白白将其加入jetpack系列。

理解SavedState

用法

深入理解之前呢,我们必须要会用不是嘛!我们下面来看一下例子

在Activity onCreate方法中执行以下代码注意:savedStateRegistry.isRestored在onCreate之后就会变成true,
也就是说,我们必须在ComponentActivity的onCreate走完之后才能用,
原理看下边的解析if(savedStateRegistry.isRestored){val consumeRestoredStateForKey = savedStateRegistry.consumeRestoredStateForKey("test")val test = consumeRestoredStateForKey?.getInt("test")Log.i("hello","tag test is $test")
}
savedStateRegistry.registerSavedStateProvider("test",DataProvider())
复制代码
class DataProvider: SavedStateRegistry.SavedStateProvider {override fun saveState(): Bundle {return Bundle().apply {this.putInt("test",1)}}
}
复制代码

如果对上诉程序不理解,不要紧,我们会接下来讲解!运行上诉程序,在app一开始启动的时候,log输出就是null,这个时候我们退到后台(可以设置不保留活动),再次打开的时候,可以看到activity被回收重建,这个时候log输出的却是1,这里就验证了SavedState具有保存数据的能力。

在demo中savedStateRegistry其实是ComponentActivity的一个对象,我们接下里分析一下,SavedState的概念组成

SavedState组成概念

SavedState库中,有以下几个概念

SavedStateRegistryOwner SavedStateRegistryController SavedStateRegistry SavedStateProvider
保存状态拥有者,用于提供声明周期的同步操作 控制器,相当于一个连接角色,用于连接SavedStateRegistryOwner与SavedStateRegistry 数据管理者,用于提供对存储数据的相关操作 数据提供者,用于提供存储的数据

我们再来看一下依赖关系,可以看到这个是单向依赖

看到这里,我们就对SavedState有了个初步的认识,这个时候就可以再回到demo了,首先我们的Activity是继承了ComponentActivity,而ComponentActivity其实是实现了SavedStateRegistryOwner接口的

所以我们的数据存储过程发起者都是由该activity的生命周期决定,而ComponentActivity里面拥有一个SavedStateRegistryController对象

我们可以通过SavedStateRegistryController对象,获取到SavedStateRegistry

构成已经很清楚了,我们再来解释一下上面的用法,其实分为两步:

  1. savedStateRegistry.registerSavedStateProvider,通过savedStateRegistry的registerSavedStateProvider方法注册一个数据保存集合,第一个参数就是自定义的key,第二个参数为实现SavedStateProvider接口的类,即DataProvider
  2. savedStateRegistry.consumeRestoredStateForKey可以获取当前的存储的数据集合,参数为我们自定义的key,如果当前存在key对应的数据,就返回具体存储的数据(发生在重组时),如果不存在就返回null(发生在首次进入的时候)。值得注意的一个点是,如果我们没有调用unregisterSavedStateProvider(删除对应key的存储数据)方法,那么除了首次进入之外,每次系统回收都会帮我们恢复在registerSavedStateProvider创建时的数据集合。

到这里,用法其实就很明确了,通过以上的分层,我们就可以把原本耦合在onSaveInstanceState的数据存储逻辑,变成了一个个SavedStateProvider,方便了后期数据的管理与复用,即像插件一样我们随时可以替换具体的逻辑而不影响业务本身。

原理探究

我们来看一下registerSavedStateProvider究竟做了些什么

@MainThread
public void registerSavedStateProvider(@NonNull String key,@NonNull SavedStateProvider provider) {SavedStateProvider previous = mComponents.putIfAbsent(key, provider);if (previous != null) {throw new IllegalArgumentException("SavedStateProvider with the given key is"+ " already registered");}
}
复制代码
private SafeIterableMap<String, SavedStateProvider> mComponents =new SafeIterableMap<>();
复制代码

可以看到,其实就是在mComponents里面放了一个SavedStateProvider对象,当前,如果我们之前存放过了,再次存放就会抛出异常,而mComponents,其实就是一个map对象。

那么我们SavedStateRegistry什么时候才触发这个存储逻辑呢?其实在performSave方法里面

@MainThread
void performSave(@NonNull Bundle outBundle) {Bundle components = new Bundle();if (mRestoredState != null) {components.putAll(mRestoredState);}for (Iterator<Map.Entry<String, SavedStateProvider>> it =mComponents.iteratorWithAdditions(); it.hasNext(); ) {Map.Entry<String, SavedStateProvider> entry1 = it.next();// 触发了SavedStateProvider的saveState方法components.putBundle(entry1.getKey(), entry1.getValue().saveState());}outBundle.putBundle(SAVED_COMPONENTS_KEY, components);
}
复制代码

这里有个有趣的点,它接受一个外来的Bundle,在外来的Bundle里面,再次存了一个子Bundle,而这个子Bundle,其实就是我们上文的mComponents的数据,而对应的key是一个常量

private static final String SAVED_COMPONENTS_KEY ="androidx.lifecycle.BundlableSavedStateRegistry.key";
复制代码

存放的子Bundle通过遍历的方式,触发了SavedStateProvider的saveState方法,获取到我们实际上想要保存的数据。 那么是谁调用了SavedStateRegistry的performSave方法呢?当然就是 SavedStateRegistryController啦,我们说过它其实是个中间角色,大部分操作需要经过它才能调用到SavedStateRegistry

@MainThread
public void performSave(@NonNull Bundle outBundle) {mRegistry.performSave(outBundle);
}
复制代码

而SavedStateRegistryController的performSave方法,真的被调用起来,就是在ComponenntActivity中

@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {Lifecycle lifecycle = getLifecycle();if (lifecycle instanceof LifecycleRegistry) {((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);}super.onSaveInstanceState(outState);被调用 mSavedStateRegistryController.performSave(outState);mActivityResultRegistry.onSaveInstanceState(outState);
}
复制代码

到这里我们应该豁然开朗了,其实还是换汤不换药,都是在onSaveInstanceState里面进行的逻辑保存,只不过这一层被SavedState给封装起来了,同时加入到了androidx中,也算是官方想要重新完善onSaveInstanceState架构的体现。 看到这里了,我们也就能解释存放在SavedStateProvider的数据,其实就存放在了onSaveInstanceState的Bundle中,key为SAVED_COMPONENTS_KEY的子Bundle中。

我们再来看一下consumeRestoredStateForKey,取数据的逻辑

@MainThread
@Nullable
public Bundle consumeRestoredStateForKey(@NonNull String key) {if (!mRestored) {throw new IllegalStateException("You can consumeRestoredStateForKey "+ "only after super.onCreate of corresponding component");}if (mRestoredState != null) {Bundle result = mRestoredState.getBundle(key);mRestoredState.remove(key);if (mRestoredState.isEmpty()) {mRestoredState = null;}return result;}return null;
}
复制代码

可以看到,只有mRestored为true的时候,我们才能真正进入到取数的逻辑,否则抛出异常,因为我们能够获取到数据的前提是,系统帮我们把数据进行恢复之后!而mRestored被赋值的时候,其实就在performRestore中

@SuppressWarnings("WeakerAccess")
@MainThread
void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState) {if (mRestored) {throw new IllegalStateException("SavedStateRegistry was already restored.");}if (savedState != null) {mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY);}.... mRestored = true;
}
复制代码

有了上面存数据的逻辑,我们很容易知道,performRestore的调用者,最终肯定是由实现了 SavedStateRegistryOwner接口的ComponentActivity发起调用

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {// Restore the Saved State first so that it is available to// OnContextAvailableListener instances这里就是恢复数据来源 mSavedStateRegistryController.performRestore(savedInstanceState);mContextAwareHelper.dispatchOnContextAvailable(this);super.onCreate(savedInstanceState);mActivityResultRegistry.onRestoreInstanceState(savedInstanceState);ReportFragment.injectIfNeededIn(this);if (mContentLayoutId != 0) {setContentView(mContentLayoutId);}
}
复制代码

最后我们再给出具体的流程图,存数据和取数据的逻辑:

最后

通过SavedState,我们也能够看到jetpack官方希望改变历史android架构的决心,也想要提供更加便捷优雅的方式提供给开发者。看到这里了,也希望我们在日常开发中,可以运用到SavedState进行数据保存恢复,别让它继续雪藏啦!毕竟连依赖都不需要导入呢

作者:Pika
链接:https://juejin.cn/post/7123416990768693256
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SavedState-Jetpack中被“雪藏”的状态保存利器相关推荐

  1. Android Fragment使用(三) Activity, Fragment, WebView的状态保存和恢复

    Android中的状态保存和恢复 Android中的状态保存和恢复, 包括Activity和Fragment以及其中View的状态处理. Activity的状态除了其中的View和Fragment的状 ...

  2. 转:ASP.NET状态保存方法

    ASP.NET状态保存分为客户端保存和服务器端保存两种: 使用客户端选项存储页信息而不使用服务器资源的这些选项往往具有最低的安全性但具有最快 的服务器性能,因为对服务器资源的要求是适度的.但是,由于必 ...

  3. 【Android 应用开发】Activity 状态保存 OnSaveInstanceState参数解析

    作者 : 韩曙亮 转载请著名出处 : http://blog.csdn.net/shulianghan/article/details/38297083 一. 相关方法简介 1. 状态保存方法示例 p ...

  4. asp.net中此页的状态信息无效,可能已损坏的解决之道

    asp.net中此页的状态信息无效,可能已损坏的解决之道[转] 默认分类 2009-02-06 16:16:06 阅读137 评论0 字号:大中小 针对此问题网上有一种解决办法,就是在该工程中的web ...

  5. 【Android 应用开发】Activity 状态保存 OnSaveInstanceState參数解析

    作者 : 韩曙亮 转载请著名出处 : http://blog.csdn.net/shulianghan/article/details/38297083 一. 相关方法简单介绍 1. 状态保存方法演示 ...

  6. (转)Hibernate框架基础——在Hibernate中java对象的状态

    http://blog.csdn.net/yerenyuan_pku/article/details/52760627 在Hibernate中java对象的状态 Hibernate把对象分为4种状态: ...

  7. Activity 生命周期与状态保存

    看API的时候,零零散散的记录下来的,看完了总算对Activity的生命周期有了一个全面的了解.相信会对大家有些帮助的. onCreate->onRestart->onStart-> ...

  8. 路由跳转时的页面状态保存

    前言 我们在开发网页时,经常会遇到一种情况--在一个页面对页面初始状态进行了修改(如已请求到的数据.表单数据.滚动高度等等),跳转到另外一个页面之后再返回到原页面(路由回退),原页面需要保持原先的状态 ...

  9. 魔坊APP项目-25-种植园,植物的状态改动、当果树种植以后在celery的异步任务中调整浇水的状态、客户端通过倒计时判断时间,显示浇水道具

    种植园 植物的状态改动 一.当果树种植以后在celery的异步任务中调整浇水的状态 在进行果树种植的时候, 在服务端设置当前果树到等待浇水的redis变量中.通过celery不断进行周期任务的处理, ...

最新文章

  1. python3 byte int string 互转 转换
  2. 网络营销外包——网络营销外包专员对网站标题修改都是有原因的
  3. 【采用】【风险管理】(第一篇)风险管理核心指标
  4. angular 字符串转换成数字_Angular日期在TypeScript中格式化转换应用
  5. MAC硬盘空间减少的隐藏杀手,VM到底是什么?
  6. 计算机vfp系统,计算机等级考试VFP教程:第一章数据库系统
  7. 基于php旅游网站的设计与实现
  8. 【数据库】SQL中的rollup() 函数的作用?
  9. cad角度怎么画_初学入门CAD,就这样成精了!
  10. Window7激活 电话激活小记;
  11. 根据坐标点在图片上标记
  12. JSON decoding error: Invalid UTF-8 start byte 0xb6
  13. web前端人员培训要学些什么?
  14. 【医疗健康项目】传智健康项目(三)
  15. 大学生必备的十大网站有哪些?
  16. js如何运行python代码_手把手教你如何使用Python执行js代码
  17. 使用Amazon免费云主机和Docker,快速搭建PPTP服务器!
  18. Maya动画1:基础知识小球弹跳
  19. 软考-测试评测师(两版目录对比)
  20. 显示农历的html代码,很全的显示阴历(农历)日期的js代码

热门文章

  1. mysql千万数据查重_mysql查重 去除重复数据
  2. 日语常见食物名称,感觉自己在看金手指
  3. 移动跨平台开发选型参考建议
  4. HTML层问题之如何能将文字至于视频上面?
  5. jqery图片展示效果
  6. SQL-server第三次实验(单表查询)
  7. 本周热榜 · 叮咚买菜自动下单
  8. CENTOS7 防火墙放行ftp
  9. 【Excel】日期时间转任意时区
  10. 三分钟简单了解闭包及作用域