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的數據恢復

先看一個未做任何數據恢復的頁面銷燬和重建的效果,
TextView自動恢復數據
在這個例子中並沒有保存和恢復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中清除該值,防止內存泄漏。大致效果如下,冒昧得直接使用微信做效果演示:
在這裏插入圖片描述
可以看到頁面銷燬前顯示的是泓洋的公衆號,經過不保留活動,重新打開微信,顯示的仍然是泓洋的公衆號。
但這種方法有個問題,就是如果是關閉應用權限導致的頁面銷燬和重建,進程也銷燬重建了,怎麼辦呢?仍然看下微信的做法。
在這裏插入圖片描述
可以看到,微信一開始顯示鈦師傅的公衆號,然後關閉權限後重新打開,會啓動歡迎頁,之後雖然跳轉到訂閱號消息頁,但是回到了頂部,且只展示第一頁的數據,因此此種情況下,微信其實是沒有去進行“完整”的數據恢復的。
不知道有沒有其他的處理方法?

三、參考文章

  1. Activity的非正常銷燬
  2. Android 關於Activity的銷燬和重建
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章