Android View state saving and it's problem.

Android开发中,有一个不大不小的部分,用到的机会确实不多,甚至没有它也不会有什么明显的影响,它就是——Android状态保存。它的存在是这样的:在EditText中输入内容,旋转屏幕,EditText中的文字还在。有了状态保存,App的使用体验就会更加顺畅自然。

默认配置下,旋转屏幕后,Activity会重建,View重新创建之后,之前的内容自然消失,而如果文在还在的话,那就是状态保存机制在起作用了。Activity重新创建前后会分别调用onSaveInstanceState/onRestoreInstanceState,将Activity中的数据和寄于其中的Fragment和View的状态都保存下来,并在重建完成后还原。今天要讨论的是View级别的状态保存

状态保存原理以及实现

说明这个部分已经有非常好的文章,所以直接贴上链接:

http://trickyandroid.com/saving-android-view-state-correctly/

这篇文章完整解释了Android状态保存的原理和原生、自定义View的保存方法,非常推荐阅读。
http://cyrilmottier.com/2014/09/25/deep-dive-into-android-state-restoration/
这篇是Cyril在Droidcon France上的演讲,可以下载keynote和观看视频(youtube),也是讲解的非常详细,可以和上面的文章一起看。
下面贴一个通用的模板,一般情况下自定义View和ViewGroup都可以直接使用,方便快捷。(来自第一篇文章)

View:

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
public class CustomSwitch extends Switch {

private int customState;

.............

public void setCustomState(int customState) {
this.customState = customState;
}

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

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
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
public class MyCustomLayout extends LinearLayout {

// other code

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

static class SavedState extends BaseSavedState {
SparseArray childrenStates;

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

private SavedState(Parcel in, ClassLoader classLoader) {
super(in);
childrenStates = in.readSparseArray(classLoader);
}

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

public static final ClassLoaderCreator<SavedState> CREATOR
= new ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel source, ClassLoader loader) {
return new SavedState(source, loader);
}

@Override
public SavedState createFromParcel(Parcel source) {
return createFromParcel(source, null);
}

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

简单解释一下ViewGroup为什么要这么写。自定义ViewGroup中会包含很多View,而Android会将View的id作为State的key进行存储,所以一旦同一个布局中出现两次同一个ViewGroup,其中的View就会出现两次,保存第二个ViewGroup中子View的状态的时候,会将第一个的状态覆盖。上面的写法,是将ViewGroup中View的状态连同自己的状态都保存到以ViewGroup的id作为key的空间(SparseArray)内,这样子View的key就不会发生冲突,也就不会有异常情况出现了。

状态保存的坑

上面提到这两个代码代码片段适合一般情况,那什么是所谓的一般情况,什么又是特别情况呢?

一般情况:CustomView直接继承自Android Framework中的View,如View、Checkbox、RelativeLayout

特殊情况:CustomCustomView继承自CustomView,CustomCustomView和CustomView的状态都要保存的时候,就不能直接使用上面的代码片段。
下面深入一下Android状态恢复的机制

Android在保存ViewA的状态的时候,会在ViewA.SavedState中保存ViewA的父类ViewB的状态ViewB.SavedState,ViewA.SavedState.superState。在恢复状态的时候,也是要先从Parcel中读取ViewA.SavedState.superState。

从Parcel中读取SavedState时,会调用SavedState(Parcel source)方法,而AbsSavedState(BaseSavedState的父类)的构造方法会首先读取superState,如下图:abssavedstate

一个很令人苦笑不得的事情,这行代码上面还有一行FIXME标注,看来Android的工程师也知道这里有坑啊。

言归正传,readParcelable()方法的参数是ClassLoader对象,如果像上图中传null的话,Android会使用默认的ClassLoader,也就是DexClassLoader

// 调用链
|–Parcelable superState = source.readParcelable(null);
|–Parcelable.Creator<?> creator = readParcelableCreator(loader);
|– ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader);
|– Class<?> parcelableClass = Class.forName(name, false, parcelableClassLoader);

上面代码中getClass().getClassLoader()会返回DexClassLoader,这个ClassLoader只能加载Android Framework中的类,如果需要加载的superState.Creator是我们自己App中的类的话,DexClassLoader就无能为力了,Class.forName(name, false, parcelableClassLoader); 会抛出ClassNotFoundException,这也是我最初遇到的异常。

对于一般情况,CustomView的SavedState中的superState的类型是AbsSavedState或者AbsSavedState,这两个类来自于Android Framework,所以上面的代码片段都是适用的。

而对于特殊情况,CustomCustomView继承自CustomView,所以CustomCustomView.SavedState中的superState的类型自然是CustomView.SavedState,毫无疑问,如果使用DexClassLoader进行加载CustomView.SavedState,就会抛出ClassNotFoundException。

问题找到了,如何解决呢?那就是加载superState时,使用PathClassLoader。废话不多说,直接贴代码:

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
public static class SavedState implements Parcelable {
public static final Creator&lt;SavedState&gt; CREATOR
= new Creator&lt;SavedState&gt;() {

public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}

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

private boolean checked;
private Parcelable mSuperState;

public SavedState(Parcel source) {
Parcelable superState = source.readParcelable(SuperView.class.getClassLoader());
this.mSuperState = superState != null ? superState : BaseSavedState.EMPTY_STATE;
checked = (boolean) source.readValue(this.getClass().getClassLoader());
}

SavedState(Parcelable superState) {
this.mSuperState = superState != null ? superState : BaseSavedState.EMPTY_STATE;
}

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

@Override
public void writeToParcel(Parcel out, int flags) {
out.writeParcelable(mSuperState, flags);
out.writeValue(checked);
}

public Parcelable getSuperState() {
return mSuperState;
}
}

没错,不再让SavedState继承自BasedSavedState,而是直接实现Parcelable,同时自己实现superState的保存和读取。

One more thing

因为使用ClassLoader加载Creator来恢复Sates,如果项目使用了Proguard进行混淆(几乎是一定的啦),就会因为找不到类进而报错。在proguard-rules.pro文件中加入下面的代码就可以解决。

1
2
3
-keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR;
}

Reference: StackOverflow
Bingo, problem solved! 希望能帮助各位。

Demo项目:Github