android中正确保存view的状态

英文原文: 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="My Text"/>  

    <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="My Text"/>  

    <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

public class CustomSwitch extends Switch {

    private int customState;//所谓状态其实就是数据

    .......

    public void setCustomState(int customState) {

        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

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

            }  

        };

    }  

}

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

首先,既然重写了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

public class MyCustomLayout extends LinearLayout {

.........

    @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(null);

            }

            public SavedState[] newArray(int size) {

                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上 找到。

时间: 2024-11-03 05:27:12

android中正确保存view的状态的相关文章

Android中activity保存数据和状态在哪个方法实现

以前只知道在Activity销毁之前,要把数据保存在 onSaveInstanceState(Bundle)方法中,后来学习了别人的微博,学到了很多细节问题,所以整理了一下,希望能帮到大家. 如果看官方文档会发现:对于activity的销毁,有下面这么一个表: "Killable"表示当前activity是否可以被杀死,意思是说当上面标记为Killable的方法返回之后,activity就可能随时被杀死.从表中不难看出在onPause方法调用完之前,activity都是不能够被杀死的,

Android中Activity的4种状态

Android中Activity的四种状态: 1)Active/Runing: 一个新 Activity 启动入栈后,它在屏幕最前端,处于栈的最顶端,此时它处于可见并可和用户交互的激活状态. 2)Paused:当 Activity 被另一个透明或者 Dialog 样式的 Activity 覆盖时的状态.此时它依然与窗口管理器保持连接,系统继续维护其内部状态,所以它仍然可见,但它已经失去了焦点故不可与用户交互. 3)Stoped :当 Activity 被另外一个 Activity 覆盖.失去焦点

Android中Activity Window View ViewGroup之间的关系

Activity:是Android四大组件之一,用于展示一个与用户交互的界面 ----展示界面 ----与用户交互 Activity相当于控制器,负责调用业务类的方法.简单的业务可以直接在Activity中处理. Activity通过内置是Window对象的setContentView(资源位置.资源类型.资源)方法来展示界面. 用户通过View操作界面. 与用户交互时,通过View来捕获事件,再通过WindowManagerService传递消息(当前操作的控件,事件的类型).Android框

面试中被问到:Android中activity保存状态数据到底该在哪个方法中进行

今天接到一个电面,途中面试官问到一个问题,如果一个activity在后台的时候,因为内存不足可能被杀死,在这之前如果想保存其中的状态数据,比如说客户填的一些信息之类的,该在哪个方法中进行. 我听到的第一反应就是说:在onPause方法中进行保存状态的操作.但是面试官说:onPause()的持续时间很短,假如要进行一些长时间的操作呢? 然后我就纠结了,因为我知道,如果是因为内存不足而被清理,onDestroy()方法一般是不会被执行的.所以只好实话实说,只知道onDestroy在这种情况下不一定会

Android中自定义视图View之---进阶篇(Canvas的使用)

更多技术内容请移步:我的个人博客 一.前言 今天是周日,昨天刚刚写完了一篇关于如何搭建LNMP环境,让自己可以DIY有个性的个人主页: http://blog.csdn.net/jiangwei0910410003/article/details/50929955 那么今天,我们继续来看一篇关于Android中的UI篇,如何自定义视图View的进阶篇,关于前奏篇之前已经写过了,还没有了解的同学可以去看看:http://blog.csdn.net/jiangwei0910410003/articl

Android中Window添加View的底层原理

一,WIndow和windowManager Window是一个抽象类,它的具体实现是PhoneWindow,创建一个window很简单,只需要创建一个windowManager即可,window具体实现在windowManagerService中,windowManager和windowManagerService的交互是一个IPC的过程. 下面是用windowManager的例子: mFloatingButton = new Button(this); mFloatingButton.set

Android中自定义视图View之---前奏篇

前言 好长时间没写blog了,心里感觉有点空荡荡的,今天有时间就来写一个关于自定义视图的的blog吧.关于这篇blog,网上已经有很多案例了,其实没什么难度的.但是我们在开发的过程中有时候会用到一些自定义的View以达到我们所需要的效果.其实网上的很多案例我们看完之后,发现这部分没什么难度的,我总结了两点: 1.准备纸和笔,计算坐标 2.在onDraw方法中开始画图,invalidate方法刷新,onTouchEvent方法监听触摸事件 对于绘图相关的知识,之前在弄JavaSE相关的知识的时候,

Android中自定义视图View之---开发案例

自定义视图View的案例 下面我们就是开始正式的进入自定义视图View了 在讲解正式内容之前,我们先来看一下基本知识 1.我们在自定义视图View的时候正确的步骤和方法 1).必须定义有Context/Attrbuite参数的构造方法,并且调用父类的方法 public LabelView(Context context, AttributeSet attrs) 不然会报错: 2).重写onMeasure方法 @Override protected void onMeasure(int width

android中button有几种状态?

android中,按钮通常有三个状态:normal(正常状态);focus(焦点状态),pressed(按下状态) 按下后未松开前是pressed,表示按下.松开后当前项目获得焦点,是focused.focused的项只有一个. 更多请参考: Android开发技巧不同状态的Button:http://blog.csdn.net/sjf0115/article/details/7333895