Activity 知識梳理(3) Activity狀態儲存和恢復

澤毛發表於2017-12-21

一、概述

在開發過程中,不可避免地會遇到Activity被回收的場景, Activity被回收有兩種情況:主動和被動。

  • Activity是被主動回收時,例如按下了Back鍵,那麼這時候是無法恢復的,因為系統認為你已經不再需要它了。
  • 在被動回收的情況下,雖然這個Activity的例項已經被銷燬了,但是系統在新建一個Activity例項的時候,會帶上先前被回收Activity的資訊,這些資訊是被儲存在Bundle的鍵值對,這裡面有些是系統幫我們讀寫的,例如Activity當中View的狀態,這部分的資訊並不需要我們擔心。(為了系統能幫我們恢復狀態,必須要為每個View都指定一個id),如果我們希望儲存更多臨時的資訊,而這些資訊又沒有必要寫入到持久化的儲存當中,這時候我們應該重寫onSaveInstanceState方法,在該回撥當中會傳入一個Bundle物件,只需要把儲存的資訊放到裡面,之後再在onRestoreInstanceStateonCreate方法中把它讀取出來就可以了,這裡面有兩點需要注意的:
  • onRestoreInstance只有在Bundle不為空時才會回撥,而在onCreate方法當中是通過判空操作來判斷是否有需要恢復的狀態的。
  • 在重寫onRestoreInstanceState方法時,應當先呼叫super方法,這樣由系統負責儲存的部分才能夠恢復。

二、疑問

關於狀態的儲存和恢復,實現方法很簡單,我們主要了解一下它內部的原理,主要有這麼幾個問題:

2.1 對於View的狀態,是怎麼做到自動恢復的

由於文件要求我們必須呼叫super方法,那麼就可以知道儲存View狀態的程式碼入口必然在Activity當中,我們看下Activity的預設實現:

<!-- Activity.java -->
protected void onSaveInstanceState(Bundle outState){
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState);
}

//看到mWindow,自然就想到了PhoneWindow.java
<!-- PhoneWindow.java -->
public Bundle saveHierarchyState() {
     mContentParent.saveHierarchyState(states); //這裡是儲存View狀態的地方。  
     outState.putInt(FOCUSED_ID_TAG, focusedView.getId()); //儲存Focus。
     outState.putSparseParcelableArray(PANELS_TAG, panelStates); //儲存Panel狀態。
}

<!-- View.java -->
public void saveHierachyState(SparseArray<Parcelable> container) {
   dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstceState(Parcelable> container) {
    Parcelable state = onSaveInstanceState();
    container.put(mID, state);
}
複製程式碼

整個儲存View狀態的流程如下:

  • 呼叫ActivityonSaveInstanceState方法
  • 該方法又呼叫mWindow.saveHierarchyState,把返回的結果儲存到WINDOW_HIERARCHY_TAG這個Key對應的Value
  • mWindow的實現類PhoneWindow當中:
  • 呼叫根佈局的saveHierarchyState方法,這裡面會從根佈局按樹形結構遍歷,呼叫每個ViewGroup/ViewonSaveInstanceState
  • 儲存FoucusView

同時,我們也可以得到結論,儲存的前提有兩個

  • View的子類必須實現了onSaveInstanceState
  • 它必須要有一個ID,這個ID作為Bundlekey,這也為我們實現自定義 View 時,需要儲存狀態提供了思路。

2.2 onSaveInstanceState呼叫時機

下面看下第二個問題,有了前面分析Activity生命週期的經驗,我們直接在ActivityThread中找相關的程式碼:

<!-- ActivityThread.java -->
//在ActivityThread中,一共有3處呼叫了 saveInstanceState

private void handleRelaunchActivity(ActivityClientRecord tmp) {
    performPauseActivity(r.token, false, r.isPreHoneycomb());
    if (r.state == null && !r.stopped && !r.isPreHoneycomb) {
        //儲存
    }
}

final Bundle performPauseActivity(ActivityRecord r, boolean finished, boolean saveState) {
   if (!r.activity.mFinished && saveState) { //這裡saveState的條件是!r.isPreHoneycomb。
       //儲存
   }
   mInstrumentation.callActivityOnPause(r.activity);
}

//這個函式呼叫的地方有:
1.private void handleWindowVisibility(IBinder token, boolean show) //這裡saveState是false
2.public void performStopActivity(IBinder token, boolean saveState) //LocalActivityManager調過來的也是false
3.private void handleStopActivity(IBinder token, boolean show, int configChanges) //這裡是true,通過 ActivityManagerSupervisor 調過來的

private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShow, boolean saveState) {
    if (r.state == null) {
        //儲存  
    }
    r.activity.performStop();
}
複製程式碼

看完這三個呼叫的地方,問題又解決了,結論就是:

  • 如果是honeycomb之後的版本,那麼在performPauseActivity時是不會儲存的,而對於honeycomb之前的版本,會在回撥onPause()之前儲存。
  • 如果在performPauseActivity時沒有儲存,那麼在執行performStopActivityInnerr.state為空並且是從 handleStopActivity過來的,那麼會在onPause()onStop()之間儲存狀態,其它情況下不會儲存狀態。
  • ReLaunchAcitivity時,也會儲存狀態。

2.3 onRestoreInstanceState呼叫時機

Activity的生命週期解析中分析onStart()方法的時候我們已經知道,它是在onStart()onResume()之間被呼叫的。

相關文章