1、前言
针对Activity和Fragment状态的保存与恢复,首先来列举两个生活中的场景,并以此来说明现场保护的重要性。
- 比如你到银行开户,好不容易等到一个窗口,并且已经填完了开户申请单,就差短信验证码了,可短信验证码迟迟不来,后面的人等急了,因此你就把窗口让给他,让他先办,等他办完了,你的验证码也来了,于是你又回到窗口,这个时候,业务员递给你一张空白的申请单,让你重新填写,你一定会抱怨,他没有保存你之前的状态。
- 再比如,你到饭馆吃饭,刚吃了两口,这时候来了个电话,因为饭店太吵,于是你起身出去找个安静点的地方接听电话,几分钟后打完了电话,回来后发现饭菜不见了,你可能会质疑服务员,为什么不保留我的饭菜呢?
生活里的这些个场景都需要保存状态,就是当再次回来的时候状态还是离开时的样子,同样地,应用也需要保存状态,以便再次打开应用后还是原来的状态。
2、系统默认横竖屏切换时Activity的生命周期
2.1、Activity的状态
- 什么是对象的状态呢?我们知道在Java中,对象是状态和行为的结合体,对象所有成员变量的值构成 了对象当前的状态,同样地,Activity所有成员变量的值就是Activity的状态。另外Activity都有一个显示视图的窗口,这个视图树上每一个视图的状态也是Activity状态的一部分。
知识点补充
:下面写代码会用,到详见SimpleDateFormat
1.下面我们来进行代码演示。首先创建一个名为State
的项目,布局文件的代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="创建时间:" />
<TextView
android:id="@+id/tvCreateTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="意见反馈:" />
<EditText
android:id="@+id/etFeedback"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginTop="8dp"
android:text="提交" />
</LinearLayout>
2.MainActivity代码如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity-state";
private String currentTime;
private TextView tvCreateTime;
private EditText etFeedback;
private Button btnSubmit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取当前时间
currentTime = SimpleDateFormat.getDateTimeInstance().format(System.currentTimeMillis());
Log.d(TAG, "onCreate: " + currentTime);
//初始化控件
initView();
}
private void initView() {
tvCreateTime = findViewById(R.id.tvCreateTime);
tvCreateTime.setText(currentTime);
etFeedback = findViewById(R.id.etFeedback);
btnSubmit = findViewById(R.id.btnSubmit);
}
}
3.运行代码,结果如图所示,则该Activity的状态则包含第二个文本框中显示时间的文本内容和编辑框当中的文本内容两个状态。
2.2、系统配置变更
当我们的应用正在运行的时候,手机的配置可能会发生变化,比如说:
屏幕的方向
从竖屏切换到横屏;再比如说系统的语言
从英文切换到了中文等等。当这些变化发生的时候,系统为了让Activity能够加载能够与当前配置相匹配的资源,会重启Activity,而这个重启行为会包含两个动作:一是销毁正在运行的Activity对象;二是创建一个新的Activity对象。
1.下面我们继续编写代码,在MainActivity中的onCreate函数中添加一句打印语句,并重写onDestro函数,也添加一句打印语句,将Activity对象打印出来。
Log.d(TAG, "onCreate: " + this);
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: " + this);
}
2.运行程序,旋转手机的屏幕,查看打印信息如下,这边印证了上面的说法,并且文本框中的时间也发生了变化。
3、Activity的现场保护方式1
3.1、应对配置变更-限定方向
针对上面讲到的行为造成的后果,我们第一个应对方法是限定屏幕的方向,比如我们将Activity的方向限定为竖屏,这样就告诉Android系统,这个应用压根就不支持横屏,就不需要加载和横屏相匹配的资源,那系统自然就不会销毁并重新创建Activity了。
1.我们需要在AndroidManifest.xml
文件中给activity标签
添加一个screenOrientation
属性,将其设置为portrait
(竖屏)或者landscape
(横屏,现在好多手机游戏就支持横屏模式)。代码如下所示:
<activity
android:name=".MainActivity"
android:screenOrientation="portrait">
......
</activity>
2.代码运行结果是Activity始终未竖屏模式,并不会随着手机屏幕的旋转而销毁和重新创建。log打印结果略。
3.2、应对配置变更-我们来处理变更
限定屏幕方向的使用场景有限,这样做,毕竟限制了用户对设备的使用。下面来看第二个应对方法:告诉Android的系统,你不要重启Activity了,我们来处理这个变更。
当在activity加上configChanges
属性时,就不会重启activity.而只是调用onConfigurationChanged
方法,这样就可以在这个方法里调整显示方式了(这里我们不重写该方法,具体看后面的代码演示)。
1.我们需要在AndroidManifest.xml
文件中给activity标签
添加一个configChanges
属性,将其设置为orientation|screenSize|keyboardHidden
,这样做会告诉Android系统,屏幕方向发生变化时,我们自己来处理屏幕的方向、屏幕的宽高及键盘的可见性等这些配置的变化。代码如下所示:
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden">
......
</activity>
2.此时再次运行代码,旋转屏幕时,使用的是同一个Activity,并且状态也得到了保存。
3.3、应对配置变更-让系统处理变更
1.删除上一步骤在清单文件中添加的configChanges
属性,让系统来处理变更。
2.在MainActivity
中添加以下代码:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
Log.d(TAG, "onRestoreInstanceState() called with: savedInstanceState = [" + savedInstanceState + "]");
}
3.运行代码,该变手机屏幕方向,查看日志打印结果:
这里面用到三个回调函数:
- onSaveInstanceState
- onCreate
- onRestoreInstanceState
下面会针对这几个回调函数作具体的分析
4、Activity的现场保护方式2
4.1、保存时机
当需要保存状态的时候,onSaveInstanceState这个函数会被调用,然而什么时候需要保存状态呢?
- 按HOME键时:此时Activity处于停止状态(onStop),已经处在后台,当系统资源紧张的时候,就有可能把这个Activity销毁掉(onDestroy)。
- 被来电覆盖时:情况和上面差不多
当然,并不是任何时候都需要保存状态,比如去饭馆吃完饭结账离开了,就不要再保存状态了,正如我们按了返回键,这个时候意图非常明显,就是我要离开这个Activity了。总之我们是否应该保存Activity状态的衡量标准就是是否符合用户的预期。
以上这些场景我们可以利用模拟器来进行模拟,这里就不再赘述了。
4.2、保存Activity状态-视图状态
一个Activity的状态主要由两部分组成:一个是成员变量的值;另外一个是构成界面的整个视图树上每个视图的状态。默认情况下,Android已经保存了视图的状态,系统定义的视图控件的状态,都会被保存下来但是有前提条件:
- 设置了id属性;
- 实现了onSaveInstanceState回调
要想实现状态的恢复还需要一个条件:- 实现了onRestoreInstanceState回调
我们可以尝试去掉控件的id和注释掉super.onSaveInstanceState(outState);
进行模拟,最终结果就是不会保存Activity的状态。
4.3、保存Activity状态-Bundle
- 要保存状态,我们就需要一个存放数据的容器,同样要恢复状态,也需要一个容器从里面读取数据,而Bundle就是这样一个容器。
- Bundle(和HashMap有点像)可以存放一系列的键值对形式的数据。在需要保存状态的时候,我们给状态一个名字(键),然后把这个名字和状态的值存到Bundle对象里。
- 系统会管理Bundle对象,在系统重启Activity的时候,系统会把销毁的Activity的状态存到Bundle对象里,然后再把这个Bundle对象(新旧Activity使用的是同一个Bundle对象,可以自己尝试在上面的三个回调函数中打印log进行验证)传递给新创建的Activity对象。
- Bundle提供了一系列put和get方法,put方法用于向Bundle中写数据,get方法用与从Bundle中读数据。
4.4、Activity状态的保存与恢复
1.修改MainActivity的代码为:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity-state";
private String currentTime;
private TextView tvCreateTime;
private EditText etFeedback;
private Button btnSubmit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
if (savedInstanceState != null) {
currentTime = savedInstanceState.getString("currentTime");
etFeedback.setText(savedInstanceState.getString("feedback"));
} else {
//获取当前时间
currentTime = SimpleDateFormat.getDateTimeInstance().format(System.currentTimeMillis());
}
tvCreateTime.setText(currentTime);
}
private void initView() {
tvCreateTime = findViewById(R.id.tvCreateTime);
etFeedback = findViewById(R.id.etFeedback);
btnSubmit = findViewById(R.id.btnSubmit);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("currentTime", this.currentTime);
outState.putString("feedback", etFeedback.getText().toString());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
运行代码,连续两次旋转屏幕,如下图所示,Activity的状态得到了保存与恢复:
5、Fragment的现场保护
很多应用里都用Fragment来承载ui,每个Fragment对象都有它所依赖的Activity对象,它的生命周期也受制于它所依赖的Activity对象,当系统配置发生变更的时候,Activity对象会被重启,依附于Activity对象的Fragment的对象也会被重启,也就是旧Fragment对象的销毁和新Fragment对象的创建,那如何应对呢?接着往下看。
5.1、保持Fragment的对象
保持Fragment对象,就是在手机处于水平和竖直方向时使用一个Fragment对象,告诉系统不要重启正在运行的Fragment对象。要做到这点,需做到:
1. 扩展Fragment
2. 在onCreate函数里调用setRetainInstance(true)
;
3. 把Fragment对象添加到Activity中;
4. 当Activity重启时,通过FragmentManager获取此Fragment对象
1.新建一个空白的Fragment
,不需要包含方法和接口,勾选上布局文件,布局文件fragment_score.xml
代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ScoreFragment">
<TextView
android:id="@+id/tvShowScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="50sp" />
<Button
android:id="@+id/btnAdd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Add" />
</RelativeLayout>
2.ScoreFragment
代码修改如下:
public class ScoreFragment extends Fragment {
private static final String TAG = "ScoreFragment-state";
private int score;
public ScoreFragment() {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//添加保持Fragment对象的属性
setRetainInstance(true);
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_score, container, false);
final TextView tvShowScore = view.findViewById(R.id.tvShowScore);
tvShowScore.setText(String.valueOf(this.score));
final Button btnAdd = view.findViewById(R.id.btnAdd);
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tvShowScore.setText(String.valueOf(++ScoreFragment.this.score));
}
});
return view;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: " + this);
}
}
3.新建一个ScoreActivity
,不需要布局文件,并将其在AndroidManifest
清单文件中注册为默认启动的Activity,其代码修改如下:
public class ScoreActivity extends AppCompatActivity {
private static final String TAG = "ScoreActivity-state";
private Fragment fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//通过FragmentManager来查找Fragment对象,若没有找到,则新建
FragmentManager fm = getSupportFragmentManager();
fragment = fm.findFragmentByTag("ScoreFragment");
if (fragment == null) {
fragment = new ScoreFragment();
fm.beginTransaction()
.replace(android.R.id.content, fragment, "ScoreFragment")
.commit();
}
Log.d(TAG, "onCreate: ");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}
}
4.运行代码,点击Add按钮,然后旋转手机屏幕,发现与旋转屏幕前显示的数字并没有变化,日志打印结果如下,发现Fragment对象并没有被销毁。
5.2、保存和恢复Fragment的状态
如果Fragment在配置发生变化的时候不需要加载不同的资源,最好使用上面的保持Fragment对象的方法;
但有的时候会需要对正在运行的Fragment进行重启,这样就涉及到如何把旧Fragment对象的状态保存起来,然后把保存的状态传递给新创建的Fragment对象,和Activity类似(无onRestoreInstanceState回调方法):
- onSaveInstanceState:可以将Fragment对象的状态信息保存到Bundle对象当中;
- onActivityCreated:会收到一个Bundle对象,可以将之前的状态读取出来,以便进行恢复。
1.删除(注释)ScoreFragment
中onCreate
方法中的保持Fragment对象的属性setRetainInstance(true);
2.在ScoreFragment
中添加onSaveInstanceState
方法,代码如下:
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
//保存状态
outState.putInt("score", this.score);
Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
}
3.然后我们在onCreate
方法中恢复状态(也可以在onActivityCreated
方法中恢复),在onCreate
方法中添加下面代码:
if (savedInstanceState!=null) {
//恢复状态
this.score=savedInstanceState.getInt("score");
}
4.运行代码,点击Add按钮,然后旋转屏幕,发现数字没有发生变化。日志打印结果如下: