正确的保存View状态.

纯手打原创,转载请注明出处,感谢!

前言

android 的 View以及ViewGroup保存相应状态是一件很重要的事情,但是最近由于用到了相关的内容, 搜了下国内相关文章,涉及到的很少.
可能很多人都在开发的时候忽略掉了这个功能.自己看了下相应的源码以及翻阅了下相关的文章,了解了下相应的知识,于是写下这篇记录,算是给以后自己翻阅做个笔记.

首先我们要明白View 为什么要保存自己的状态? 我咨询了一些做android开发的朋友,给出的答案都是五花八门, 但大部分都是说不关心保存状态这件事, 当我说这样不对的时候,
往往得到的回复是我的代码一直好好的啊 没什么问题啊.
是的,代码如果不去关心View 保存状态这件事 在大部分时间都是没问题的,但是在一些情况下会出乱子.
我举个例子, 我们的Activity A 去Activity B, 这时候A会走 onPause onStop onSaveInstanceState() 的生命周期. 我们要先明白Activity给我们这个onSaveInstanceState()回调时机的意义,
来看一下源码的说明

意思就是说 这个Activity在被杀死之前 给你一个机会让你保留你希望保存的东西, 而你保存的东西会在什么时候还给你呢, 会在onCreate 或者onRestoreState的时候.
之前有人跟我, 我完全可以不用care这个回调啊,我可以在Activity内自己创建一个Member变量容器 去保存我希望保存的内容啊, 这里是我们需要明白的一个关键,
那就是 Activity A 去Activity B, Activity A 如果不被系统杀死回收的话,那怎么搞都是没问题的,但是一旦因为内存不足,Activity A被回收掉,你想想,你的Member容器也会随之一起回收掉.
等你从Activity B back 回Activity A的时候, 就会丢失掉你所有希望保存的内容.
这里有一个小细节就是 会有人不知道怎么去测试 Activity A 去Activity B, 然后Activity A 被回收的情况, 我们的android手机内 开发者模式, 有一个选项叫 不保留活动.打开以后,
只要你退出当前的Activity(按home或者去别的Activity), 你当前的Activity 就必定会被系统杀死回收. 看到这里如果不了解这个选项的朋友可以去打开这个选项试试自己的app.或许你会发现打开了新大门..
还有一个小细节就是有可能打开这个开关以后你检查了下你的app 然后告诉我说并没有什么问题, 不会有崩溃啊什么的, 要注意,这里的问题是指 本来应该保存的一些内容 或者用户已经加载过 或者用户已经在当前页面做过的一些动作
没有被保存,这句话需要仔细去理解一下,然后再check下自己的app,看看是不是会有东西丢失.

View 保存State的一些知识点

View saveState 与 restoreState 两套动作都是相对应的, 分别有这么几个关键的方法.

saveState.
  • void saveHierarchyState(SparseArray container)
    这个方法是android framework层调用,在需要保存状态的时机. 看下View源码发现通常是不做什么事情的.是直接调用dispatchSaveInstanceState的
  • void dispatchSaveInstanceState(SparseArray container)
    这个方法是被saveHierarchyState调用,它内部会调用onSaveInstanceState 方法去拿到一个存储View当前状态的Parcelable对象, 然后存在container中.这里有两个个关键点
    1 它的第一句就是 if(mId != NO_ID) 才会执行接下来的保存状态的工作,也就是说如果我们的View希望保存状态那么一定要有一个ID,其实这很好理解, 保存着我们View状态的parcelable 是以key value的形式
    保存在container中的,那么key是我们的id 也是合情合理的.
    代码如下 非常清晰
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
    mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
    Parcelable state = onSaveInstanceState();
    if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
    throw new IllegalStateException(
    "Derived class did not call super.onSaveInstanceState()");
    }
    if (state != null) {
    // Log.i("View", "Freezing #" + Integer.toHexString(mID)
    // + ": " + state);
    container.put(mID, state);
    }
    }
    }

2 如果当前View是一个ViewGroup那么它会去循环调用每个child的dispatchSaveInstanceState(), 去给child机会保存状态, android这样设计的地方很多, 比如Touch分发 比如 Measure分发等等,这里就不过多表述了.
代码如下

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);
}
}
}

  • Parcelable onSaveInstanceState()
    这个方法就很简单了, 是真正干活的地方,保存当前View的状态, 我们写自定义View的时候 也往往是重写该方法去保存我们需要保存的状态.
restoreState.
  • void restoreHierarchyState(SparseArray container)
    对应saveHierarchyState()
  • void dispatchRestoreInstanceState(SparseArray container)
    对应dispatchSaveInstanceState()
  • void onRestoreInstanceState(Parcelable state)
    对应onSaveInstanceState()

还有一个方法是需要特殊说明的, 如果你的自定义View需要保存状态, 那么你需要调用setSaveEnabled(true)方法. 当然widgets官方提供的view都是打开了的.你不用操心.
看到这里大家应该以及知道了View保存恢复状态的整个流程是什么样的了,一个View 如果想要保存状态,那么只需要做两件事

  1. 有id
  2. setSaveEnabled(true)

如何保存自己的状态

我们已经知道了我们自定义View应该在onSaveInstanceState的时候去保存我们希望保存的状态,那么具体应该怎么去做呢.接下来是示范(炒鸡简单)
比如我们的View内有1个int值希望保存下来, 那么我们的代码应该这样写,

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
private int mState;
...
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.state = customState;
return ss;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setCustomState(ss.state);
}

static class SavedState extends BaseSavedState {
int state;

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

private SavedState(Parcel in) {
super(in);
state = in.readInt();
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(state);
}

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];
}
};
}

一个经验教训.

当我们做一个View的时候,那么上面的内容足够让你保存你想要保存的状态. 但是如果是一个ViewGroup的话, 代码或许在某些情况下应该这样去设计,

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
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.childrenStates = new SparseArray();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).saveHierarchyState(ss.childrenStates);
}
return ss;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).restoreHierarchyState(ss.childrenStates);
}
}

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}

你需要去注意一下在 dispatchSaveInstanceState 和 dispatchRestoreInstanceState,我们并不是使用super方法 去遍历我们的children去挨个保存和恢复状态,而是
调用dispatchFreezeSelfOnly 和 dispatchThawSelfOnly 来告诉系统,我这个ViewGroup不需要去挨个找我的child去保存状态,我们(ViewGroup)自己来就可以了.
当然并不是说,如果我们写ViewGroup的时候就一定要这样去写,具体情况要根据自己的实际情况去决定.

另外一个血和泪的经验教训.

在上面的例子中, 我们的SavedState 存储了一个int值, 这属于比较简单的一种实现,一般是不会有什么问题的,如果你需要存储的内容是一个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
static class SavedState extends BaseSavedState {
Bundle state;

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

private SavedState(Parcel in) {
super(in);
state = in.readBundle(Bundle.class.getClassLoader());
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeBundle(state);
}

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];
}
};
}

请注意,这些代码是系统自己生成的, 在大多数情况这样写依然是没有问题的, 但是 一旦你的Bundle里存储了一个你自定义的Parcelable类的时候,事情就会变的蛋疼起来,在你这个Bundle进行unparcel()的时候,会崩溃, 爆出BadParcelableException, 原因是 找不到你的自定义的那个类. 这个bug卡了我好长时间,
最后定位到了这个异常,爆出位置 : 首先你可以在Parcel类中的readValue(ClassLoader loader) 中找到readParcelable() 然后跟进去最后发现 会执行这么一段代码.

1
2
3
// If loader == null, explicitly emulate Class.forName(String) "caller
// classloader" behavior.
ClassLoader parcelableClassLoader =(loader == null ? getClass().getClassLoader() : loader);

第一次看到这个代码的时候,并没有注意什么. 后来查了很多stackoverflow,开始怀疑跟这个ClassLoader有关系.然后开始相应查相关的内容,最终找到了问题所在, 直接上结论把.

Android has two different classloaders: the framework classloader (which knows how to load Android classes) and the APK classloader (which knows how to load your code).
The APK classloader has the framework classloader set as its parent, meaning it can also load Android classes.

说的很清晰了, 就是ClassLoader找错了, 解决方案也很简单,

1
2
3
4
private SavedState(Parcel in) {
super(in);
state = in.readBundle(getClass().getClassLoader());
}

这样写就会给Bundle 设置一个APK的ClassLoader, 这样就可以找到你自己定义的那个Parcelable类了.

Share Comments