分类:C#、Android、VS2015;
创建日期:2016-02-21
一、简介
通过重写(也叫回调)对应的方法来管理Activity的生命周期,比如用户旋转屏幕时应用程序要能自动保存和恢复实例的状态,这对于开发一个健壮而又灵活的应用程序而言至关重要。
1、本节要点
一旦真正理解了Activity的生命周期,就可以轻松自如地通过C#代码去控制它了。这一节我们主要学习如何用Boundle存储简单类型的数据(比如int、double、string、bool、……等)。
当一个Activity停止或销毁时,系统会存储该Activity的状态,这个保存的状态称为实例状态。在Activity的生命周期中,Android提供了存储实例状态的三个选项:
- 在叫做Boundle的“key/value”字典中保存元数据的值。
- 创建一个自定义的类,用该类来维持复杂数据的状态,比如图像。
- 不保存Activity的任何状态。即:以上两个事都不做。比如,从活动A转到活动B后,活动A原来的临时数据丢就丢了,反正活动B也用不到这些数据。
下面主要介绍前两个选项。
2、利用Bundle保存和恢复场景(实例)的状态
由于实例状态保存在称为Boundle的“key/value”字典中,因此,当创建一个Activty时,就可以在重写的OnCreate()方法中传递boundle来恢复原来的实例状态。
但是,不建议将图像等复杂数据保存在boundle中,这是因为图像不容易被串行化为“key/value”,这种情况下,应该将整个图像文件作为一个简单数据类型来看待(仅仅用字符串保存图像的文件名或URL,而不是保存完整的图像数据)。
Bundle为Activity提供了“保存”和“恢复”实例状态的两个方法:
- OnSaveInstanceState – 当开始销毁Activity时Android将自动调用此方法。在这个方法中,可通过一系列【“键/值”对】来存储需要保存的项。
- OnRestoreInstanceState – 当OnCreate()方法完成后Android将自动调用此方法,通过重写该方法,就可以恢复原来的状态。
注:不要对“状态”这个词迷惑了,它的本质含义举例来说就是:当活动A从可见变为不可见,然后再从不可见转为可见后,活动A原来的数据、选项、……等“状态”还需要在再次可见时重现吗,如果需要重现,就先在重写的OnSaveInstanceState()方法中保存这些数据、选项、……等场景,然后再在重写的OnRestoreInstanceState()方法中恢复这些场景。这样一来,不管Android系统内部怎样折腾,这些数据也不会丢了,仍然能在重新可见时重现原来的场景。
下图演示了如何使用这两个方法:
(1)OnSaveInstanceState()方法
当一个Activity被Paused或者Stopped时,如果这个Activity对象仍然被保留在内存中,那么就可以简单地通过Resume让这个Activity返回到前台。可是,当内存紧张时,系统可能会销毁这个Activity,这样程序就无法简单地通过Resume还原这个Activity了。如果用户还想返回到这个Activity,系统必须新建一个Activity让用户觉得原来的那个Activity还存在,可是如何还原到销毁前的状态呢?解决办法就是让系统在杀死那个Activity之前先调用OnSaveInstanceState()来保存原来的状态,这样就可以用它去恢复了。
具体办法是:在重写的OnCreate()方法或者重写的OnRestoreInstanceState()方法中从传递的Bundle中解析出保存的信息,并用它来恢复Activity原来的状态。
如果原来并没有储存状态信息,此时传入的Bundle将为null,比如第1次创建Activity时就是这种情况。
总之,将Activity状态完整地返回给用户的两种情况有:
(a)如果原来的Activity对象还保存在内存中,系统就直接通过Resumed还原原来的状态;
(b)如果原来的Activity对象已经不存在,系统就新建一个Activity,并检查是否保存了原来的状态,如果保存了就还原它,否则将新建的状态设置为null。
注意:系统不一定每次都在销毁这个Activity之前调用OnSaveInstanceState()方法 ,比如用户使用【Back】键关闭了这个Activity。在这种情况下,系统通常会在OnStop()方法之前或者在OnPause()之前调用OnSaveInstanceState(),而不是在销毁前去调用它。
还有一些情况,比如在layout文件夹下的布局文件中声明的控件,即使你不显式调用OnSaveInstanceState()方法,系统仍然会默认将其状态保存下来。但前提是你必须为想要保存其状态的控件(widget)提供一个唯一的ID。如果不给widget指定ID,系统是无法保存它的状态的。
另外,通过把android:saveEnabled 设置为"false",或者调用 SetSaveEnabled() 方法,也可以显式地阻止layout中的某个view保存状态。不过,通常不应该禁用保存,但是,假如你需要恢复activity UI的各个不同的状态,也许可以这么做。
尽管默认实现的 OnSaveInstanceState() 方法会保存activity UI的有用信息,但你仍然需要重写(override)它来存入更多的信息。 例如,你可能需要保存在activity生命周期中改变的成员变量值(比如恢复UI的值,默认情况下,这些UI状态的成员变量值是不会被恢复的)。
因为默认实现的 OnSaveInstanceState() 方法已经帮你保存了一些UI的状态,所以如果你为了保存更多的状态信息而重写此方法,那么在执行自己的代码之前应该确保先调用一次父类的 OnSaveInstanceState() 方法。同理,如果重写 OnRestoreInstanceState(),也应该调用一次父类的该方法,这样缺省的代码就能正常恢复view的状态了。
注意:因为 OnSaveInstanceState() 并不保证每次都会被调用,所以你应该只用它来记录activity的一些临时状态信息(UI的状态)——千万不要用它来保存那些需要长久保存的数据(比如那些需要存入数据库里的数据),而是应该在用户离开activity的时候在重写的 OnPause()方法中来保存这些永久性数据。
一个检测应用程序状态恢复能力的好办法就是旋转设备,使屏幕方向发生改变(在模拟器中可通过按<Ctrl>+<F11>让其旋转)。当屏幕的方向改变时,因为要换用符合实际屏幕参数的资源,所以系统会销毁并重建这个activity。正因如此,你的activity在被重建时能完整地恢复状态非常重要,因为用户可能会在使用应用程序时频繁地旋转屏幕。
本节的例子演示了如何在重写的OnSaveInstanceState()方法中保存状态数据。在这个例子中,用户每单击一次按钮,都会将count的值加1,并在TextView中将count的值显示出来。当改变配置状态时--比如用户旋转屏幕时--会丢失count的值(此时boundle将会为null),重而写该Activity的OnSaveInstanceState()方法即可保存count的值。这样一来,当设备旋转到一个新的状态时(比如按<Ctrl>+<F11>将模拟器从纵向变为横向),由于count的值已经保存到boundle中,因此可通过下面的办法重新获取这个值:
count = bundle.GetInt ("counter", -1);
注意:调用base.OnSaveInstanceState (outState)非常重要,这能确保始终存储视图的层次结构的所有原始状态。
(2)OnRestoreInstanceState()方法
设备的某些配置可能会在运行时发生变化(比如屏幕方向、键盘可用性以及国家语言等状态发生了变化)。当发生这些变化时,Android会重建这个运行中的activity,即:系统会先调用OnDestroy() ,再调用OnCreate()。这种设计有助于应用程序适用新的参数配置,措施就是通过把你预置的可替换资源(比如对应各种屏幕方向和尺寸的layout)自动重新装载到应用程序中。
如果你采取了适当的设计,让activity能够正确地处理这些因为屏幕方向而引起的重启,并能如上面所述的那样恢复activity的状态,那么你的应用程序将对生命周期中其它的意外事件更具适应能力。
处理这类重启的最佳方式,就是在重写的OnSaveInstanceState()方法中保存状态,在重写的OnRestoreInstanceState()或者OnCreate()中恢复状态。
这个例子是通过在重写的OnCreate()方法恢复状态的。
实际上,在许多情况下,并不需要总是重写OnRestoreInstanceState()方法,因为大多数活动都可以在重写的OnCreate()方法中直接恢复原来的状态。
二、示例2—状态保存和恢复
此示例演示了如何保存和恢复用户旋转屏幕前后的状态。
1、运行截图
单击按钮3次的截图如下:
按【Ctrl】+【F11】后的截图如下:
再次按【Ctrl】+【F11】再次切换为纵向放置。
2、实现步骤
(1)添加ch1102_Layout.axml文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:text="你单击了按钮 0 次" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/textViewCount" android:layout_margin="20dp" android:gravity="center_horizontal" /> <Button android:id="@+id/btn1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="请多次单击" /> <TextView android:text="提示:【Ctrl】+【F11】用于控制模拟器的屏幕【纵向/横向】转换,多次按它观察屏幕旋转后单击按钮的次数显示的情况。" android:textAppearance="?android:attr/textAppearanceSmall" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/textView2" android:layout_margin="20dp" /> </LinearLayout>
(2)添加ch1102Activity.cs文件
using Android.App; using Android.OS; using Android.Widget; namespace MyDemos.SrcDemos { [Activity(Label = "【例11-2】状态保存与恢复")] public class ch1102Activity : Activity { private int count = 0; protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); SetContentView(Resource.Layout.ch1102_Layout); var txtCount = FindViewById<TextView>(Resource.Id.textViewCount); if (savedInstanceState != null) { count = savedInstanceState.GetInt("counter", 0); txtCount.Text = $"你单击了按钮 {count} 次"; } var btn1 = FindViewById<Button>(Resource.Id.btn1); btn1.Click += (s, e) => { count++; txtCount.Text = $"你单击了按钮 {count} 次"; }; } protected override void OnSaveInstanceState(Bundle outState) { outState.PutInt("counter", count); base.OnSaveInstanceState(outState); } } }