Android Activity 重建之狀態儲存與恢復

钰琪發表於2024-08-14

為什麼要重建 Activity ?

Activity 負責使用者介面,提供檢視顯示和接收使用者輸入。使用者介面的顯示和互動依賴於系統的配置,當系統的配置發生了變化,就需要修改使用者介面。Android 預設透過重建 Activity,用同型別的新的 Activity 物件替換舊的 Activity 物件的方式來將系統配置的最新狀態應用到使用者介面上。

會引發系統重建 Activity 的系統配置有很多,比如螢幕方向、系統語言、字型等。當這些配置發生變化,系統就進入重建 Activity 的流程。

除了系統配置的變更,系統回收後臺程序導致 Activity 銷燬,當使用者重新回到後臺的應用的時候,也會觸發重建流程。

要注意,這個銷燬的過程是靜默的、“未經使用者允許的”,是系統自主行為。嚴格來說,Android 應該只在記憶體不足時回收後臺程序,但是實際上生產裝載 Android 系統的廠商有著自己的考量。比如長時間在後臺執行的 App ,幾小時甚至幾天都沒有再開啟它,他們就會考慮讓系統回收其程序;又或者使用者希望系統的執行地省電,那麼後臺的程式則會更容易被回收。

相比之下,配置變更引起的重建是一個確定的過程,配置變更就立刻重建;而回收後臺程序之後的重建則取決於使用者是否重新回到後臺應用,以及裝置廠商想不想讓系統重建。如果他們不想,就會修改 Android 的原始碼,執行他們自己的策略。

狀態是什麼?

狀態指的是 Acticity 內會可能會變化的細節,比如介面上顯示的文字、圖片,使用者在輸入框輸入的文字、勾選的選擇框,使用者新增或刪除的資料,也包括無需使用者參與,程式自主發生的改變。

當 Activity 發生了變化,而之後系統又重建了 Activity,新的替換舊的,一切回到我們初始化 Activity 時的狀態,所有的改變都會丟失。

我們想要將舊的 Activity 發生的變化應用到新的 Activity 上,就需要儲存一個資料來承載發生的變化並在之後使用該資料的方法。Android 的工程師也想到了這些,所以提供了這樣的功能。

如何儲存與恢復?

onSaveInstanceState(Bundle)onRetainNonConfigurationInstance() 兩個方法就是用來儲存 Activity 狀態的,還有兩個與前兩者一一對應的 onRestoreInstanceState(Bundle)getLastNonConfigurationInstance() 方法是獲取狀態的。

既然準了備兩個組合,其功能想來是有差異的,雖然都有在重建 Activity 時儲存與傳遞資料的功能,但是它們應對不同的場景。

一個是應對 Activity 程序被回收後在新的程序重建 Activity 的場景,另一個是應對系統狀態變化後在同一程序重建 Activity 的場景。但是它們功能又有重合的地方,前者同時也具備後者的功能,但是我們應該讓它們各司其職。

onSaveInstanceState()onRestoreInstanceState() 是如何工作的?

在 Activity 的重建過程中,舊的 Activity 物件將被銷燬之際,也即在呼叫其生命週期方法 onStop()之後 與 onDestory() 之前,系統呼叫其 onSaveInstanceState() 方法,將 Bundle 型別的資料容器傳遞進去,該 Activity 將變化的狀態存放到該容器,隨後該 Activity 被銷燬,該過程即“儲存狀態”;

只要 Activity 是“被動銷燬”或“有被動銷燬的可能”,系統就一定會儲存狀態,以便之後重建 Activity 時恢復狀態,它們分別對應著“系統配置的變化”與“系統回收後臺程序”對 Activity 的影響。回到啟動器桌面、跳轉到當前應用或其他應用的 Activity,都會使當前 Activity 進入後臺,有被回收的可能,從而觸發該方法的呼叫;而使用者主動終止 Activity 程序導致 Activity 被銷燬,明確不會重建 Activity,系統則不會呼叫此方法。

之後在合適的時機,系統重建 Activity,新的 Activity 物件被建立,系統
會呼叫其 onRestoreInstanceState() 方法,其呼叫時機是在 onStart()onResume() 之間,將 Bundle 型別的資料容器傳遞進去,該 Activity 從中取出舊的 Activity 狀態應用於自身,該過程即“恢復狀態”。

onRestoreInstanceState() 只在 Activity 被重建時才會呼叫,和 onSaveInstanceState() 是成對地被呼叫。而當使用者主動結束 Activity 程序,則不會重建 Activity,自然也無法呼叫。又或者 Activity 是正常新建的,此方法也不執行。

這一組方法是為跨程序邊界的資料傳遞所準備的功能,這兩者都接收一個 Bundle 型別的引數,它是一個存放資料的容器。因為程序間的記憶體空間是獨立的,不能直接傳遞資料引用,故而所有存放到該容器的資料都必須是可被序列化的,以便在程序間傳遞資料。

另外,即使系統配置更改所觸發的 Activity 重建過程所涉及的兩個 Activities 物件在同一程序,系統也會呼叫這兩個方法,但資料的傳遞方式不是引用傳值,Bundle 還是被透過序列化的方式傳遞到主執行緒,再由主執行緒傳遞回來,再經歷反序列化,轉換為 Bundle 物件。

需要格外注意,使用 Bundle 傳遞資料有大小資料限制,上限為 1Mb,它是為簡化程序間通訊進行設計的,不是為了複製大量資料。這意味著我們需要分析 Activity 的狀態,僅傳遞恢復 Activity 狀態的必要資料,而不是包含所有狀態的資料,非必要資料可以透過檔案儲存或資料庫的方式傳遞。

比如說檢視展示一個列表,有很多項,其資料由一個 List 物件維護,使用者當前滾動到了列表檢視的特定位置。

此時若應用進入後臺,並被回收,我們不能將列表資料和定位資訊都儲存起來。系統回收 Activity 程序的目的往往是為了釋放記憶體,我們卻留存一份可能會佔用比較大記憶體的資料,就違背了系統的意圖,還很有可能超出傳遞資料的上限。

恢復檢視的必要資料是列表的定位,我們僅儲存並傳遞這個定位即可。至於列表的資料,儲存到資料庫或者序列化後儲存到檔案中。當呼叫新的 Activity 的 onRestoreInstanceState() 方法時,我們讀取到了定位資料,然後在從檔案中讀取快取的序列化資料,反序列化後渲染到列表檢視,再根據定位資訊恢復列表檢視的位置。

儘量不要在 onCreate(Bundle) 生命週期方法中恢復資料

系統在重建 Activity 過程中,在呼叫 onCreate() 時,也會傳遞 Bundle 資料容器,此時 Bundle 有值,是先前“儲存狀態”時定義的;但是如果 Activity 不是重建的,而是正常新建的,那這個值就是 null,如果不經驗證就使用,程式會崩潰。

onRetainNonConfigurationInstance()getLastNonConfigurationInstance() 如何工作?

它們是為同一程序中的 Activity 重建時傳遞資料所準備的功能,可以傳遞任何資料型別,是引用傳值。

系統呼叫舊的 Activity 的 onRetainNonConfigurationInstance()方法,該方法返回一個任意資料型別,系統儲存它;新的 Activity 呼叫 getLastNonConfigurationInstance() 獲取儲存的資料,並做型別轉換以取用資料。

onRetainNonConfigurationInstance() 的呼叫事件在 onSaveInstanceState() 之後,在 onDestory() 之前; getLastNonConfigurationInstance() 在 Activity 的任何生命週期方法中都能訪問。

透過這種方式,我們可以傳遞承載 Activity 所有的狀態的資料,並將該資料應用到重建的 Activity 中。

這一組方法永遠是 Activity 在前臺時被呼叫,與前面所提到的那組方法唯一重疊的地方就是在受到系統引數變化的影響時,系統會呼叫 onSaveInstanceState()onRetainNonConfigurationInstance() 兩個方法來“儲存狀態”,但是 另外的兩個恢復狀態的方法,只有 onRestoreInstanceState() 由系統呼叫,而 getLastNonConfigurationInstance() 的呼叫時機掌握在我們手裡。

注意 Manifest 宣告檔案中宣告 activity 的 android:configChanges 屬性對 Activity 重建過程的影響

configChanges 對應著系統屬性的變化,宣告該屬性意味著允許當前 Activity 響應指定的系統屬性變化,而不是在這些屬性變化時重建 Activity。該屬性允許多個值,使用 | 分隔。

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize"
    android:label="@string/app_name">

上面的生命告知系統,該 Activity 希望響應螢幕方向和螢幕大小的變化,這會使系統在屬性變化時呼叫 Activity 的 onConfigurationChanged() 方法,隨後 Activity 呼叫關聯檢視的 onConfigurationChanged() 方法。
這些方法接收 Configuration 型別的物件,它承載著所有代表系統最新配置的資料,使用這些資料對系統屬性變更作出響應。

怎麼組合使用上面的兩組方法?

使用 onSaveInstanceState()onRestoreInstanceState() 恢復因系統回收 Activity 程序導致重建的 Activity 的狀態。

使用 onRetainNonConfigurationInstance()getLastNonConfigurationInstance() 恢復因系統狀態變化後在同一程序重建的 Activity 的狀態。

維護一個 isRestored 欄位表示是否已恢復 Activity 狀態。

onCreate() 方法中呼叫 getLastNonConfigurationInstance() 取得資料,如果取得了資料,說明 Activity 一定是在當前程序中重建的,然後為 Activity 恢復狀態,設定為 isRestored 為 true;如果資料為空說明是在新程序中重建的,什麼也不做。

onRestoreInstanceState() 中判斷 isRestored,若為 true,說明 Activity 是因為系統配置變化被重建的,已經使用了 getLastNonConfigurationInstance() 的資料恢復了狀態;否則就是 Activity 程序被回收後在新程序重建的,需要在 onRestoreInstanceState() 方法中恢復。

相關文章