怎么處理SaveState

前文鏈接:是時候使用SaveState了

要使前文介紹的5.0新機制生效,應用需要設計為多Task結構,而且要處理好頁面的SaveState相關邏輯。

這里先討論SaveState的相關邏輯,再介紹怎么設計應用的Task結構。

處理SaveState的四個方面

在之前演示代碼的基礎上,我們稍做改動:代碼鏈接

  1. ActivityTwo包含一個文字列表
  2. 文字列表中每一項的前綴是由啟動的Intent的Extra決定
  3. 文字列表中每一項的后綴是由創建頁面的時間戳決定

如果我們不處理SaveState,則在恢復ActivityTwo時,列表中每一項的后綴會發生變化,如果處理SaveState,則能保證返回時頁面和創建時一樣。在代碼中通過修改ActivityTwo#ENABLE_SAVE_STATE可以切換兩種狀態。

創建時的頁面:

create.png
創建時的頁面
創建時的頁面

不處理SaveState恢復后的頁面

restore.png

處理SaveState恢復后的頁面

create.png

SaveState的處理應該包括4個部分

  1. 保存數據
  2. 恢復數據
  3. 處理View
  4. 處理Fragment

第一:保存數據

這一步相對簡單,只要把頁面中的數據變量保存到outState中

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (ENABLE_SAVE_STATE) {
            outState.putString(KEY_PREFIX, mPrefix);
            outState.putString(KEY_SUFFIX, mSuffix);
        }
    }

實際項目中,可能由Intent中傳入頁面id,再通過網絡接口獲取頁面詳情。這時,也需要將網絡返回的數據也保存在outState中。

第二:恢復數據

恢復數據時,需要考慮onCreate的正常處理邏輯

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two);
        mListView = (ListView) findViewById(R.id.lv);

        if (ENABLE_SAVE_STATE && savedInstanceState != null) {
            // 恢復流程
            mPrefix = savedInstanceState.getString(KEY_PREFIX);
            mSuffix = savedInstanceState.getString(KEY_SUFFIX);
            mListView.setAdapter(new TwoAdapter(mPrefix, mSuffix));
        } else {
            // 初始化流程
            mPrefix = getIntent().getStringExtra(KEY_PREFIX);
            mSuffix = String.valueOf(SystemClock.uptimeMillis());
            mListView.setAdapter(new TwoAdapter(mPrefix, mSuffix));
        }
    }

實際項目中,初始化網絡請求應放初始化流程中,而類似mListView.setAdapter的View更新邏輯應該在網絡請求的回調中處理。

第三:處理View

上面的例子中,除了ListView的內容,我們還應注意到ListView的位置。不管我們是否處理SaveState,ListView都會恢復到離開時的位置。這是因為ListView的基類AbsListView實現了Save和Restore,下面是節選的一小段源碼。

// AbsListView

    @Override
    public Parcelable onSaveInstanceState() {
        ......

        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);

        if (mPendingSync != null) {
            // Just keep what we last restored.
            ss.selectedId = mPendingSync.selectedId;
            ss.firstId = mPendingSync.firstId;
            ss.viewTop = mPendingSync.viewTop;
            ss.position = mPendingSync.position;
            ss.height = mPendingSync.height;
            ss.filter = mPendingSync.filter;
            ss.inActionMode = mPendingSync.inActionMode;
            ss.checkedItemCount = mPendingSync.checkedItemCount;
            ss.checkState = mPendingSync.checkState;
            ss.checkIdState = mPendingSync.checkIdState;
            return ss;
        }
        .......
     }

除了AbsListView,還有很多View實現了Save和Restore的機制,包括ViewPager當前的位置,EditText和TextView中的文本等。

關于View的處理主要注意以下幾點:

  • 系統以一個Map來保存View的狀態,以id為key
  • 沒有id的View是不會被保存狀態的
  • 如果id重復,則View的狀態會被覆蓋
  • 自定義的View,要注意處理View的Save和Restore
  • 如果View的恢復有特殊處理邏輯,需要充分考慮View更新的時機,注意onCreate、onRestoreInstanceState和網絡請求回調的時序問題。

第三:處理Fragment

關于Fragment的處理,和Activity的處理基本一致。只需要注意一個特殊的地方

很多項目都會只在Manifest中聲明一個FragmentContainerActivity的模式,各個頁面通過Fragment實現。然后啟動FragmentContainerActivity,通過Extra傳遞要展現的Fragment類名和Argument。FragmentContainerActivity#onCreate中創建并添加Fragment。

由于Activity的恢復機制會自動重建Fragment,所以在恢復時不要再重復創建添加Fragment。當然也不要新創建,并通過replace替換老的,這樣會使Fragment的恢復機制失效。

這部的例子演示,可使用如下步驟復現:

  1. 修改ActivityThree#ENABLE_SAVE_STATE進行切換
  2. 啟動Three
  3. 點擊Three的文字可返回到One
  4. 在One中消耗內存,直到logcat中出現ActivityThree#onDestroy
  5. 再次啟動Three

合理區分Task

Android關于Task的定義十分復雜,而且很多特性在普通應用開發中根本用不到。而且在5.0之后,又引入了android:documentLaunchMode讓它變得更加復雜了。

關于Task,還需要另一篇專題來討論。這里只舉兩類多Task的例子。

使用singleInstance

給一些特定的頁面設置singleInstance,可使他們處于單獨的Task中。這類頁面一般和其他頁面沒有很強的邏輯關系,同時又是消耗資源的大戶。

適用場景:

  • 視頻播放或錄制頁(視頻的編碼解碼,視頻上的絢麗彈幕和禮物等)
  • 應用介紹頁(包含很多大圖和動畫)
  • 應用內用于現實外部網頁的單WebView頁
  • ViewPager實現的大圖圖集頁

使用taskAffinity

給一組完成某一功能的Activity設置相同的taskAffinity,就可使用FLAG_ACTIVITY_NEW_TASK啟動新Task。使用taskAffinity啟動的新Task一般都包括多個Activity,而且和別的參數相互影響,請謹慎使用。

適用場景:

  • 注冊、登陸、找回密碼等頁面(這類頁面一般占用的資源并不多,不一定要設計在獨立的Task中)
  • 自定義的選擇相冊,照相機,圖片預覽,圖片裁剪等頁面

關于多Task的補充

由于不同的Task之間不能通過StartActivityForResult傳遞結果,可能需要EventBus或其他機制在Task之間傳遞信息。

默認的每個Task都會出現在最近應用中。上述的這些情況中,都可使用android:excludeFromRecents避免這些Task在最近應用中出現

后面單獨補充了一節進程被殺的介紹,因為它很容易和Task中Activity銷毀混淆

Task中Activity銷毀 vs 進程被殺

我們先看下再ActivityManagerService中進程Process和Task的關系

Task&Process.gif
  1. ActivityManagerService通過一個列表mHistory來管理所有ActivityRecord
  2. 相同TaskRecord中的ActivityRecord在列表中處于連續位置
  3. 同一個TaskRecord中的ActivityRecord可能處于不同的ProcessRecord

由于以下兩個因素,使得很難找到Task和進程之間關聯的清晰線索。

  • 同一Task中的Activity可能屬于不同進程
  • 進程中不僅有Activity,還有Service和BroadcastReceiver

先看Task中Activity銷毀

  • 處理的問題:一個進程內部,前后臺Task的資源協調
  • 觸發時機:進程使用的內存接近上限時(根據機型不同,大約在64M~256M之間)

再看進程被殺

  • 處理的問題:系統控制中,多進程之間的資源協調
  • 觸發時機:整個系統使用的內存接近機器配置的內存上限時

我們以一個簡化的例子討論兩者的關系。假設:

  • 單進程最大可使用內存為100M,進程使用內存超過90M時會觸發后臺Task銷毀。
  • 系統總可用內存為200M,系統使用內存超過190M時會觸發后臺進程被殺。
  • 系統中運行著3個進程,他們在三個Task中的分布和內存使用如下
  • Task1處于前臺運行
Momory Usage Process1 Process2 Process 3
Task1 60M 20M -
Task2 20M 20M -
Task3 - - 40M

如果T1 P1部分消耗的內存由60M上升到75M,由于P1的總內存消耗達到95M,所以會導致P1 T2中的Activity被銷毀。

如果T1 P2部分消耗的內存由20M上升到50M,會導致系統總內存消耗達到190M。此時三個Process中,P1和P2和前臺Task關聯,優先級較高,所以系統會殺掉P3。

這個例子,只是對兩者關系的一個簡要說明。系統對進程的實際處理方式要復雜得多!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容