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參數不爲空。如果想展示回被銷燬前狀態,就需要利用這個變量。舉個例子,設置用戶信息頁面UserInfoActivity裏有更改用戶頭像的功能,選擇圖片返回到該頁面時會把選擇後的圖片路徑存儲在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類型,看下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的方法在Activity的setContentView()中
public void setContentView(int layoutResID) {
...
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
...
mContentParent.addView(view, params);
...
}
可以看到,mContentParent的子View就是我們setContentView()添加進去的View。
看下ViewGroup的saveHierarchyState()方法,實際上是View的saveHierarchyState()方法。
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
直接調用了自己的dispatchSaveInstanceState()方法,其中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);
}
}
}
主要調用了View自己的onSaveInstanceState()方法,並將返回值(View的狀態信息)和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();
// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp; // ①
} else {
ss.text = mText.toString(); // ①
}
}
if (hasSelection) { // ②
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
}
if (isFocused() && start >= 0 && end >= 0) {
ss.frozenWithFocus = true; // ③
}
ss.error = getError(); // ④
if (mEditor != null) {
ss.editorState = mEditor.saveInstanceState(); // ⑤
}
return ss;
}
return superState;
}
保存了TextView的文字、選中狀態、獲取焦點狀態等信息。
@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); // ①
}
if (ss.selStart >= 0 && ss.selEnd >= 0) {
if (mSpannable != null) {
int len = mText.length();
if (ss.selStart > len || ss.selEnd > len) {
String restored = "";
if (ss.text != null) {
restored = "(restored) ";
}
} else {
Selection.setSelection(mSpannable, ss.selStart, ss.selEnd); // ②
if (ss.frozenWithFocus) {
createEditorIfNeeded();
mEditor.mFrozenWithFocus = true; // ③
}
}
}
}
if (ss.error != null) {
final CharSequence error = ss.error;
// Display the error later, after the first layout pass
post(new Runnable() {
public void run() {
if (mEditor == null || !mEditor.mErrorWasChanged) {
setError(error); // ④
}
}
});
}
if (ss.editorState != null) {
createEditorIfNeeded();
mEditor.restoreInstanceState(ss.editorState); // ⑤
}
}
onRestoreInstanceState()中對這些信息進行了相應的恢復處理。
Android這種讓基礎View自己進行數據保存和恢復,符合高內聚低耦合的設計原理,簡化了開發。我們自己定義的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()方法。
public void restoreAllState(Parcelable state, List<Fragment> nonConfigList) {
mHost.mFragmentManager.restoreAllState(state,
new FragmentManagerNonConfig(nonConfigList, null, null));
}
其中mHost其實就是當前的FragmentActivity,這裏就是直接調用了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);
// Now that the fragment is instantiated (or came from being
// retained above), clear mInstance in case we end up re-restoring
// from this FragmentState again.
fs.mInstance = null;
}
}
// Build the list of currently added fragments.
mAdded.clear();
if (fms.mAdded != null) {
for (int i=0; i<fms.mAdded.length; i++) {
Fragment f = mActive.get(fms.mAdded[i]);
...//省略異常處理代碼
f.mAdded = true;
...//省略異常處理代碼
synchronized (mAdded) {
mAdded.add(f); // ②
}
}
}
// Build the back stack.
...// 省略
}
這個函數主要根據FragmentState創建了Fragment,並放到FragmentManager的mActive和mAdded兩個變量中。看下FragmentState的instantiate()方法:
public Fragment instantiate(FragmentHostCallback host, FragmentContainer container,
Fragment parent, FragmentManagerNonConfig childNonConfig,
ViewModelStore viewModelStore) {
if (mInstance == null) {
...
if (container != null) {
mInstance = container.instantiate(context, mClassName, mArguments);
} else {
mInstance = Fragment.instantiate(context, mClassName, mArguments);
}
...
}
return mInstance;
}
此處中container爲null,所以直接調用了Fragment的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。此時我們回到該部分開頭的onCreate()方法,除了恢復創建Fragment外,mFragments.dispatchCreate()這裏,將標明爲added的Fragment,設置爲Created,觸發Fragment的onCreate()回調。
總結下此部分內容,在FragmentActivity頁面恢復導致的onCreate()中,FragmentActivity會把頁面銷燬時處於Active狀態的Fragment給全部恢復出來。作爲開發者,我們不需要再去創建新的Fragment,只需要通過FragmentManager的findFragmentXXX()方法將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 ActivityClientRecord mActivity;
private Bundle mState;
private PersistableBundle mPersistentState;
private CharSequence mDescription;
public void setActivity(ActivityClientRecord activity) {
mActivity = activity;
}
public void setState(Bundle state) {
mState = state;
}
@Override
public void run() {
// Tell activity manager we have been stopped.
try {
// TODO(lifecycler): Use interface callback instead of AMS.
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變量中!這樣即使應用所在的進程被kill了,頁面的狀態信息也還會在內存中存儲,如果頁面重建,就可以從ActivityManagerService進程傳遞迴應用進程,然後進行恢復,保證了效率和流程的簡化。
4.TransactionTooLargeException
當然,所有的方案一定有一定的弊端。Binder進行數據傳輸,一個讓人頭痛的問題是,數據量過大時,會出現TransactionTooLargeException。這個在StopInfo類中的run()方法中也可以看到。
try {
// TODO(lifecycler): Use interface callback instead of AMS.
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();
}
如果IPC調用出現異常,且異常類型爲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中清除該值,防止內存泄漏。大致效果如下,冒昧得直接使用微信做效果演示:
可以看到頁面銷燬前顯示的是泓洋的公衆號,經過不保留活動,重新打開微信,顯示的仍然是泓洋的公衆號。
但這種方法有個問題,就是如果是關閉應用權限導致的頁面銷燬和重建,進程也銷燬重建了,怎麼辦呢?仍然看下微信的做法。
可以看到,微信一開始顯示鈦師傅的公衆號,然後關閉權限後重新打開,會啓動歡迎頁,之後雖然跳轉到訂閱號消息頁,但是回到了頂部,且只展示第一頁的數據,因此此種情況下,微信其實是沒有去進行“完整”的數據恢復的。
不知道有沒有其他的處理方法?