如何優(yōu)雅、高效地恢復(fù)Activity數(shù)據(jù)

google 原文可以點(diǎn)這里

保存 UI 狀態(tài)

在因內(nèi)存不足被系統(tǒng)殺死的應(yīng)用或者因?yàn)?Configuration changes 導(dǎo)致的Activity重建中,以及時(shí)的方式保存和恢復(fù)Activity的UI狀態(tài)是用戶體驗(yàn)的關(guān)鍵部分。在這種情況下用戶是希望新啟動(dòng)的Activity 和最后銷毀的 Activity之間的狀態(tài)要保持一致,但是如果我們不加以處理,系統(tǒng)并不會(huì)自動(dòng)恢復(fù)這些數(shù)據(jù)。

為了實(shí)現(xiàn)用戶的期望,我們可以單獨(dú)或者組合使用 ViewModel、onSaceInstanceState()方法或者是一個(gè)本地文件存儲(chǔ)的方式來保存 UI 狀態(tài),至于是組合還是單獨(dú)用這三種手段,要取決于 UI 數(shù)據(jù)的復(fù)雜度、運(yùn)行設(shè)備的情況、恢復(fù)數(shù)據(jù)的速度和消耗內(nèi)存的大小來綜合考慮。

不管我們使用哪種方式,我們應(yīng)該讓用戶看到他們最后看到的 UI 狀態(tài),用一種自然平滑的方式來加載數(shù)據(jù),避免占用太多時(shí)間加載需要重新呈現(xiàn)的 UI 狀態(tài),特別是在頻繁的發(fā)生 Configuration changes 的情況,比如旋轉(zhuǎn)屏幕。大多數(shù)情況下我們應(yīng)該使用 ViewModel 和 onSaveInstanceState()方法。

本章討論我們用來滿足期望所使用的幾種方式之間的差別,并列舉不同的使用場(chǎng)景,在不同的場(chǎng)景中我們將討論這幾種方式的優(yōu)缺點(diǎn),并權(quán)衡地選出一種較為合理的手段。

用戶期望和系統(tǒng)行為


用戶可能希望清空 UI 狀態(tài),也可能期望保存 UI 狀態(tài),這取決于用戶的操作。在某些情況中系統(tǒng)的行為結(jié)果和用戶的期望是一致的,在另外一些情況則不滿足用戶的期望。

用戶主動(dòng)讓 UI 狀態(tài)消失

用戶期望當(dāng)啟動(dòng)一個(gè) Activity 之后,一些不重要的 UI 狀態(tài)也可以一直存在,除非他們自己手動(dòng)關(guān)閉一個(gè) Activity,用戶可以通過以下幾種方式關(guān)閉一個(gè) Activity:

  • 按返回按鈕
  • 后臺(tái)滑掉應(yīng)用
  • 返回導(dǎo)航棧中上一個(gè) Activity 的按鈕
  • 在設(shè)置中心中強(qiáng)制停止一個(gè)應(yīng)用
  • 操作了某些執(zhí)行結(jié)束操作的動(dòng)作,比如某些動(dòng)作所調(diào)用的Activity.finish()方法

用戶覺得通過以上的方式后,應(yīng)該是會(huì)銷毀所有臨時(shí)的 UI 狀態(tài)的,如果重新打開 Activity 以后,它的界面應(yīng)該是一個(gè)初始的、干凈的界面。在這些場(chǎng)景中,系統(tǒng)的行為結(jié)果就滿足用戶的期望---Activity 實(shí)例會(huì)被銷毀并從內(nèi)存中移除,并且任何的 UI 狀態(tài)都不會(huì)被保存,在重新打開也就不會(huì)恢復(fù)這些 UI 狀態(tài)了。

對(duì)于某些場(chǎng)景來說這樣的操作后用戶并不希望徹底不保存 UI 狀態(tài),比如對(duì)于一個(gè)瀏覽器而言,用戶期望的可能是點(diǎn)擊返回按鈕之后不銷毀這個(gè)界面而是返回上一頁瀏覽的網(wǎng)頁。

系統(tǒng)讓 UI 狀態(tài)消失

用戶希望在Configuration changes,比如旋轉(zhuǎn)屏幕、拖入多窗口模式之后,Activity 的 UI 狀態(tài)應(yīng)該和之前的保持一致。但是系統(tǒng)的默認(rèn)行為結(jié)果和此相反,任何的 UI 狀態(tài)都不會(huì)得到保存和恢復(fù),雖然我們可以通過更改 Configuration changes 的默認(rèn)行為,但是我們并不推薦這種方式。

用戶希望在他們短暫地離開了 Activity 之后再回來還能看到的 Activity 還是最后看到的樣子,比如用戶在我們的搜索 Activity 中輸入搜索內(nèi)容之后按了 home 鍵或者被一個(gè)來電打斷了---當(dāng)他們重新回到這個(gè)界面的時(shí)候,用戶希望能看到他們最后輸入的搜索詞。

在這種場(chǎng)景中,我們的 app 應(yīng)該在后臺(tái)做一些操作讓我們的應(yīng)用能在內(nèi)存中駐留。但是系統(tǒng)可能因?yàn)閮?nèi)存不足而殺死我們的不可視應(yīng)用,在這種情況下,Activity 對(duì)象會(huì)被銷毀,所以用戶重新加載 app 之后看到的不是他們最后看到的狀態(tài),這種情況是用戶不希望看到的。

保存UI狀態(tài)幾種方式的介紹


當(dāng)用戶期望UI狀態(tài)得以保存而系統(tǒng)默認(rèn)的行為是不保存的時(shí)候,我們必須手動(dòng)存儲(chǔ)和恢復(fù)丟失的數(shù)據(jù),確保用戶感知不到Activity被銷毀重建了。
保存UI狀態(tài)大致分為三類,它們之間保存數(shù)據(jù)的方式和時(shí)效各不相同,如下圖所示:


截圖.png

使用ViewModel來保存因Configuration changes所丟失的數(shù)據(jù)


ViewModel中保存的數(shù)據(jù)不會(huì)因?yàn)镃onfiguration changes發(fā)生而丟失,并且由于其中的數(shù)據(jù)是保存在內(nèi)存中的,所以恢復(fù)數(shù)據(jù)的速度是幾種方式中最快的。

ViewModel是與Activity(或者其他LifecycleOwner對(duì)象)關(guān)聯(lián)的,在configuration changes之后,ViewModel對(duì)象依然在內(nèi)存中存活,并且自動(dòng)和新重建的Activity(或者其他LifecycleOwner對(duì)象)建立關(guān)聯(lián)。

當(dāng)我們調(diào)用finish()方法的時(shí)候,這意味著用戶希望清除Activity的狀態(tài),這時(shí)ViewModel對(duì)象會(huì)被系統(tǒng)自動(dòng)銷毀。

不像saved instance state在應(yīng)用、Activity被系統(tǒng)殺死、自動(dòng)重建還能存活,在這個(gè)過程中ViewModel對(duì)象會(huì)被銷毀。所以這也是我們應(yīng)該使用ViewModel配合onSaveInstanceState()或者其它硬盤存儲(chǔ)手段來保存UI狀態(tài)的原因,我們可以在onSaveInstanceState()中保存數(shù)據(jù)的關(guān)鍵標(biāo)志,在系統(tǒng)殺死、重建應(yīng)用之后去恢復(fù)數(shù)據(jù)。

使用onSaveInstanceState()備份數(shù)據(jù),在系統(tǒng)殺死、重建應(yīng)用之后恢復(fù)


在系統(tǒng)殺死一個(gè)應(yīng)用(Activity)后,如果稍后這個(gè)應(yīng)用(Activity)又被系統(tǒng)自動(dòng)啟動(dòng),那么該Activity的onSaveInstanceState()會(huì)被調(diào)用,我們可以在這個(gè)方法中恢復(fù)Activity中的UI狀態(tài)。

雖然通過saved instance state方式保存的Bundle類型數(shù)據(jù),可以在Configuration changes和系統(tǒng)殺死、自動(dòng)重建的情況下存活,但是這種方式會(huì)受到硬盤大小和讀寫速度所限制,除此之外,需要保存的數(shù)據(jù)需要先序列化,序列化數(shù)據(jù)的開銷也受到數(shù)據(jù)大小的限制,因?yàn)樾蛄谢^程是發(fā)生在主線程的,所以如果我們需要序列化大量數(shù)據(jù),這時(shí)候可能會(huì)導(dǎo)致程序丟幀,對(duì)用戶體驗(yàn)造成一定的影響。

所以不要用onSaveInstanceState()去保存大量數(shù)據(jù),比如圖片或者需要消耗大量序列化、反序列化時(shí)間的復(fù)雜結(jié)構(gòu)數(shù)據(jù)。相反的我們應(yīng)該只保存一些基礎(chǔ)、簡(jiǎn)單的數(shù)據(jù)比如String。同樣的我們應(yīng)該保存最關(guān)鍵的少量必要數(shù)據(jù),比如一個(gè)ID,當(dāng)其它恢復(fù)策略時(shí)效后,我們可以通過這個(gè)ID去恢復(fù)之前的UI狀態(tài)。

onSaveInstanceState()不是必須實(shí)現(xiàn)的。舉個(gè)例子,在瀏覽器中,當(dāng)用戶點(diǎn)擊返回按鈕的時(shí)候,我們可能讓瀏覽器顯示用戶上一個(gè)訪問的網(wǎng)頁,這個(gè)時(shí)候我們就沒必要使用onSaveInstanceState()方法,而是通過其它手段將上一個(gè)網(wǎng)頁的所有數(shù)據(jù)保存在本地。

除此之外,當(dāng)你使用Intent打開一個(gè)Activity的時(shí)候,如果需要傳遞數(shù)據(jù),我們使用Intent.putExtras方法來傳遞數(shù)據(jù),當(dāng)被啟動(dòng)的Activity因Configuration changes導(dǎo)致銷毀重建的時(shí)候,我們?nèi)匀豢梢詮腎ntent處獲取extras數(shù)據(jù),這可以代替onSaveInstanceState()方法。

但是ViewModel是無可替代的,在任何情況下我們都需要使用ViewModel保存數(shù)據(jù),避免在Configuration changes情況下浪費(fèi)資源去從數(shù)據(jù)庫(kù)加載數(shù)據(jù)。

如果要保存的UI數(shù)據(jù)簡(jiǎn)單且輕量級(jí),我們可以單獨(dú)使用onSaveInstanceState()來保存狀態(tài)數(shù)據(jù)。

使用硬盤存儲(chǔ)來復(fù)雜、大量的數(shù)據(jù)


通過將數(shù)據(jù)保存在本地,比如數(shù)據(jù)庫(kù)或者偏好設(shè)置文件,只要用戶不卸載或者清理app的數(shù)據(jù),我們的數(shù)據(jù)都不會(huì)丟失。所以應(yīng)用因?yàn)槭裁丛蛑匦麓蜷_,我們都可以從本地文件中恢復(fù)之前的數(shù)據(jù)。但是這種方式的開銷比前面兩種大,因?yàn)槲覀冃枰獙⒂脖P中的數(shù)據(jù)先讀取、載入內(nèi)存。通常我們?cè)O(shè)置一個(gè)APP的時(shí)候,數(shù)據(jù)庫(kù)存儲(chǔ)已經(jīng)成為架構(gòu)中的一個(gè)重要環(huán)節(jié),它用來保存用戶所有想要保存的數(shù)據(jù),并且在下次打開應(yīng)用的時(shí)候還可以恢復(fù)。

在這種情況下,ViewModel和saved instance state這兩種方式保存的時(shí)間都不夠長(zhǎng),所以這種情況下本地化存儲(chǔ)方案是無法替代的,比如使用數(shù)據(jù)庫(kù)存儲(chǔ)。相反的,對(duì)于臨時(shí)數(shù)據(jù),我們不應(yīng)該使用本地化存儲(chǔ)方案,而應(yīng)該使用ViewModel或者saved instance state方案。

使用分治法存儲(chǔ)UI狀態(tài)


將'保存和恢復(fù)'任務(wù)分配給這幾種方式,讓它們協(xié)和完成這個(gè)過程是一種高效的手段。在大多數(shù)情況下,鑒于需要保存數(shù)據(jù)的復(fù)雜性、幾種方式的生命周期和獲取數(shù)據(jù)的速度,我們應(yīng)該讓它們存儲(chǔ)不同類型的數(shù)據(jù)。

  • 本地文件存儲(chǔ):當(dāng)我們重復(fù)開閉一個(gè)Activity,我們不希望某些重要數(shù)據(jù)丟失,這時(shí)候我們就需要用到這種存儲(chǔ)方案。
    比如存儲(chǔ)音樂文件。
  • ViewModel:存儲(chǔ)臨時(shí)的、和Activity關(guān)聯(lián)的數(shù)據(jù),數(shù)據(jù)可以是復(fù)雜的,因?yàn)閿?shù)據(jù)存儲(chǔ)在內(nèi)存中。
    比如存儲(chǔ)最近播放的音樂和最近搜索的關(guān)鍵字
  • onSaveInstanceState(): 存儲(chǔ)少量、簡(jiǎn)單、必須的數(shù)據(jù),當(dāng)系統(tǒng)殺死、自動(dòng)重建Activity以后用來恢復(fù)數(shù)據(jù)。如果是復(fù)雜的數(shù)據(jù),那么應(yīng)該講數(shù)據(jù)存儲(chǔ)在本地文件比如數(shù)據(jù)庫(kù),然后在這個(gè)方法中存儲(chǔ)數(shù)據(jù)的標(biāo)志符,在Activity重建后通過這個(gè)標(biāo)志符去數(shù)據(jù)庫(kù)中加載、恢復(fù)UI狀態(tài)。
    比如存儲(chǔ)最近搜索的關(guān)鍵字

舉個(gè)例子,當(dāng)我們的Activity允許我們?cè)谝魳穾?kù)搜索音樂的時(shí)候,以下是處理不同事件的方法:

當(dāng)用戶添加一首音樂的時(shí)候,ViewModel會(huì)通知本地存儲(chǔ)組件去存儲(chǔ)這首音樂。如果這首新添加的音樂需要呈現(xiàn)在UI中,我們還應(yīng)該更新ViewModel對(duì)象中的數(shù)據(jù),反應(yīng)它關(guān)聯(lián)的UI狀態(tài)。請(qǐng)記住,我們應(yīng)該在子線程對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作。

當(dāng)用戶搜索一首歌的時(shí)候,從數(shù)據(jù)庫(kù)讀取出來的歌曲不管多么復(fù)雜, 我們都應(yīng)該馬上存儲(chǔ)在ViewModel中,同時(shí)將這個(gè)搜索關(guān)鍵字也一并保存在ViewModel對(duì)象中。

當(dāng)應(yīng)用進(jìn)入后臺(tái),系統(tǒng)會(huì)調(diào)用onSaveInstanceState()。我們應(yīng)該保存搜索關(guān)鍵字在onSaveInstanceState()的bundle參數(shù)中,這個(gè)小數(shù)據(jù)容易保存,這個(gè)數(shù)據(jù)也是我們恢復(fù)UI狀態(tài)所需的所有信息,輕量又全面。

使用組裝法恢復(fù)復(fù)雜數(shù)據(jù)


當(dāng)用戶返回Activity時(shí),有兩種可能的場(chǎng)景會(huì)重新創(chuàng)建Activity:

  • 該Activity在被系統(tǒng)停止后被重新創(chuàng)建。我們可以獲取之前保存在onSaveInstanceState()中的查詢關(guān)鍵字,我們應(yīng)該將這個(gè)關(guān)鍵字傳遞給ViewModel對(duì)象,ViewModel發(fā)現(xiàn)內(nèi)存中并沒有存儲(chǔ)對(duì)應(yīng)的歌曲,所以它通知本地?cái)?shù)據(jù)層去加載對(duì)應(yīng)的歌曲,最后將結(jié)果反饋給UI層。

  • 當(dāng)Configuration changes之后,Activity被重新加載,我們可以獲取之前保存在onSaveInstanceState()中的查詢關(guān)鍵字,我們應(yīng)該將這個(gè)關(guān)鍵字傳遞給ViewModel對(duì)象,因?yàn)閂iewModel可以在Configuration changes后存活,所以它可以在內(nèi)存中找到對(duì)應(yīng)的歌曲信息,并直接將結(jié)果反饋給UI層。這意味著它不需要從數(shù)據(jù)庫(kù)中重新查詢。

?著作權(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)容