12 设计模式——备忘录模式

返回设计模式博客目录

介绍


备忘录(Memento)模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到先前保存的状态

其实很多应用软件都使用了该模式,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

优点

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

缺点

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

使用场景

  • 需要保存一个对象在某一时刻的状态或部分状态。
  • 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过中间对象可以间接访问其内部状态。

结构与实现


模式包含以下主要角色。

  • Originator(发起人角色):负责创建一个备忘录(Memoto),能够记录内部状态,以及恢复原来记录的状态。并且能够决定哪些状态是需要备忘的。
  • Memoto(备忘录角色):将发起人(Originator)对象的内部状态存储起来;并且可以防止发起人(Originator)之外的对象访问备忘录(Memoto)。
  • Caretaker(负责人角色):负责保存备忘录(Memoto),不能对备忘录(Memoto)的内容进行操作和访问,只能将备忘录传递给其他对象。

其结构图如下图所示。

备忘录模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class MementoPattern {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState("S0");
System.out.println("初始状态:" + originator.getState());
caretaker.setMemento(originator.createMemento()); // 保存状态
originator.setState("S1");
System.out.println("新的状态:" + originator.getState());
originator.restoreMemento(caretaker.getMemento()); // 恢复状态
System.out.println("恢复状态:" + originator.getState());
}
}
// 备忘录
class Memento {
private String state;
public Memento(String state) {
this.state=state;
}
public void setState(String state) {
this.state=state;
}
public String getState() {
return state;
}
}
// 发起人
class Originator {
private String state;
public void setState(String state) {
this.state=state;
}
public String getState() {
return state;
}
public Memento createMemento() {
return new Memento(state);
}
public void restoreMemento(Memento m) {
this.setState(m.getState());
}
}
// 管理者
class Caretaker {
private Memento memento;
public void setMemento(Memento m) {
memento=m;
}
public Memento getMemento() {
return memento;
}
}

示例


以游戏存档为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* Originator
* 这里则是游戏类,游戏类提供存档和读档的功能
*/
public class Game {
private int mLevel = 1; // 等级
private int mCoin = 0; // 金币数量
@Override
public String toString() {
return "game{" +
"mLevel=" + mLevel +
", mCoin=" + mCoin +
'}';
}
public void play() {
System.out.println("升级了");
mLevel++;
System.out.println("当前等级为:" + mLevel);
System.out.println("获得金币:32");
mCoin += 32;
System.out.println("当前金币数量为:" + mCoin);
}
public void exit() {
System.out.println("退出游戏");
System.out.println("退出游戏时的属性 : " + toString());
}
// 创建备忘录,即游戏存档
public Memento createMemento() {
Memento memento = new Memento();
memento.level = mLevel;
memento.coin = mCoin;
return memento;
}
public void setMemento(Memento memento) {
mLevel = memento.level;
mCoin = memento.coin;
System.out.println("读取存档信息:" + toString());
}
}
/**
* Memento
* 负责将游戏类的内部状态存储起来
*/
public class Memento {
public int level; // 等级
public int coin; // 金币数量
}
/**
* Caretaker
* 备忘录管理类
*/
public class Caretaker {
private Memento mMemento;
public void setMemento(Memento memento) {
mMemento = memento;
}
public Memento getMemento() {
return mMemento;
}
}

客户端测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void test() {
System.out.println("首次进入游戏");
Game game = new Game();
game.play();
Memento memento = game.createMemento(); // 创建存档
Caretaker caretaker = new Caretaker();
caretaker.setMemento(memento); // 保存存档
game.exit();
System.out.println("-------------");
System.out.println("二次进入游戏");
Game secondGame = new Game();
secondGame.setMemento(caretaker.getMemento()); // 读取存档
secondGame.play();
secondGame.exit();
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首次进入游戏
升级了
当前等级为:2
获得金币:32
当前金币数量为:32
退出游戏
退出游戏时的属性 : game{mLevel=2, mCoin=32}
-------------
二次进入游戏
读取存档信息:game{mLevel=2, mCoin=32}
升级了
当前等级为:3
获得金币:32
当前金币数量为:64
退出游戏
退出游戏时的属性 : game{mLevel=3, mCoin=64}

ANDROID 源码中的实现


状态保存是 ANDROID 中备忘录模式的典型使用,主要对应 Activity的两个回调方法 onSaveInstanceState() 和 onRestoreInstanceState()。

onSaveInstanceState 方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void onSaveInstanceState(Bundle outState) {
// 存储当前窗口的视图树的状态
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
// 存储 fragment 的状态
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
// 存储自动填充的字段
if (mAutoFillResetNeeded) {
outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
getAutofillManager().onSaveInstanceState(outState);
}
// 如果用户还设置了 Activity 的 ActivityLifecycleCallbacks,
// 那么调用这些 ActivityLifecycleCallbacks 的 onSaveInstanceState 进行存储状态
getApplication().dispatchActivitySaveInstanceState(this, outState);
}

上述 onSaveInstanceState 函数中,主要分为如下 3 步:
1)存储窗口的视图树的状态;
2)存储 Fragment 的状态
3)调用 ActivityLifecycleCallbacks 的 onSaveInstanceState 函数进行状态存储。

我们先看第一步,在这一步将 Window 对象中的视图树中欧冠各个 View 状态存储到 Bundle 中。这样一来,当用户重新进入到该 Activity 时,用户 UI 的结构、状态才会被重新恢复,以此来保证用户界面的一致性。Window 类的具体实现类是 PhoneWindow,其中 saveHierarchyState 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();
if (mContentParent == null) {
return outState;
}
// 通过 SparseArray 类来存储,这相当于一个 key 为整型的 map
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
// 调用 mContentParent 的 saveHierarchyState 方法,这个 mContentParent 就是调用 Activity 的
// setContentView 函数设置的内容视图,它是内容视图的根节点,在这里存储整棵视图树的结构。
mContentParent.saveHierarchyState(states);
// 将视图树结构放到 outState 中
outState.putSparseParcelableArray(VIEWS_TAG, states);
// 保存当前界面中获取了焦点的 View
// Save the focused view ID.
final View focusedView = mContentParent.findFocus();
if (focusedView != null && focusedView.getId() != View.NO_ID) {
// 持有焦点的 View 必须要设置 id,否则重新进入该界面时不会恢复它的焦点状态
outState.putInt(FOCUSED_ID_TAG, focusedView.getId());
}
// 存储整个面板的状态
// save the panels
SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>();
savePanelState(panelStates);
if (panelStates.size() > 0) {
outState.putSparseParcelableArray(PANELS_TAG, panelStates);
}
// 存储 ActionBar 的状态
if (mDecorContentParent != null) {
SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>();
mDecorContentParent.saveToolbarHierarchyState(actionBarStates);
outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
}
return outState;
}

在 saveHierarchyState 中,主要时存储了与当前 UI、ActionBar 相关的 View 状态,这里用 mContentParent 来分析。这个 mContentParent 就是我们通过 Activity 的 setContentView 函数设置的内容视图,它是整个内容视图的根节点,存储它层级结构中的 View 状态也就存储了用户界面的状态。mContentParent 是一个 ViewGroup 对象,但是,saveHierarchyState 并不是在 ViewGroup 中,而是在 ViewGroup 的父类 View。

View 的 saveHierarchyState 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// 注意:如果 View 没有设置 id,那么这个 View 的状态将不会被存储。
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
// 调用 onSaveInstanceState 获取自身的状态
Parcelable state = onSaveInstanceState();
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// 将自身状态放到 container 中,key 为 id、value 为自身状态。
container.put(mID, state);
}
}
}
// View 类默认存储的状态为空
protected Parcelable onSaveInstanceState() {
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
...
return BaseSavedState.EMPTY_STATE;
}

在 View 类中的 saveHierarchyState 函数调用了 dispatchSaveInstanceState 函数来存储自身的状态,而 ViewGroup 则覆写了 dispatchSaveInstanceState 函数来存储自身以及子视图的状态,函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}

dispatchSaveInstanceState 会首先调用 super 的方法存储自身的状态,然后调用每个子视图的 dispatchSaveInstanceState。

注意:如果 View 没有设置 id,那么这个 View 的状态将不会被存储。设置了这个 id 也要保证在一个 Activity 的布局中必须是唯一的,否则会出现状态覆盖的情况。

这些被存储的状态通过 onSaveInstanceState 函数得到,但在 View 类中我们看到返回的是一个空状态。这就意味着,当我们需要存储 View 状态是,需要覆写 onSaveInstanceState 方法,将要存储的数据放到 Parcelable 对象中,并且将它返回。我们看看 TextView 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// 代码省略
// 存储 TextView 的 start、end 以及文本内容
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp;
} else {
ss.text = mText.toString();
}
}
if (hasSelection) {
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
}
// 代码省略
return superState;
}
}

存储完 Window 的视图树状态后,会存储每个 Fragment 的状态,调用它们的 onSaveInstanceState 方法。最后调用 ActivityLifecycleCallbacks 的 onSaveInstanceState。

P 版本(Android 9)之前,onSaveInstanceState 会在 onStop 之前调用。P 版本(Android 9)之后,onSaveInstanceState 会在 onStop 之后调用。ActivityThread 的 performStopActivity 会调用 callActivityOnStop。callActivityOnStop 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
r.activity.performStop(false /*preserveWindow*/, reason);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to stop activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
r.setState(ON_STOP);
if (shouldSaveState && !isPreP) {
callActivityOnSaveInstanceState(r);
}
}

callActivityOnSaveInstanceState 方法会将状态信息存储到 ActivityClientRecord 对象的 state 字段中。

1
2
3
4
5
6
7
8
9
10
11
private void callActivityOnSaveInstanceState(ActivityClientRecord r) {
r.state = new Bundle();
r.state.setAllowFds(false);
if (r.isPersistable()) {
r.persistentState = new PersistableBundle();
mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state,
r.persistentState);
} else {
mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
}
}

在 ActivityThread 类的 performLaunchActivity 方法会回调 onCreate,将 ActivityClientRecord 对象的 state 字段传递给 onCreate。

1
2
3
4
5
6
7
8
9
10
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
return activity;
}

在 ActivityThread 类的 handleStartActivity 方法中会调用 callActivityOnRestoreInstanceState 恢复 InstanceState。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
...
// Restore instance state
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}

总结:

  • 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 这种数据结构完成自己状态的管理。

实战


简化版记事本:保存、撤销、重做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class NoteEditText extends AppCompatEditText {
public NoteEditText(Context context) {
super(context);
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoteEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 创建备忘录对象,即存储编辑器的指定数据
public Memo createMemo() {
Memo memo = new Memo();
memo.text = getText().toString();
memo.cursor = getSelectionStart();
return memo;
}
// 从备忘录中恢复数据,设置光标位置
public void restore(Memo memo) {
setText(memo.text);
setSelection(memo.cursor);
}
}
public class Memo {
public String text;
public int cursor;
}
// 负责管理 Memo 对象
public class NoteCaretaker {
// 最大存储数量
private static final int MAX = 30;
// 存储 30 条记录
private List<Memo> mMemos = new ArrayList<>(MAX);
private int mIndex = 0;
/**
* 保存备忘录到记录列表中
* @param memo Memo
*/
public void saveMemo(Memo memo) {
if (mMemos.size() > MAX) {
mMemos.remove(0);
}
mMemos.add(memo);
mIndex = mMemos.size() - 1;
}
// 获取上一个存档信息,相当于撤销功能
public Memo getPrevMemo() {
mIndex = mIndex > 0 ? --mIndex : mIndex;
return mMemos.get(mIndex);
}
// 获取下一个存档信息,相当于重做功能
public Memo getNextMemo() {
mIndex = mIndex < mMemos.size() - 1 ? ++mIndex : mIndex;
return mMemos.get(mIndex);
}
}

在 TestActivity 的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TestActivity extends BaseActivity {
private NoteEditText mNoteEditText;
private TextView mSaveBtn;
private ImageView mUndoBtn;
private ImageView mRedoBtn;
private NoteCaretaker mCaretaker = new NoteCaretaker();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_test);
initViews();
}
private void initViews() {
mNoteEditText = findViewById(R.id.et_note);
mSaveBtn = findViewById(R.id.btn_save);
mUndoBtn = findViewById(R.id.btn_undo);
mRedoBtn = findViewById(R.id.btn_redo);
mSaveBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCaretaker.saveMemo(mNoteEditText.createMemo());
}
});
mUndoBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditText.restore(mCaretaker.getPrevMemo());
}
});
mRedoBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditText.restore(mCaretaker.getNextMemo());
}
});
}
}

附 res/layout/test.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.xxt.xtest.demo.NoteEditText
android:id="@+id/et_note"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="left"
android:hint="写点嘛~"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="50dp"
android:paddingRight="50dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/btn_undo"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@drawable/undo"/>
<TextView
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="保存"
android:textSize="20sp"
android:textColor="#000"/>
<ImageView
android:id="@+id/btn_redo"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:src="@drawable/redo"/>
</RelativeLayout>
</LinearLayout>