介绍
备忘录(Memento)模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到先前保存的状态。
其实很多应用软件都使用了该模式,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。
优点
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点
- 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
使用场景
- 需要保存一个对象在某一时刻的状态或部分状态。
- 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过中间对象可以间接访问其内部状态。
结构与实现
模式包含以下主要角色。
- Originator(发起人角色):负责创建一个备忘录(Memoto),能够记录内部状态,以及恢复原来记录的状态。并且能够决定哪些状态是需要备忘的。
- Memoto(备忘录角色):将发起人(Originator)对象的内部状态存储起来;并且可以防止发起人(Originator)之外的对象访问备忘录(Memoto)。
- Caretaker(负责人角色):负责保存备忘录(Memoto),不能对备忘录(Memoto)的内容进行操作和访问,只能将备忘录传递给其他对象。
其结构图如下图所示。
备忘录模式的实现代码如下:
示例
以游戏存档为例子:
客户端测试:
输出结果:
ANDROID 源码中的实现
状态保存是 ANDROID 中备忘录模式的典型使用,主要对应 Activity的两个回调方法 onSaveInstanceState() 和 onRestoreInstanceState()。
onSaveInstanceState 方法的代码如下:
上述 onSaveInstanceState 函数中,主要分为如下 3 步:
1)存储窗口的视图树的状态;
2)存储 Fragment 的状态
3)调用 ActivityLifecycleCallbacks 的 onSaveInstanceState 函数进行状态存储。
我们先看第一步,在这一步将 Window 对象中的视图树中欧冠各个 View 状态存储到 Bundle 中。这样一来,当用户重新进入到该 Activity 时,用户 UI 的结构、状态才会被重新恢复,以此来保证用户界面的一致性。Window 类的具体实现类是 PhoneWindow,其中 saveHierarchyState 方法如下:
在 saveHierarchyState 中,主要时存储了与当前 UI、ActionBar 相关的 View 状态,这里用 mContentParent 来分析。这个 mContentParent 就是我们通过 Activity 的 setContentView 函数设置的内容视图,它是整个内容视图的根节点,存储它层级结构中的 View 状态也就存储了用户界面的状态。mContentParent 是一个 ViewGroup 对象,但是,saveHierarchyState 并不是在 ViewGroup 中,而是在 ViewGroup 的父类 View。
View 的 saveHierarchyState 方法如下:
在 View 类中的 saveHierarchyState 函数调用了 dispatchSaveInstanceState 函数来存储自身的状态,而 ViewGroup 则覆写了 dispatchSaveInstanceState 函数来存储自身以及子视图的状态,函数如下:
dispatchSaveInstanceState 会首先调用 super 的方法存储自身的状态,然后调用每个子视图的 dispatchSaveInstanceState。
注意:如果 View 没有设置 id,那么这个 View 的状态将不会被存储。设置了这个 id 也要保证在一个 Activity 的布局中必须是唯一的,否则会出现状态覆盖的情况。
这些被存储的状态通过 onSaveInstanceState 函数得到,但在 View 类中我们看到返回的是一个空状态。这就意味着,当我们需要存储 View 状态是,需要覆写 onSaveInstanceState 方法,将要存储的数据放到 Parcelable 对象中,并且将它返回。我们看看 TextView 的实现。
存储完 Window 的视图树状态后,会存储每个 Fragment 的状态,调用它们的 onSaveInstanceState 方法。最后调用 ActivityLifecycleCallbacks 的 onSaveInstanceState。
P 版本(Android 9)之前,onSaveInstanceState 会在 onStop 之前调用。P 版本(Android 9)之后,onSaveInstanceState 会在 onStop 之后调用。ActivityThread 的 performStopActivity 会调用 callActivityOnStop。callActivityOnStop 代码如下:
callActivityOnSaveInstanceState 方法会将状态信息存储到 ActivityClientRecord 对象的 state 字段中。
在 ActivityThread 类的 performLaunchActivity 方法会回调 onCreate,将 ActivityClientRecord 对象的 state 字段传递给 onCreate。
在 ActivityThread 类的 handleStartActivity 方法中会调用 callActivityOnRestoreInstanceState 恢复 InstanceState。
总结:
- Bundle 对应备忘录:Android 的状态,包括视图树状态和 Fragment 状态以及生命周期状态都是通过 Bundle 这个数据结构存储键值对的 Parcel 对象保存的,特别注意一点,对于同一个 Activity的视图放到一个 Bundle 中用 SparceArray(类似 HashMap 不过空间使用效率更高,内部查找二分法,而且键只能是整数)来存储。键:ViewId。值:对应的 Parcel 对象,所以 ViewId 不能重复,不然会覆盖。
- Activity 对应备忘录管理类。严格来说应该是 Activity 中的内部属性。mActivities 实际是一个 ActivityClientRecord 集合,每个 Activity 的信息对应一个 ActivityClientRecord,相应的键是 Token。ActivityClientRecord 的 Bundle 类型的 State 对应 Bundle(备忘录)。
- View 和 Fragment 等都对应 Originator 类,他们都需要伴随 Activity 的生命周期函数 onSaveInstanceState() 和 OnRestoreInstanceState() 通过 Bundle 这种数据结构完成自己状态的管理。
实战
简化版记事本:保存、撤销、重做。
在 TestActivity 的代码如下所示:
附 res/layout/test.xml