View 层级状态自动保存机制
原理与契约
Android 提供了一套**自动的 View 层级状态保存/恢复机制**,核心契约如下:
- 系统会在 Activity 的
onSaveInstanceState()调用后,自动遍历整个 View 树,收集需要保存状态的 View 子节点。 - 只有同时满足三个条件的 View 才会被保存:
- View 设置了 唯一的 android:id(XML 中或代码中
setId()); - View 的
setSaveEnabled(true)(默认即为true); - View 本身或父容器没有禁用状态保存(
setSaveFromParentEnabled未被设置为false)。
- View 设置了 唯一的 android:id(XML 中或代码中
- 系统使用 SparseArray<Parcelable> 以 View ID 为 key 集中存放所有 View 的状态数据,最终打包到 Activity 的
Bundle中。 - 重建恢复时,系统再次按同一遍历顺序将保存的状态分发回对应 ID 的 View,调用其
onRestoreInstanceState(Parcelable)方法。
关键结论:ID 唯一性是整个机制的基石。两个 View 拥有相同 ID 时,后一个 View 的状态会覆盖前一个,导致数据混乱或丢失。
实现流程
从触发保存到最终恢复的完整链路分为 4 个阶段:
- 触发阶段:Activity 的
onSaveInstanceState()被系统调用(通常在onStop()之前)。 - 遍历收集阶段:系统通过
View.dispatchSaveInstanceState()递归遍历所有子 View。对每个节点判断:是否拥有 ID &&isSaveEnabled()为 true。 - 状态序列化阶段:满足条件的 View 调用
onSaveInstanceState()返回一个Parcelable对象,连同 ID 存入SparseArray。 - 恢复分发阶段:Activity 重建后,系统调用
View.dispatchRestoreInstanceState(SparseArray)按相同遍历顺序将状态对象交还给对应 ID 的 View,由onRestoreInstanceState()完成内部状态恢复。
⚠ 常见陷阱:自定义 View 重写
onSaveInstanceState() 时,必须调用 super.onSaveInstanceState() 并返回组合后的对象;否则系统内置状态(如 EditText 的文本选择)会丢失。
核心 API 与代码示例
以下表格列出相关 API 的作用与使用场景:
| API | 作用 | 默认行为 |
|---|---|---|
setSaveEnabled(boolean) |
启用/禁用当前 View 的状态保存 | true |
onSaveInstanceState() |
返回当前 View 需要保存的状态数据 | 返回 BASE_CURSOR / 文本内容等基础信息 |
onRestoreInstanceState(Parcelable) |
从保存的数据恢复 View 状态 | 恢复文本、选中、滚动等 |
setId(int) |
为 View 设置唯一标识 | 无 ID(View.NO_ID) |
getId() |
获取当前 View 的 ID | 可能为 NO_ID |
自定义 View 保存自定义状态的标准写法:
public class MyCustomView extends View {
private int customValue = 0;
// 必须实现两个参数的构造方法
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Parcelable onSaveInstanceState() {
// Step 1: 保存父类的基础状态
Parcelable superState = super.onSaveInstanceState();
// Step 2: 创建 SavedState 持有自定义数据
SavedState savedState = new SavedState(superState);
savedState.customValue = this.customValue;
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
// Step 1: 强转为 SavedState
SavedState savedState = (SavedState) state;
// Step 2: 恢复父类状态
super.onRestoreInstanceState(savedState.getSuperState());
// Step 3: 恢复自定义数据
this.customValue = savedState.customValue;
invalidate(); // 触发重绘
}
// 自定义 SavedState 内部类
protected static class SavedState extends BaseSavedState {
int customValue;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.customValue = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(customValue);
}
public static final Creator<SavedState> CREATOR =
new Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
调试与验证方法
在开发和测试阶段,可采用以下手段检查 View 状态是否被正确保存/恢复:
- 开发者选项 — 不保留活动(Don't keep activities):强制 Activity 重建,可直观验证 View 文本、滚动位置等是否恢复。
- 日志插桩:在
onSaveInstanceState()和onRestoreInstanceState()中打印 Log,观察调用链。 - 断点调试:在
View.dispatchSaveInstanceState()和dispatchRestoreInstanceState()处设置断点,查看 SparseArray 的内容。
// 示例:在自定义 View 中添加日志
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
Log.d("ViewState", "Saving ID=" + getId() + " superState=" + superState);
// ... 自定义保存逻辑
return combined;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Log.d("ViewState", "Restoring ID=" + getId() + " state=" + state);
// ... 恢复逻辑
super.onRestoreInstanceState(state);
}
常见错误与对策
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| Activity 重建后 EditText 内容丢失 | EditText 没有设置 android:id | 为 EditText 添加唯一的 id |
| 两个 View 状态互相覆盖 | 两个 View 使用了相同的 id(如都用了 @+id/title) | 确保整个布局层级的 id 全局唯一 |
| 自定义 View 恢复后数据为空 | onSaveInstanceState 返回了 null 或没有调用 super | 始终返回组合后的 Parcelable,并调用 super |
| Fragment 中 View 状态未恢复 | Fragment 在重建时未重新 inflate 布局,或未调用 super | 确保 Fragment 的 onCreateView 正确 inflate,且布局的 View 都有 id |
性能与稳定性优化
- 精确控制保存范围:对于只包含静态显示内容的 View(如 ImageView 无需保存图片引用),可调用
setSaveEnabled(false)避免无意义的序列化。 - 避免保存大型数据:不要将 Bitmap、大量 List 直接放入
onSaveInstanceState返回的 Parcelable 中;建议使用 ViewModel 或持久化存储。 - ID 生成策略:在 XML 中推荐使用
@+id/xxx自动分配;代码动态创建的 View 应使用View.generateViewId()(API 17+)确保不重复。 - 容器层级优化:若父容器调用了
setSaveEnabled(false),其所有子 View 的状态也会被跳过;需要子 View 独立保存时,应避免在父容器整体禁用保存。
💡 最佳实践:所有可交互的 View(EditText、CheckBox、SeekBar、自定义控件)务必设置唯一 ID。若在代码中动态创建 View 并需要状态保存,请使用
setId(View.generateViewId())。
内联 SVG:View 层级状态保存流程
图中展示了从 Activity 状态保存触发到遍历、判断、收集、再到重建后恢复分发的完整链路。菱形判断节点强调了 ID 与 saveEnabled 两个前提条件。