安卓自定义控件状态保存

因为业务原因, 我在安卓端实现了一个简单的富文本编辑器. 在初步实现后, 我将这个自定义的 RichEditor 放到了我们 App 的发帖界面中, 而发帖界面的复杂逻辑也引发了后续的问题.

界面回收与状态保持

我们的发帖界面可能唤起选图界面, 而图片处理往往需要消耗可观的内存. 在部分内存紧缺的机型上, 安卓系统为了保证选图界面的内存, 会将发帖界面回收并保存其状态, 在返回到发帖界面时又会将界面重建并恢复其控件状态.

安卓官方控件, 如 TextView, EditText 等控件在界面重建后, 其中的文本内容和光标位置都会被还原到重建前的状态, 而我的 RichEditor 却一片空白, 为啥?

关键方法

Activity/Fragment 的销毁和重建涉及到了两个重要的方法: onSaveInstanceState(Bundle)onRestoreInstanceState(Bundle), 在这两个方法中, 开发者可以自行存入当前界面的关键信息以便恢复界面状态(当然, Fragment + ViewPager 配套使用时会有一些问题, 相关资料大家可以上网查查).

View 的状态保存与重建与 Activity 类似, 但是实现过程稍有不同:

  • Activity 通过 Bundle 对象保存关键值, 其存取采用 key-value 一一对应方式
  • View 通过自定义的 SavedState 类型保存关键值, 其存取基于 Parcelable 实现

了解了两者的差别后, 我们来看看具体实践:

实践

一个简单的例子

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
70
71
72
73
74
75
76
public class StateSavedView extends View {

/* 省略若干无关代码 */

private int mClickCount; // 这个 View 被点击的次数
private boolean mHasClicked; // 是否被点击过

/**
* 存储当前状态
*/

@Override
protected Parcelable onSaveInstanceState() {
// 一定要将父类状态保存起来
Parcelable superState = super.onSaveInstanceState();
SavedState state = new SavedState(superState);
state.clickCount = mClickCount;
state.hasClicked = mHasClicked;
return state;
}

/**
* 恢复当前状态
*/

@Override
protected void onRestoreInstanceState(Parcelable state) {
// 检查一下传入的对象是不是自定义的状态类型, 不是的话就要交由父类处理
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(state);

mClickCount = ss.clickCount;
mHasClicked = ss.hasClicked;
}

/**
* 自定义状态存储, 需要遵守 Parcelable 规约
*/

public static class SavedState extends BaseSavedState {
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}

public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
int clickCount;
boolean hasClicked;

public SavedState(Parcelable superState) {
super(superState);
}

public SavedState(Parcel source) {
super(source);
clickCount = source.reandInt();
hasClicked = source.readByte() != 0;
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(clickCount);
out.writeByte(hasClicked ? (byte)1 : (byte) 0);
}

@Override
public int describeContents() {
return 0;
}
}
}

有以下几点需要注意:

  1. 状态的存取由 Parcelable 实现, 因此字段的存取顺序一定要保持一一对应
  2. 不要忘记调用父类方法, 否则会出错
  3. 这里保存的都是 UI 层的状态, 业务逻辑相关的状态千万不要放在这里, 否则很有可能引发一些隐蔽的 bug