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

转载此译文须注明出处。

今天我们聊一聊安卓中保存和恢复view状态的问题。我刻意强调View状态是因为我发现这个过程要比保存 Activity 和 Fragment状态稍微复杂,还有一个原因是因为网上有太多“重复造的轮子”(有时还是奇丑无比的轮子)。

为什么我们需要保存View的状态?

这个问题问的好!我坚信移动应用应该帮助你解决问题,而不是制造问题。

想象一下一个非常复杂的设置页面:

这并不是从一个移动应用的截图(这不是典型的win32程序吗。。),但是适合用于说明我们的问题:

这里有非常多的文字输入控件,多选框,开关(switch)等等,你花了15分钟填完所有这些格子,总算轮到点击"完成"按钮了,但是突然,你不小心旋转了下屏幕,omg,所有的改动都没了,一切都回归到了初始状态。

当然,总有一些用户喜欢你的app简直到不行,不在乎重新填一次。但是老实说,这样做真的正确吗?(原文有老外常喜欢的喋喋不休的幽默句子,略了)。

别犯傻,我们需要保存用户的修改,除非用户特意让我们不要这样做。

如何保存View的状态?

假设我们这里有一个带有图像,文字和 Switch toggle控件的简单布局:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "horizontal" android:padding= "@dimen/activity_horizontal_margin" > <ImageView android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:src= "@drawable/ic_launcher" /> <TextView android:layout_width= "0dip" android:layout_weight= "1" android:layout_height= "wrap_content" android:text= "MyText" /> <Switch android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:layout_margin= "8dip" /> </LinearLayout>

看吧,非常简单的布局。但是当我们滑动一下switch开关然后旋转屏幕方向,switch又回到了原来的状态。

通常,安卓会自动保存这些View(一般是系统控件)的状态,但是为什么在我们的案例中不起作用了呢?

让我们先停下来,弄明白安卓是如何管理View状态的。这里是正常情况下保存与恢复的示意图:

  • saveHierarchyState(SparseArray<Parcelable> container)

    - 当状态需要保存的时候被安卓framework调用,通常会调用dispatchSaveInstanceState() 。

  • dispatchSaveInstanceState(SparseArray<Parcelable> container)

    - 被saveHierarchyState()调用。 在其内部调用onSaveInstanceState(),并且返回一个代表当前状态的Parcelable。这个Parcelable被保存在container参数中,container参数是一个键值对的map集合。View的ID是加键Parcelable是值。如果这是一个ViewGroup,还需要遍历其子view,保存子View的状态。

  • Parcelable onSaveInstanceState()

    - 被 dispatchSaveInstanceState()调用。这个方法应该在View的实现中被重写以返回实际的View状态。

  • restoreHierarchyState(SparseArray<Parcelable> container)

    - 在需要恢复View状态的时候被android调用,作为传入的SparseArray参数,包含了在保存过程中的所有view状态。

  • dispatchRestoreInstanceState(SparseArray<Parcelable> container)

    - 被restoreHierarchyState()调用。根据View的ID找出相应的Parcelable,同时传递给onRestoreInstanceState()。如果这是一个ViewGroup,还要恢复其子View的数据。

  • onRestoreInstanceState(Parcelable state)

    - 被dispatchRestoreInstanceState()调用。如果container中有某个view,ViewID所对应的状态被传递在这个方法中。

理解这个过程的重点是,container在整个view层级中是被共享的。我们将看到为什么它这么重要。

既然View的状态是基于它的ID存储的 , 因此如果一个VIew没有ID,那么将不会被保存到container中。没有保存的支点(id),我们也无法恢复没有ID的view的状态,因为不知道这个状态是属于哪个View的。

其实这是安卓的策略,假如我们来做也许会这样设计,大致这样:所有view按照一定的顺序依次存储,在恢复的时候只需知道这个View在保存的时候的顺序就可以了,不过显然这样要耗费更多的开销。- 译者注。

看样子这就是switch开关状态没有被保存的原因。那我们试试在switch开关上添加id(其他的View也加上id):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "horizontal" android:padding= "@dimen/activity_horizontal_margin" > <ImageView android:id= "@+id/image" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:src= "@drawable/ic_launcher" /> <TextView android:id= "@+id/text" android:layout_width= "0dip" android:layout_weight= "1" android:layout_height= "wrap_content" android:text= "MyText" /> <Switch android:id= "@+id/toggle" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:layout_margin= "8dip" /> </LinearLayout>

ok,看结果,确实可行。在configuration changes期间状态是可以保持的。下面是SparseArray的示意图:

就如你看到的那样,每个view都有一个id来把状态保存在container的SparseArray中。

你可能会问这是如何发生的 - 我们并没有提供任何Parcelable来代表状态啊。答案是 - 安卓处理好了这个事情,安卓知道如何保存系统自带控件的状态。在经过上面的一番解释之后,这句话来的太迟了吧 -译者注。

除了ID之外,你还需要明确的告诉安卓你的view需要保存状态,调用setSaveEnabled(true)就可以了。通常你不需要对自带的控件这样做,但是如果你从开始开发一个自定义的view,则需要手动设置(setSaveEnabled)。

要保存view的状态,至少有两点需要满足:

  1. view要有id

  2. 要调用setSaveEnabled(true)

现在我们知道如何保存自带控件的状态,但是如果我们有一些自定义的状态,想在configuration变化的时候保持这些状态又该如何呢?

保存自定义的状态

下面,让我们举一个更为复杂的例子。我想在继承自Switch的的类中添加一个自定义的状态:

1 2 3 4 5 6 7 8 9 10 publicclassCustomSwitchextendsSwitch{ privateintcustomState; //所谓状态其实就是数据 ....... publicvoidsetCustomState(intcustomState){ this .customState=customState; } }

下面是我们将如何保存这个状态的过程:

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 publicclassCustomSwitchextendsSwitch{ privateintcustomState; ............. publicvoidsetCustomState(intcustomState){ this .customState=customState; } @Override publicParcelableonSaveInstanceState(){ ParcelablesuperState= super .onSaveInstanceState(); SavedStatess= new SavedState(superState); ss.state=customState; return ss; } @Override publicvoidonRestoreInstanceState(Parcelablestate){ SavedStatess=(SavedState)state; super .onRestoreInstanceState(ss.getSuperState()); setCustomState(ss.state); } staticclassSavedStateextendsBaseSavedState{ intstate; SavedState(ParcelablesuperState){ super (superState); } privateSavedState(Parcel in ){ super ( in ); state= in .readInt(); } @Override publicvoidwriteToParcel(Parcelout,intflags){ super .writeToParcel(out,flags); out.writeInt(state); } publicstaticfinalParcelable.Creator<SavedState>CREATOR = new Parcelable.Creator<SavedState>(){ publicSavedStatecreateFromParcel(Parcel in ){ return new SavedState( in ); } publicSavedState[]newArray(intsize){ return new SavedState[size]; } }; } }

让我来解释一下上面所做的事情。

首先,既然重写了onSaveInstanceState,我就必须调用其父类的相应方法让父类保存它想保存的所有东西。在我的情况中,Switch将创建一个Parcelable,将状态放进去然后返回给自己。不幸的是,我们无法在这个parcelable中添加更多的状态,因此需要创建一个封装类来封装这个父类的状态,然后放入额外的状态。在安卓中有一个类(View.BaseSavedState)专门做这件事情 - 通过继承它来实现保存上一级的状态同时允许你保存自定义的属性。

在onRestoreInstanceState()期间我们则需要做相反的事情 - 从指定的Parcelable中获取上一级的状态 ,同时让你的父类通过调用super.onRestoreInstanceState(ss.getSuperState())来恢复它的状态。之后我们才能恢复我们自己的状态。

Since you override onSaveInstanceState() - always save super state - state of your super class.

View的ID必须唯一

现在我们决定将布局放在一个自定义的view中达到重用的效果,然后在其他的布局中include几次:

注:这里是include了两次。


当我们改变configuration之后,所有的状态都一团糟了,让我们看看在SparseArray中是什么情况:

哈哈!因为状态的保存是基于view id的,而SparseArray container是整个View层次结构中共享的 ,所以view的id必须唯一。否则你的状态就会被另外一个具有相同id的view覆盖。在这里有两个view的id都是@id/toggle,而container只持有一个它的实例- 存储过程中最后到来的一个。

到了恢复数据的时候 - 这两个view都从container那里得到一个相同的状态。

那么该如何解决这个问题?

最直接的答案是 - 每个子view都具有独立的SparseArray container,这样就不会重叠了:

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 publicclassMyCustomLayoutextendsLinearLayout{ ......... @Override publicParcelableonSaveInstanceState(){ ParcelablesuperState= super .onSaveInstanceState(); SavedStatess= new SavedState(superState); ss.childrenStates= new SparseArray(); for (inti=0;i<getChildCount();i++){ getChildAt(i).saveHierarchyState(ss.childrenStates); } return ss; } @Override publicvoidonRestoreInstanceState(Parcelablestate){ SavedStatess=(SavedState)state; super .onRestoreInstanceState(ss.getSuperState()); for (inti=0;i<getChildCount();i++){ getChildAt(i).restoreHierarchyState(ss.childrenStates); } } @Override protectedvoiddispatchSaveInstanceState(SparseArray<Parcelable>container){ dispatchFreezeSelfOnly(container); } @Override protectedvoiddispatchRestoreInstanceState(SparseArray<Parcelable>container){ dispatchThawSelfOnly(container); } staticclassSavedStateextendsBaseSavedState{ SparseArraychildrenStates; SavedState(ParcelablesuperState){ super (superState); } privateSavedState(Parcel in ,ClassLoaderclassLoader){ super ( in ); childrenStates= in .readSparseArray(classLoader); } @Override publicvoidwriteToParcel(Parcelout,intflags){ super .writeToParcel(out,flags); out.writeSparseArray(childrenStates); } publicstaticfinalClassLoaderCreator<SavedState>CREATOR = new ClassLoaderCreator<SavedState>(){ @Override publicSavedStatecreateFromParcel(Parcelsource,ClassLoaderloader){ return new SavedState(source,loader); } @Override publicSavedStatecreateFromParcel(Parcelsource){ return createFromParcel( null ); } publicSavedState[]newArray(intsize){ return new SavedState[size]; } }; } }

让我们过一遍这段乱麻了的代码:

  • 在自定义的布局中没我创建了一个特殊的SaveState类,它持有父类状态以及保存子view状态的独立SparseArray。

  • 在onSaveInstanceState()中我主动存储父类与子view的状态到独立的SparseArray中。

  • 在onRestoreInstanceState()中我主动从保存期间创建的SparseArray中恢复父类和子view的状态。

  • 记住如果这是一个ViewGroup - dispatchSaveInstanceState()还需要遍历子View然后保存它们的状态吗?既然我们现在是手动的了,我需要废弃这种行为。幸运的是使用dispatchFreezeSelfOnly()方法可以告诉安卓只保存viewGroup的状态,不要碰它的子View(在dispatchSaveInstanceState()中调用)。

  • dispatchRestoreInstanceState()需要做同样的事情 - 调用dispatchThawSelfOnly()。告诉安卓只恢复自身的状态 ,子view我们自己来处理。

下面是SparseArray的示意图:

正如你看到的每个view group都有了独自的SparseArray,因此他们就不会重叠和覆盖彼此了。

状态保存了 赚大了!

这篇文章的代码可以在GitHub上找到。

更多相关文章

  1. Android(安卓)透明状态栏实践
  2. android中使用自定义控件checkbox,ToggleButton,ProgressBar
  3. Android--隐藏状态栏图标
  4. 已解决:android 模拟器调用本地的webservice 引用不到
  5. Android待机状态更新
  6. Android之Intent显示和隐式调用
  7. Android的图形API调用(一)
  8. Android(安卓)SurfaceView onDraw()绘图问题
  9. android关于View的截图

随机推荐

  1. 【Android】Android(安卓)权限大全
  2. Android的几种数据存储方式
  3. Android——Intent 相关
  4. OpenGL ES教程I之创建OpenGL视图(原文对
  5. Android中bitmap图片透明度的处理(以撕美
  6. Android之Service
  7. Android中的Touch事件
  8. android github 知名库
  9. Cocos2d-x 3.2编译Android程序错误的解决
  10. Android实现模拟时钟(简单+漂亮)--时针、