怎么處理SaveState

前文鏈接:是時(shí)候使用SaveState了

要使前文介紹的5.0新機(jī)制生效,應(yīng)用需要設(shè)計(jì)為多Task結(jié)構(gòu),而且要處理好頁(yè)面的SaveState相關(guān)邏輯。

這里先討論SaveState的相關(guān)邏輯,再介紹怎么設(shè)計(jì)應(yīng)用的Task結(jié)構(gòu)。

處理SaveState的四個(gè)方面

在之前演示代碼的基礎(chǔ)上,我們稍做改動(dòng):代碼鏈接

  1. ActivityTwo包含一個(gè)文字列表
  2. 文字列表中每一項(xiàng)的前綴是由啟動(dòng)的Intent的Extra決定
  3. 文字列表中每一項(xiàng)的后綴是由創(chuàng)建頁(yè)面的時(shí)間戳決定

如果我們不處理SaveState,則在恢復(fù)ActivityTwo時(shí),列表中每一項(xiàng)的后綴會(huì)發(fā)生變化,如果處理SaveState,則能保證返回時(shí)頁(yè)面和創(chuàng)建時(shí)一樣。在代碼中通過(guò)修改ActivityTwo#ENABLE_SAVE_STATE可以切換兩種狀態(tài)。

創(chuàng)建時(shí)的頁(yè)面:

create.png
創(chuàng)建時(shí)的頁(yè)面
創(chuàng)建時(shí)的頁(yè)面

不處理SaveState恢復(fù)后的頁(yè)面

restore.png

處理SaveState恢復(fù)后的頁(yè)面

create.png

SaveState的處理應(yīng)該包括4個(gè)部分

  1. 保存數(shù)據(jù)
  2. 恢復(fù)數(shù)據(jù)
  3. 處理View
  4. 處理Fragment

第一:保存數(shù)據(jù)

這一步相對(duì)簡(jiǎn)單,只要把頁(yè)面中的數(shù)據(jù)變量保存到outState中

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

實(shí)際項(xiàng)目中,可能由Intent中傳入頁(yè)面id,再通過(guò)網(wǎng)絡(luò)接口獲取頁(yè)面詳情。這時(shí),也需要將網(wǎng)絡(luò)返回的數(shù)據(jù)也保存在outState中。

第二:恢復(fù)數(shù)據(jù)

恢復(fù)數(shù)據(jù)時(shí),需要考慮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) {
            // 恢復(fù)流程
            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));
        }
    }

實(shí)際項(xiàng)目中,初始化網(wǎng)絡(luò)請(qǐng)求應(yīng)放初始化流程中,而類似mListView.setAdapter的View更新邏輯應(yīng)該在網(wǎng)絡(luò)請(qǐng)求的回調(diào)中處理。

第三:處理View

上面的例子中,除了ListView的內(nèi)容,我們還應(yīng)注意到ListView的位置。不管我們是否處理SaveState,ListView都會(huì)恢復(fù)到離開(kāi)時(shí)的位置。這是因?yàn)長(zhǎng)istView的基類AbsListView實(shí)現(xiàn)了Save和Restore,下面是節(jié)選的一小段源碼。

// 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實(shí)現(xiàn)了Save和Restore的機(jī)制,包括ViewPager當(dāng)前的位置,EditText和TextView中的文本等。

關(guān)于View的處理主要注意以下幾點(diǎn):

  • 系統(tǒng)以一個(gè)Map來(lái)保存View的狀態(tài),以id為key
  • 沒(méi)有id的View是不會(huì)被保存狀態(tài)的
  • 如果id重復(fù),則View的狀態(tài)會(huì)被覆蓋
  • 自定義的View,要注意處理View的Save和Restore
  • 如果View的恢復(fù)有特殊處理邏輯,需要充分考慮View更新的時(shí)機(jī),注意onCreate、onRestoreInstanceState和網(wǎng)絡(luò)請(qǐng)求回調(diào)的時(shí)序問(wèn)題。

第三:處理Fragment

關(guān)于Fragment的處理,和Activity的處理基本一致。只需要注意一個(gè)特殊的地方

很多項(xiàng)目都會(huì)只在Manifest中聲明一個(gè)FragmentContainerActivity的模式,各個(gè)頁(yè)面通過(guò)Fragment實(shí)現(xiàn)。然后啟動(dòng)FragmentContainerActivity,通過(guò)Extra傳遞要展現(xiàn)的Fragment類名和Argument。FragmentContainerActivity#onCreate中創(chuàng)建并添加Fragment。

由于Activity的恢復(fù)機(jī)制會(huì)自動(dòng)重建Fragment,所以在恢復(fù)時(shí)不要再重復(fù)創(chuàng)建添加Fragment。當(dāng)然也不要新創(chuàng)建,并通過(guò)replace替換老的,這樣會(huì)使Fragment的恢復(fù)機(jī)制失效。

這部的例子演示,可使用如下步驟復(fù)現(xiàn):

  1. 修改ActivityThree#ENABLE_SAVE_STATE進(jìn)行切換
  2. 啟動(dòng)Three
  3. 點(diǎn)擊Three的文字可返回到One
  4. 在One中消耗內(nèi)存,直到logcat中出現(xiàn)ActivityThree#onDestroy
  5. 再次啟動(dòng)Three

合理區(qū)分Task

Android關(guān)于Task的定義十分復(fù)雜,而且很多特性在普通應(yīng)用開(kāi)發(fā)中根本用不到。而且在5.0之后,又引入了android:documentLaunchMode讓它變得更加復(fù)雜了。

關(guān)于Task,還需要另一篇專題來(lái)討論。這里只舉兩類多Task的例子。

使用singleInstance

給一些特定的頁(yè)面設(shè)置singleInstance,可使他們處于單獨(dú)的Task中。這類頁(yè)面一般和其他頁(yè)面沒(méi)有很強(qiáng)的邏輯關(guān)系,同時(shí)又是消耗資源的大戶。

適用場(chǎng)景:

  • 視頻播放或錄制頁(yè)(視頻的編碼解碼,視頻上的絢麗彈幕和禮物等)
  • 應(yīng)用介紹頁(yè)(包含很多大圖和動(dòng)畫)
  • 應(yīng)用內(nèi)用于現(xiàn)實(shí)外部網(wǎng)頁(yè)的單WebView頁(yè)
  • ViewPager實(shí)現(xiàn)的大圖圖集頁(yè)

使用taskAffinity

給一組完成某一功能的Activity設(shè)置相同的taskAffinity,就可使用FLAG_ACTIVITY_NEW_TASK啟動(dòng)新Task。使用taskAffinity啟動(dòng)的新Task一般都包括多個(gè)Activity,而且和別的參數(shù)相互影響,請(qǐng)謹(jǐn)慎使用。

適用場(chǎng)景:

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

關(guān)于多Task的補(bǔ)充

由于不同的Task之間不能通過(guò)StartActivityForResult傳遞結(jié)果,可能需要EventBus或其他機(jī)制在Task之間傳遞信息。

默認(rèn)的每個(gè)Task都會(huì)出現(xiàn)在最近應(yīng)用中。上述的這些情況中,都可使用android:excludeFromRecents避免這些Task在最近應(yīng)用中出現(xiàn)

后面單獨(dú)補(bǔ)充了一節(jié)進(jìn)程被殺的介紹,因?yàn)樗苋菀缀?strong>Task中Activity銷毀混淆

Task中Activity銷毀 vs 進(jìn)程被殺

我們先看下再ActivityManagerService中進(jìn)程Process和Task的關(guān)系

Task&Process.gif
  1. ActivityManagerService通過(guò)一個(gè)列表mHistory來(lái)管理所有ActivityRecord
  2. 相同TaskRecord中的ActivityRecord在列表中處于連續(xù)位置
  3. 同一個(gè)TaskRecord中的ActivityRecord可能處于不同的ProcessRecord

由于以下兩個(gè)因素,使得很難找到Task和進(jìn)程之間關(guān)聯(lián)的清晰線索。

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

先看Task中Activity銷毀

  • 處理的問(wèn)題:一個(gè)進(jìn)程內(nèi)部,前后臺(tái)Task的資源協(xié)調(diào)
  • 觸發(fā)時(shí)機(jī):進(jìn)程使用的內(nèi)存接近上限時(shí)(根據(jù)機(jī)型不同,大約在64M~256M之間)

再看進(jìn)程被殺

  • 處理的問(wèn)題:系統(tǒng)控制中,多進(jìn)程之間的資源協(xié)調(diào)
  • 觸發(fā)時(shí)機(jī):整個(gè)系統(tǒng)使用的內(nèi)存接近機(jī)器配置的內(nèi)存上限時(shí)

我們以一個(gè)簡(jiǎn)化的例子討論兩者的關(guān)系。假設(shè):

  • 單進(jìn)程最大可使用內(nèi)存為100M,進(jìn)程使用內(nèi)存超過(guò)90M時(shí)會(huì)觸發(fā)后臺(tái)Task銷毀。
  • 系統(tǒng)總可用內(nèi)存為200M,系統(tǒng)使用內(nèi)存超過(guò)190M時(shí)會(huì)觸發(fā)后臺(tái)進(jìn)程被殺。
  • 系統(tǒng)中運(yùn)行著3個(gè)進(jìn)程,他們?cè)谌齻€(gè)Task中的分布和內(nèi)存使用如下
  • Task1處于前臺(tái)運(yùn)行
Momory Usage Process1 Process2 Process 3
Task1 60M 20M -
Task2 20M 20M -
Task3 - - 40M

如果T1 P1部分消耗的內(nèi)存由60M上升到75M,由于P1的總內(nèi)存消耗達(dá)到95M,所以會(huì)導(dǎo)致P1 T2中的Activity被銷毀。

如果T1 P2部分消耗的內(nèi)存由20M上升到50M,會(huì)導(dǎo)致系統(tǒng)總內(nèi)存消耗達(dá)到190M。此時(shí)三個(gè)Process中,P1和P2和前臺(tái)Task關(guān)聯(lián),優(yōu)先級(jí)較高,所以系統(tǒng)會(huì)殺掉P3。

這個(gè)例子,只是對(duì)兩者關(guān)系的一個(gè)簡(jiǎn)要說(shuō)明。系統(tǒng)對(duì)進(jìn)程的實(shí)際處理方式要復(fù)雜得多!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容