前言
前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。
问我:为啥EditText上文字能够恢复?
我说:你Activity 配置了横竖屏切换时不重建Activity。
他立马给我展示了:Activity 重建的日志。
我说:系统会在重建Activity 的时候恢复整个ViewTree吧。
他又给我展示了:ImageView 横竖屏时没有恢复之前的图像。
我:...
不服输的我开始了默默地研究,于是有了这篇总结以解心中困惑。
通过本篇文章,你将了解到:
1、onSaveInstanceState/onRestoreInstanceState 作用。
2、onSaveInstanceState/onRestoreInstanceState 原理分析
3、onSaveInstanceState/onRestoreInstanceState 触发场景。
4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?
5、与Jetpack ViewModel 区别。
1、onSaveInstanceState/onRestoreInstanceState 作用
EditText/ImageView 横竖屏地表现
tt0.top-423136.gif可以看出,从竖屏到横屏再恢复到竖屏,EditText 内容没有变化。而从竖屏到横屏时,ImageView 内容已经丢失了。
都是系统控件,咱们也没有进行其它的额外区别处理,为啥表现不一致呢?
View.java 里有两个方法:
#View.java
protected Parcelable onSaveInstanceState() {...}
protected void onRestoreInstanceState(Parcelable state){...}
官方注释上写的比较清楚了:
1、onSaveInstanceState 是个钩子方法,View.java 的子类可以重写该方法,在方法里面存储一些子类的内部状态,用以下次重建时恢复。
2、onRestoreInstanceState 也是个钩子方法,用以恢复在onSaveInstanceState 里保存的状态。
既然是View的方法,分别查看EditText 与ImageView 对它们的重写情况:
#TextView.java
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
...
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
...
ss.text = sp;
} else {
//将TextView 内容存储在SavedState里
ss.text = mText.toString();
}
}
...
return ss;
}
//返回存储的对象
return superState;
}
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.text != null) {
//取出TextView 内容,并设置
setText(ss.text);
}
...
}
由此可见,TextView 重写这俩方法,先是在onSaveInstanceState 里存储文本内容,再在onRestoreInstanceState 里恢复文本内容。
而通过查看ImageView 发现它并没有重写这俩方法,当然就不能恢复了。其实这也比较容易理解,毕竟对于ImageView,Bitmap 是它的内容,暂存这个Bitmap 很耗内存。
需要注意的是:想要onSaveInstanceState 被调用,则需要给该控件设置id。因为系统是根据View id将状态存储在SparseArray 里
Activity 横竖屏的处理
现在的问题是:谁调用了View 的onSaveInstanceState/onRestoreInstanceState ? 在前一篇分析过Activity 和View的关系:Android Activity 与View 的互动思考
可知,Activity 通过Window 控制View,我们子类继承自EditText,并重写onSaveInstanceState/onRestoreInstanceState,然后在横竖屏切换时查看这俩方法的调用栈:
第一个红色框表示EditText子类里的方法(onSaveInstanceState),而第二个红框表示Activity 子类里重写的方法(onSaveInstanceState)。
由此可知,当横竖屏切换时调用了Activity.onSaveInstanceState(xx) 方法。
#Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
//saveHierarchyState 调用整个ViewTree 的onSaveInstanceState 方法
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
...
//告知生命周期回调方法状态已保存
dispatchActivitySaveInstanceState(outState);
}
同样的对于onRestoreInstanceState:
#Activity.java
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//恢复整个ViewTree 状态
mWindow.restoreHierarchyState(windowState);
}
}
}
当横竖屏切换时,会调用到Activity onSaveInstanceState/onRestoreInstanceState 方法,进而会调用整个ViewTree onSaveInstanceState/onRestoreInstanceState 方法来保存与恢复必要的状态。
Activity 数据保存与恢复
Activity 的onSaveInstanceState/onRestoreInstanceState 方法 除了触发View 的状态保存与恢复外,还可以将Activity 用到的一些重要的数据保存下来,待下次Activity 重建时恢复。
重写两者:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String restore = savedInstanceState.getString("say");
Log.d("fish", restore);
}
此时我们注意到onSaveInstanceState 的入参是Bundle 类型,往outState 写入数据,在onRestoreInstanceState 将数据取出,outState/savedInstanceState 必然不为空。
总结 onSaveInstanceState/onRestoreInstanceState 作用
1、保存与恢复View 的状态。
2、保存与恢复Activity 自定义数据。
2、onSaveInstanceState/onRestoreInstanceState 原理分析。
onSaveInstanceState 调用时机
之前 Android Activity 生命周期详解及监听 有详细分析了Activity 各个阶段的调用情况,现在结合生命周期来分析onSaveInstanceState(xx)在生命周期中的哪个阶段被调用的。
调用栈如下:
看看上图标黄色的方法,这方法很眼熟,在Activity 生命周期中分析过,它是Activity.onStop()方法的调用者:
#ActivityThread.java
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
//这句话翻译过来:
//如果目标设备是Android 9之前,那么onSaveInstanceState 在onStop 之前调用
//如果在Android 9 之后,那么onSaveInstanceState 在onStop 之后调用
//Honeycomb 指的是Android 3.0 现在基本可以忽略了。
//r.activity.mFinished 表示Activity 是否即将被销毁
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
//Android p 之前先于onStop 之前执行
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
//最终执行到Activity.onStop()方法
r.activity.performStop(r.mPreserveWindow, reason);
} catch (SuperNotCalledException e) {
...
}
//标记Stop状态
r.setState(ON_STOP);
if (shouldSaveState && !isPreP) {
//调用onSave 保存
callActivityOnSaveInstanceState(r);
}
}
以上注释比较详细了,小结一下:
1、在Android 9之前,onSaveInstanceState 在onStop 之前调用(至于在onPause 之前还是之后调用,这个时机不确定);在Android 9(包含)之后,onSaveInstanceState 在onStop 之后调用。
2、如果Activity 即将被销毁,则onSaveInstanceState 不会被调用。
对于第二句的理解,举个简单例子:
Activity 在前台时,此时按Home键回到桌面,会执行onSaveInstanceState;若是按back键/主动finish,此时虽然会执行到onStop,但是不会执行onSaveInstanceState。
onRestoreInstanceState 调用时机
现在已经弄清楚onSaveInstanceState 调用时机,接着来分析 onRestoreInstanceState 什么时候执行。
调用栈如下:
image.png
黄色部分的方法也很眼熟,它是Activity.onStart()方法的调用者:
public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
final Activity activity = r.activity;
...
//最终执行到Activity.onStart()
activity.performStart("handleStartActivity");
r.setState(ON_START);
...
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
//从持久化存储里恢复数据
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
//从内存里恢复数据
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}
小结:
1、onRestoreInstanceState 在onStart()方法之后执行。
2、pendingActions.shouldRestoreInstanceState() 返回值是执行onRestoreInstanceState()方法的关键,它是在哪赋值的呢?接下来会分析。
3、r.state 不能为空,毕竟没数据无法恢复。
通过以上分析,结合Activity生命周期,onSaveInstanceState /onRestoreInstanceState 调用时机如下:
image.png
onRestoreInstanceState 与onCreate 参数差异
onCreate参数也是Bundle类型,实际上这个参数就是onSaveInstanceState里保存的Bundle,这个Bundle分别传递给了onCreate和onRestoreInstanceState,而onCreate里的Bundle可能为空(新建非重建的情况下),onRestoreInstanceState 里的Bundle必然不为空。
官方注释也说了在onRestoreInstanceState里处理数据的恢复更灵活。
3、onSaveInstanceState/onRestoreInstanceState 触发场景
横竖屏触发的场景
在前面的分析中,与Activity 生命周期关联可能会让人有种印象:
onSaveInstanceState 调用之后onRestoreInstanceState 就会被调用。
而事实并非如此,举个简单例子:
Activity 处在前台时,此时退回到桌面,onSaveInstanceState 会被执行。而后再让Activity 回到前台,onStart()方法执行后,发现onRestoreInstanceState 并没有被调用。
也就是说onSaveInstanceState/onRestoreInstanceState 的调用不一定是成对出现的。
还记得在分析onRestoreInstanceState 遗留了个问题:pendingActions.shouldRestoreInstanceState() 返回值如何确定的 ?
在横竖屏切换时,onRestoreInstanceState 被调用了,说明pendingActions.shouldRestoreInstanceState() 在横竖屏切换时返回了true,接着来看看其来龙去脉:
#PendingTransactionActions.java
//判断是否需要执行onRestoreInstanceState 方法
public boolean shouldRestoreInstanceState() {
return mRestoreInstanceState;
}
//设置标记
public void setRestoreInstanceState(boolean restoreInstanceState) {
mRestoreInstanceState = restoreInstanceState;
}
只需要找到setRestoreInstanceState()在何处调用即可。
直接说结论:
ActivityThread.handleLaunchActivity() 里设置了setRestoreInstanceState(true)
而handleLaunchActivity()在两种情况下被调用:
image.png
横竖屏时属于重建 Activity,因此onRestoreInstanceState 能被调用。
而从后台返回到前台,并没有新建Activity也没有重建Activity,因此onRestoreInstanceState 不会被调用。
又引申出另一个问题:为啥新建Activity 时onRestoreInstanceState 没被调用?
答案:因为新建Activity 时,ActivityClientRecord 是全新的对象,它所持有的Bundle state 对象为空,因此不会调用到onRestoreInstanceState。
其它配置项更改的场景
除了横竖屏切换时会重建Activity,还有以下配置项更改会重建Activity:
image.png
当然,还有一些不常涉及的配置项,比如所在地区更改等。
重建Activity 的细节
image.png当需要重建Activity 时,AMS 发出指令,会执行到ActivityThread.handleRelaunchActivity()方法。
#ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
//从Map 里获取缓存的ActivityClientRecord
ActivityClientRecord r = mActivities.get(tmp.token);
...
//将ActivityClientRecord 传递下去
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}
mActivities 定义如下:
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
以IBinder 为key,存储ActivityClientRecord。
当新建Activity 时,存入ActivityClientRecord,当销毁Activity 时,移除ActivityClientRecord。
再来分析handleRelaunchActivityInner():
#ActivityThread.java
private void handleRelaunchActivityInner(...) {
...
if (!r.paused) {
//最终执行到onPause
performPauseActivity(r, false, reason, null /* pendingActions */);
}
if (!r.stopped) {
//最终执行到onStop
callActivityOnStop(r, true /* saveState */, reason);
}
//最终执行到onDestroy
handleDestroyActivity(r.token, false, configChanges, true, reason);
//创建新的Activity 实例
handleLaunchActivity(r, pendingActions, customIntent);
}
通过分析Activity 重建的细节,有以下结论:
1、Activity 重建过程中,先将原来的Activity 进行销毁(从onResume--onStop-->onDestroy 的生命周期)。
2、虽然是不同的Activity 对象,但重建时使用的ActivityClientRecord 却是相同的,而ActivityClientRecord 最终是被ActivityThread 持有,它是全局的。这也是onSaveInstanceState/onRestoreInstanceState 能够存储与恢复数据的本质原因。
当然也可以通过配置告诉系统在配置项变更时不重建Activity:
<activity android:name=".viewmodel.ViewModelActivity" android:configChanges="orientation|screenSize"></activity>
比如以上配置,当横竖屏切换时,不会重建Activity,而配置项的变更会通过Activity.onConfigurationChanged()方法回调。
4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?
onSaveInstanceState/onRestoreInstanceState 的参数都是Bundle 类型,思考一下为什么需要定义为Bundle类型呢?
在 Android IPC 精讲系列 中有提到过,Android 进程间通信方式大多时候使用的是Binder,而要想自定义数据能够通过Binder传输则需要实现Parcelable 接口,Bundle 实现了Parcelable 接口。
由此我们推测,onSaveInstanceState/onRestoreInstanceState 可能涉及到进程间通信,才会用Bundle 来修饰形参。但之前说的ActivityClientRecord是存储在当前进程的啊,貌似和其它进程没有关联呢?
要分析这个问题,实际上只需要在onSaveInstanceState 存储一个比较大的数据,看看报错时的堆栈。
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
//存储2M 数据
outState.putByteArray("big", new byte[1024*1024*2]);
}
保存2M 的数据,通常来说这是超出了Binder的限制,当调用onSaveInstanceState 时会有报错信息:
image.png
果然还是crash了。
找到 PendingTransactionActions ,它实现了Runnable 接口,在其run方法里:
#PendingTransactionActions.java
public void run() {
try {
//提交给ActivityTaskManagerService 处理,属于进程间通信
//mState 即是onSaveInstanceState 保存的数据
ActivityTaskManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
...
}
}
而在ActivityThread.java 里有个方法:
public void reportStop(PendingTransactionActions pendingActions) {
mH.post(pendingActions.getStopInfo());
}
该方法用于告知系统,咱们的Activity 已经变为Stop状态了,最终会执行到PendingTransactionActions.run()方法。
小结一下:
onSaveInstanceState 存储的数据,在onStop执行后,当前进程需要将Stop状态传递给ATM(ActivityTaskManagerService 运行在system_server进程),因为跨进程传递(Binder)有大小限制,因此onSaveInstanceState 不能传递大量数据。
5、与Jetpack ViewModel 区别
onSaveInstanceState 与 ViewModel 都是将数据放在ActivityClientRecord 的不同字段里。
image.png
1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。
2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。
3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。
更多的区别后续分析 ViewModel 时会提到。
测试地址
本文基于Android 10.0。
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 实践与原理系列
网友评论