Android页面销毁、重建与数据恢复
一、页面销毁和重建
1.页面销毁
Android的页面销毁可以分两种,正常的销毁和非正常的销毁。在正常的销毁情况下,页面的状态信息被丢弃,不会被重建,比如调用了activity 的finish()方法、杀死了进程、用户通过点击返回键退出了activity等。非正常的销毁是由于activity处于stopped状态,并且它长期未被使用,或者前台的activity需要更多的资源,这些情况下系统就会关闭后台的进程,以恢复一些内存。当activity被重新展现时会被自动重建。当手机屏幕旋转时,activity(如果没有锁定方向的话)也会被销毁并自动重建。
两种销毁方式都会伴随着Activity onDestroy()的调用和Activity对象的内存回收(如果Activity未被不恰当引用)。但正常销毁情况下,onDestroy()回调中isFinishing()为true,非正常销毁情况下,isFinishing()为false。
2.页面重建和数据恢复
非正常销毁的activity被重新展示时,会重新创建Activity对象,onCreate()等回调都会走一遍,但此时onCreate(Bundle savedInstanceState)的savedInstanceState参数不为空。如果想展示回被销毁前状态,就需要利用这个变量。
举个例子,设置用户信息页面里有更改用户头像的功能,选择图片返回到该页面时会把选择后的图片路径存储在mImagePath变量中,并显示更新后的图像;此时按下home键,等该页面会处于stopped状态,如果页面被销毁,等重新打开app时,由于页面重新创建,Activity对象重新创建,走onCreate()等流程,所以mImagePath还是初始的值,与用户未选择图片的效果一致,用户就有可能有疑惑:我选择了图片,为什么还显成原来的图像?
可以看到,选择图片前头像是一张美女的照片,选择图片后头像变成一张室内的照片,页面销毁和重建后又变成了未选择照片前美女的照片。
这种时候,重写Activity的onSaveInstanceState(Bundle outState)方法,将mImagePath变量保存到outState变量中,然后在onCreate(Bundle savedInstanceState)或onRestoreInstanceState(Bundle savedInstanceState)中将该变量读取出来,赋值给mImagePath,并让ImageView加载该路径,即可在页面重建后展示回页面销毁前的状态。实现如下:
private String mImagePath;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("imagePath", mImagePath);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String imagePath = savedInstanceState.getString("imagePath");
if (!TextUtils.isEmpty(imagePath)) {
mImagePath = imagePath;
ImageLoaderUtils.displayImage(UserInfoActivity.this, "file://" + imagePath, mAvatarImg, R.drawable.all_head64);
}
}
更改后的效果如下:
可以看到页面销毁前头像显示的是一张室内的照片,等重建后仍然显示为室内的照片。
3.模拟页面销毁和重建
模拟页面销毁和重建的方法有几种。一种是在开发者模式中打开不保留活动开关,这样每打开一个背景不透明的ActivityA,底部的ActivityB都会被销毁,等从ActivityA按返回键,被销毁的ActivityB就会被重建,重新显示。这种方法,只会销毁页面,不会杀掉进程。另一种需要在targetApi为22以上使用,在应用的权限管理页面把已经打开的权限关闭,该应用的进程会被杀掉(部分小米手机不会,华为等手机会)。点击应用图标,之前打开的页面也会依次重建出来,大致重建顺序为从栈顶到栈底,但并不是一定有序。还有一种方法就是不锁定Activity方向,进行横竖屏旋转。
二、一些拓展的问题
上部分简单介绍了页面销毁、重建和数据恢复,这部分相对深入点进行一些探讨。
1.View的数据恢复
先看一个未做任何数据恢复的页面销毁和重建的效果,
在这个例子中并没有保存和恢复EditText中内容,但经过页面销毁和重建后,EditText中的内容仍然得到了保持!Android是在哪里帮我们做得呢?看下Activity的onSaveInstanceState()方法。
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
... // 省略无关代码
}
可以看到,调用了mWindow的saveHierarchyState()方法,将返回值保存在了outState中,其中mWindow是PhoneWindow类型,其saveHierarchyState()方法如下。
@Override
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();
if (mContentParent == null) {
return outState;
}
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
mContentParent.saveHierarchyState(states);
outState.putSparseParcelableArray(VIEWS_TAG, states);
... // 省略无关代码
return outState;
}
可以看到,主要是调用了mContentParent的saveHierarchyState()方法,mContentParent是ViewGroup类型的变量,它的唯一子View是我们通过setContentView()添加进去的View。ViewGroup并没有声明saveHierarchyState()方法,saveHierarchyState()方法是在View中声明的。
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
直接调用了View的dispatchSaveInstanceState()方法
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();
if (state != null) {
container.put(mID, state);
}
}
}
主要调用了自己的onSaveInstanceState()方法获取状态信息,并将其和id值以键值对的形式存储到SparseArray中。而ViewGroup中重写了该方法,递归调用了ViewGroup作为View及其子View的dispatchSaveInstanceState()方法。
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}
从上面的代码可以看到,mWindow.saveHierarchyState()方法,以深度优先的方法遍历了整个View树,将View的状态信息以键值对的形式存储到SparseArray中,然后包装存储到Bundle中。
有保存就有恢复,View的状态恢复在Activity的onRestoreInstanceState()方法中,非常对称得调用了PhoneWindow的restoreHierarchyState()方法,进而调用了mContentParent的restoreHierarchyState()方法,按照id从SparseArray中取出保存的状态信息,并通过View的onRestoreInstanceState()方法进行恢复。
再细看下TextView的onSaveInstanceState()和onRestoreInstanceState()方法。
@Override
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 {
ss.text = mText.toString(); // ①
}
}
return ss;
}
return superState;
}
onSaveInstanceState()方法会在状态信息中保存文字、选中状态、获取焦点状态等。
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// XXX restore buffer type too, as well as lots of other stuff
if (ss.text != null) {
setText(ss.text); // ①
}
}
onRestoreInstanceState()方法对这些信息进行了相应的恢复处理。
我们自己定义的View如果需要在页面销毁和重建中保持状态信息,也应该通过这种方式进行处理。需要注意的是页面数据恢复的时机有两个,一个是在onCreate(Bundle savedInstanceState)中,一个在onRestoreInstanceState(Bundle savedInstanceState)中,onCreate的回调早于onRestoreInstanceState,而View的数据恢复是在onRestoreInstanceState中。这个比较好理解,因为我们通常在onCreate()中调用setContentView()方法,等onRestoreInstanceState()时恢复可以确保mContentParent及其子View都已经存在,可以放心进行恢复。这样,在页面恢复的过程中,无论我们在onCreate()中对TextView设置了什么文字,等到onRestoreInstanceState()时,都会被设置回之前保存的文字,这个还挺神奇的。
2.Fragment的数据恢复
上部分说了View的数据恢复在onRestoreInstanceState()中,那Android系统中有没有在onCreate()中进行数据恢复的组件呢?这个还真有,看下FragmentActivity的onCreate()代码:
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null); //①
...
}
mFragments.dispatchCreate(); // ②
}
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
可以看到,当savedInstanceState不为空时,也就是当页面是销毁后重建时,会调用mFragments当restoreAllState()方法,其中mFragments是FragmentController类型的对象,它的restoreAllState()方法只是直接调用了FragmentManager的restoreAllState()方法。
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
if (state == null) return;
FragmentManagerState fms = (FragmentManagerState)state;
if (fms.mActive == null) return;
... // 省略旋转屏幕页面重建的相关处理逻辑
// Build the full list of active fragments, instantiating them from
// their saved state.
mActive = new SparseArray<>(fms.mActive.length);
for (int i=0; i<fms.mActive.length; i++) {
FragmentState fs = fms.mActive[i];
if (fs != null) {
... // 省略部分参数处理逻辑
Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig, viewModelStore); // ①
mActive.put(f.mIndex, f);
}
}
// Build the back stack.
...// 省略
}
这个方法从FragmentManagerState中取出Fragment的状态信息(FragmentState),并以此重建了Fragment。创建方法为FragmentState的instantiate()方法。
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
... // 省略异常处理代码
Class<?> clazz = sClassMap.get(fname);
if (clazz == null) {
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment) clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(args);
}
return f;
... // 省略异常处理代码
}
这里通过反射创建了Fragment,并通过setArguments方法设置保存的数据。我们再回到该部分开头的onCreate()方法,除了恢复创建Fragment外,还通过mFragments.dispatchCreate()方法将标明为added的Fragment,设置为Created,触发Fragment的onCreate()回调。
总结下此部分内容,在FragmentActivity页面恢复导致的onCreate()中,FragmentActivity会把页面销毁时处于Active状态的Fragment给全部恢复出来。作为开发者,我们不需要再去创建新的Fragment,只需要通过FragmentManager的findFragmentByXXX()方法将Fragment取出,即可恢复使用。
3.状态信息的存储和恢复原理
在页面销毁后重建并恢复数据,在节省内存的同时又保证了用户体验的连续,这是Android一个非常好的设计。那页面的状态信息是存储在哪里了呢?在View和Fragment的状态信息保存和恢复中,可以状态信息都是存储在Parcelable类型的变量里了,这表明状态信息应该是保存在内存中了。这也比较容易理解,onSaveInstanceState()和onRestoreInstanceState()方法都是在UI线程中实现的,如果做IO存储操作,需要解决线程同步、文件读写同步等问题,麻烦且容易造成卡顿。但如果存储在内存中,进程被杀死,数据是不是就不存在了呢?看个关闭权限重启进程的例子,首先进入到设置用户名页面,将tc_android更改为tc,然后到权限设置页面关闭一个权限,导致应用进程重启,然后重新打开app,让页面恢复,发现显示的仍然是tc,而非tc_android!
对于这个问题,Android其实处理得很巧妙,状态信息确实是存储在内存中,只不过不是在应用所在进程的内存中,而是在ActivityManagerService所在的进程中,看下PendingTransactionActions.StopInfo类,
public static class StopInfo implements Runnable {
private Bundle mState;
private PersistableBundle mPersistentState;
@Override
public void run() {
// Tell activity manager we have been stopped.
try {
ActivityManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
// Dump statistics about bundle to help developers debug
final LogWriter writer = new LogWriter(Log.WARN, TAG);
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
pw.println("Bundle stats:");
Bundle.dumpStats(pw, mState);
pw.println("PersistableBundle stats:");
Bundle.dumpStats(pw, mPersistentState);
if (ex instanceof TransactionTooLargeException
&& mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
return;
}
throw ex.rethrowFromSystemServer();
}
}
}
主要通过Binder跨进程调用了ActivityManagerService的activityStopped()方法,其中mState参数就是Activity的onSaveInstanceState()方法的参数,看下ActivityManagerService的activityStopped()方法。
@Override
public final void activityStopped(IBinder token, Bundle icicle,
PersistableBundle persistentState, CharSequence description) {
synchronized (this) {
final ActivityRecord r = ActivityRecord.isInStackLocked(token);
if (r != null) {
r.activityStoppedLocked(icicle, persistentState, description);
}
}
}
调用了ActivityRecord的activityStoppedLocked()方法,
final void activityStoppedLocked(Bundle newIcicle, PersistableBundle newPersistentState,
CharSequence description) {
final ActivityStack stack = getStack();
if (newPersistentState != null) {
persistentState = newPersistentState;
service.notifyTaskPersisterLocked(task, false);
}
if (newIcicle != null) {
// If icicle is null, this is happening due to a timeout, so we haven't really saved
// the state.
icicle = newIcicle;
haveState = true;
launchCount = 0;
updateTaskDescription(description);
}
... // 省略
}
可以看到,将newIcicle变量保存到了ActivityRecord的icicle变量中。这样ActivityManagerService维护着ActivityRecord列表,ActivityRecord维护着Bundle 变量,即使应用所在的进程被杀死了,页面的状态信息也还会在内存中存储,如果页面重建,就可以把数据从ActivityManagerService进程传递回应用进程,然后进行恢复,保证了效率和流程的简化。
4.TransactionTooLargeException
当然,所有的方案一定有一定的弊端。Binder进行数据传输,一个让人头痛的问题是,数据量过大时,会出现TransactionTooLargeException。这个在上面的StopInfo类中的run()方法中也可以看到。
try {
ActivityManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
// Dump statistics about bundle to help developers debug
final LogWriter writer = new LogWriter(Log.WARN, TAG);
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
pw.println("Bundle stats:");
Bundle.dumpStats(pw, mState);
pw.println("PersistableBundle stats:");
Bundle.dumpStats(pw, mPersistentState);
if (ex instanceof TransactionTooLargeException
&& mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
return;
}
throw ex.rethrowFromSystemServer();
}
如果跨进程调用出现异常,且异常类型为TransactionTooLargeException,并且当应用的targetSdkVersion小于24,会丢弃该异常。否则,会抛出该异常。
对于很多应用而言,列表数据也要进行保持,如果列表数据量过大,且保存到Bundle中,那么在targetSdkVersion大于等于24时,就有可能因为TransactionTooLargeException而导致崩溃,当然targetSdkVersion小于24时,也会导致数据保持失败的问题。那应该如何处理呢?
当前的方案是通过静态变量来实现的,比如FeedListActivity中声明一个static的HashMap<String, ArrayList> sMap变量,每个FeedListActivity都存储一个特定的key值,比如页面正常启动时的System.naoTime(),然后在onSaveInstanceState()方法中,保持这个key值,同时把列表的值存入到sMap中。当onDestroy()调用时,如果isFinishing()为true,则从sMap中将该列表清除,防止内存泄漏。如果isFinishing()为false,则说明是系统销毁页面,还有可能重建,就不做处理。等页面重建时,从bundle中取出保持的key值,再通过key从sMap中取出列表值,进行显示,同时从sMap中清除该值,防止内存泄漏。大致效果如下,冒昧得直接使用微信做效果演示:
可以看到页面销毁前显示的是泓洋的公众号,经过不保留活动,重新打开微信,显示的仍然是泓洋的公众号。
但这种方法有个问题,就是如果是关闭应用权限导致的页面销毁和重建,进程也销毁重建了,静态变量的值也没有了,这个怎么办呢?仍然看下微信的做法。
可以看到,微信一开始显示钛师傅的公众号,然后关闭权限后重新打开,会启动欢迎页,之后虽然跳转到订阅号消息页,但是回到了顶部,且只展示第一页的数据,因此此种情况下,微信应该也只是放弃了数据恢复,重新请求了第一页的数据进行显示。
不知道有没有其他的处理方法?