关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android中正确保存view的状态

    Android中正确保存view的状态

    2016-01-09 00:00:00 出处:segmentfault
    分享

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

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

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

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

    android中正确保存view的状态

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

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

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

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

    如何保存View的状态?

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

    <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状态的。这里是正常情况下保存与恢复的示意图:

    android中正确保存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):

    <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的示意图:

    android中正确保存view的状态

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

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

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

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

    view要有id 要调用setSaveEnabled(true)

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

    保存自定义的状态

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

    public?class?CustomSwitch?extends?Switch?{
    
    ????private?int?customState;//所谓状态其实就是数据
    
    ????.......
    
    ????public?void?setCustomState(int?customState)?{
    ????????this.customState?=?customState;
    ????}??
    }

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

    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了两次。

    android中正确保存view的状态

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

    android中正确保存view的状态

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

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

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

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

    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的示意图:

    android中正确保存view的状态

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

    状态保存了 赚大了!

    该文的代码可以在 GitHub上 找到。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    抖音安全与信任开放日:揭秘推荐算法,告别单一标签依赖

    ultraedit编辑器打开文件时,总是提示是否转换为DOS格式,如何关闭?

    Cornell大神Kleinberg的经典教材《算法设计》是最好入门的算法教材

    从 Microsoft 下载中心安装 Windows 7 SP1 和 Windows Server 2008 R2 SP1 之前要执行的步骤

    Llama 2基于UCloud UK8S的创新应用

    火山引擎DataTester:如何使用A/B测试优化全域营销效果

    腾讯云、移动云继阿里云降价后宣布大幅度降价

    字节跳动数据平台论文被ICDE2023国际顶会收录,将通过火山引擎开放相关成果

    这个话题被围观超10000次,火山引擎VeDI如此解答

    误删库怎么办?火山引擎DataLeap“3招”守护数据安全

    IT头条

    平替CUDA!摩尔线程发布MUSA 4性能分析工具

    00:43

    三起案件揭开侵犯个人信息犯罪的黑灰产业链

    13:59

    百度三年开放2.1万实习岗,全力培育AI领域未来领袖

    00:36

    工信部:一季度,电信业务总量同比增长7.7%,业务收入累计完成4469亿元

    23:42

    Gartner:2024年全球半导体营收6559亿美元,AI助力英伟达首登榜首

    18:04

    技术热点

    iOS 8 中如何集成 Touch ID 功能

    windows7系统中鼠标滑轮键(中键)的快捷应用

    MySQL数据库的23个特别注意的安全事项

    Kruskal 最小生成树算法

    Ubuntu 14.10上安装新的字体图文教程

    Ubuntu14更新后无法进入系统卡在光标界面解怎么办?

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。