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
55public class CustomSwitch extends Switch {
private int customState;
.............
public void setCustomState(int customState) {
this.customState = customState;
}
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.state = customState;
return ss;
}
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();
}
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 | public class MyCustomLayout extends LinearLayout { |
简单解释一下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,如下图:
一个很令人苦笑不得的事情,这行代码上面还有一行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 | public static class SavedState implements Parcelable { |
没错,不再让SavedState继承自BasedSavedState,而是直接实现Parcelable,同时自己实现superState的保存和读取。
One more thing
因为使用ClassLoader加载Creator来恢复Sates,如果项目使用了Proguard进行混淆(几乎是一定的啦),就会因为找不到类进而报错。在proguard-rules.pro文件中加入下面的代码就可以解决。
1 | -keepclassmembers class * implements android.os.Parcelable { |
Reference: StackOverflow
Bingo, problem solved! 希望能帮助各位。
Demo项目:Github