4. View层级状态自动保存机制

原理与契约

Android 提供了一套**自动的 View 层级状态保存/恢复机制**,核心契约如下:

  • 系统会在 Activity 的 onSaveInstanceState() 调用后,自动遍历整个 View 树,收集需要保存状态的 View 子节点。
  • 只有同时满足三个条件的 View 才会被保存:
    1. View 设置了 唯一的 android:id(XML 中或代码中 setId());
    2. View 的 setSaveEnabled(true)(默认即为 true);
    3. View 本身或父容器没有禁用状态保存(setSaveFromParentEnabled 未被设置为 false)。
  • 系统使用 SparseArray<Parcelable> 以 View ID 为 key 集中存放所有 View 的状态数据,最终打包到 Activity 的 Bundle 中。
  • 重建恢复时,系统再次按同一遍历顺序将保存的状态分发回对应 ID 的 View,调用其 onRestoreInstanceState(Parcelable) 方法。
关键结论:ID 唯一性是整个机制的基石。两个 View 拥有相同 ID 时,后一个 View 的状态会覆盖前一个,导致数据混乱或丢失。

实现流程

从触发保存到最终恢复的完整链路分为 4 个阶段:

  1. 触发阶段:Activity 的 onSaveInstanceState() 被系统调用(通常在 onStop() 之前)。
  2. 遍历收集阶段:系统通过 View.dispatchSaveInstanceState() 递归遍历所有子 View。对每个节点判断:是否拥有 ID && isSaveEnabled() 为 true。
  3. 状态序列化阶段:满足条件的 View 调用 onSaveInstanceState() 返回一个 Parcelable 对象,连同 ID 存入 SparseArray。
  4. 恢复分发阶段: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 层级状态保存流程

View 层级状态自动保存与恢复流程 Activity.onSaveInstanceState() 遍历 View 树(深度优先) ID 唯一 && saveEnabled == true? 跳过该 View 调用 onSaveInstanceState() 存入 SparseArray<> Activity 重建后恢复 遍历 View 树 分发 onRestoreInstanceState() 状态恢复完成 ID 必须全局唯一,否则状态会串

图中展示了从 Activity 状态保存触发到遍历、判断、收集、再到重建后恢复分发的完整链路。菱形判断节点强调了 ID 与 saveEnabled 两个前提条件。