背景介紹
作為一個Android開發者,肯定會遇到這樣的一種情況,用戶在玩著你開發的app時,突然有微信來消息了,切換到了微信,然后還在微信逗留看視頻啊,聊天啊,刷朋友圈啊等等的,你所開發的app就出于后臺了,這個時候就很容易出現手機內存不足,app被內存回收干掉的情況了,等用戶終于聊完天,刷完朋友圈,回來app的時候,就會進行app的自我恢復了,如果開發者處理不好,就會出現崩潰的情況了,而且肯定會出現返回的時候一瞬間白屏,然后再顯示出來,這樣的用戶體驗非常的不好。那我們應該怎樣去解決這樣的狀況呢?扯了那么多,我們的文章就正式開始啦!
開始
首先要介紹下Android中activity的四種啟動模式(就當作復習一下舊知識吧,資料來源于網絡總結):
Standard:是默認的也是標準的Task模式,在沒有其他因素的影響下,使用此模式的Activity,會構造一個Activity的實例,加入到調用者的Task棧中去,對于使用頻度一般開銷一般什么都一般的Activity而言,standard模式無疑是最合適的,因為它邏輯簡單條理清晰,所以是默認的選擇。
singleTop:基本上于standard一致,僅在請求的Activity正好位于棧頂時,有所區別。此時,配置成singleTop的Activity,不再會構造新的實例加入到Task棧中,而是將新來的Intent發送到棧頂Activity中,棧頂的Activity可以通過重載onNewIntent來處理新的Intent(當然,也可以無視...)。這個模式,降低了位于棧頂時的一些重復開銷,更避免了一些奇異的行為(想象一下,如果在棧頂連續幾個都是同樣的Activity,再一級級退出的時候,這是怎么樣的用戶體驗...),很適合一些會有更新的列表Activity展示。一個活生生的實例是,在Android默認提供的應用中,瀏覽器(Browser)的書簽Activity(BrowserBookmarkPage),就用的是singleTop。
singleTask:配置了這個屬性的activity,最多僅有一個實例存在,而且,它在根的task中,在之后的被殺死重啟的過程中我們會利用到這個配置,也就是我們的主界面MainActivity。
singleInstance:跟上面的singleTask基本上是一樣的,但是,singleInstance的Activity,是它所在棧中僅有的一個Activity,如果涉及到的其他Activity,都移交到其他Task中進行,在實際開發中這個是用得比較少的。
這個是activity的生命周期:
咱們就不多介紹這個生命周期了,相信都熟悉不過了,有想了解的自行Google或者百度吧。
重點
接下來是我們的重點:程序如果在后臺被殺死之后,我們怎么去處理?是立刻恢復還是重新啟動?哪個方法更適合我們?
首先,我們得知道,為什么程序會在后臺被干掉的?我們又沒有手動關閉程序。
app在后臺被強殺,是在內存不足的情況下被強制釋放了,也有一些惡心的rom會強制殺掉那些后臺進程以釋放緩存以提高所謂的用戶體驗。(注:當你的代碼寫得混亂、冗余,而且非常消耗內存的時候,那你的app在后臺運行時將會比較容易被系統給干掉的,所以從現在開始要約束自己要養成良好的編碼習慣和注意內存泄漏的問題)
我們都覺得android rom很惡心,但同時還是用些更惡心的手法去繞開這些瓶頸。亂,是因為在最上層沒有一個很好的約束,這也是開源的弊端。anyway。我們還是得想破腦袋來解決這些問題,否則飯碗就沒了。
我們現在來重現這個熟悉的一幕:
假設:App A -> B -> C
在C activity中點Home鍵后臺運行,打開ddms,選中該App進程,強殺。
然后從“最近打開的應用”中選中該App,回到的界面是C activity,假設App中沒有靜態變量,這個時候是不會crash的,點擊返回到B,這個時候也只是短暫白屏后顯示B界面。但如果B中有引用靜態變量,并想要獲取靜態變量中的某個值時,就NullPointer了。
以上復現的流程就幾個點,我們展開說下:
當應用被強殺,整個App進程都是被殺掉了,所有變量全都被清空了。包括Application實例。更別提那些靜態變量了。
雖然變量被清空了,但Android給了一些補救措施。activity棧沒有被清空,也就是說A -> B -> C這個棧還保存了,只是ABC這幾個activity實例沒有了。所以回到App時,顯示的還是C頁面。另外當activity被強殺時,系統會調用onSaveInstance去讓你保存一些變量,但我個人覺得面對海量的靜態變量,這個根本不夠用。返回到B會白屏,是因為B要重繪,重走onCreate流程,渲染上需要點時間,所以會白屏了。
大概是以上這些點。如果App中沒有靜態變量的引用,那就不用出現NullPointer這個crash,也就不需要解決。一旦你有靜態變量,或者有些Application的全局變量,那就很危險了。比如登錄狀態,user profile等等。這些值都是空了。
肯定會有人說,這沒關系啊,所有的靜態變量都改到單例去不就好了嗎?然后附加上一些持久化cache,空了再取緩存就ok了嘛。嗯,這肯定也是一個辦法,但是這樣的束手束腳對開發來說也是痛苦,至少需要多50%的編碼時間才能全部cover。另外,還有那么多幫你挖坑的隊友,難省心啊。
既然App都被強殺了,干嘛不重新走第一次啟動的流程呢,別讓App回到D而是啟動A,這樣所有的變量都是按正常的流程去初始化,也就不會空指針了,對吧?有人說這方案用戶體驗一點都不好呀。但哪有十全十美的事呢,是重走流程好,還是一點一個NullPointer好?好好去溝通,相信產品也不會為難你的。當然你也可以拿來舉例,iOS在最近打開的應用里殺了某個App,重新點擊那個App,還是會重走流程的啊。
如果你說用戶已經打開了C界面,所以重新打開的是是恢復到C界面,這樣的用戶體驗會更好啊,如果你是這樣認為的,那你很多時間都是在防止恢復的時候不讓你的app crash了,與其這樣,還不如讓整個app重新走整個流程呢,這樣更省時間,而且這樣也不用擔心隨時都會崩潰的情況,難道這樣的用戶體驗不會更好嗎?
那且想想如何讓它不回到C而是重走流程呢?也就是說中斷C的初始化而回到A,并且按back鍵,不會回到C,B。考慮一下。
我們先實例化這個場景吧。
A為App的啟動頁
B為首頁
C為二級頁面
把首頁launchMode設置為singleTask,具體為什么上面介紹activity的啟動模式的時候已經介紹了singleTask的作用了。
在BaseActivity中onCreate中判斷App是否被強殺,強殺就不往下走,直接重走App流程。
首頁起一個承接或者中轉的作用,所有跨級跳轉都需要通過首頁來完成。
再給個提示,以上場景的解決方案也可以用于解決其它相關問題:
在任意頁面退出App
在任意頁面返回到首頁
其實最重要的知識點就是launchMode
具體實現
AppStatusConstant
public static final intSTATUS_FORCE_KILLED = -1;//應用放在后臺被強殺了
public static final intSTATUS_NORMAL = 2; //APP正常態//intent到MainActivity區分跳轉目的
public static finalStringKEY_HOME_ACTION = "key_home_action";//返回到主頁面
public static final intACTION_BACK_TO_HOME = 0;//默認值
public static final intACTION_RESTART_APP = 1;//被強殺
AppStatusManager
public intappStatus= AppStatusConstant.STATUS_FORCE_KILLED; //APP狀態初始值為沒啟動不在前臺狀態
public staticAppStatusManagerappStatusManager;
privateAppStatusManager() {
}
public staticAppStatusManagergetInstance() {
if(appStatusManager==null{
appStatusManager=newAppStatusManager();
}
returnappStatusManager;
}
public intgetAppStatus() {
returnappStatus;
}
public voidsetAppStatus(intappStatus) {
this.appStatus= appStatus;
}
BaseActivity(大致內容)
switch(AppStatusManager.getInstance().getAppStatus()) {
caseAppStatusConstant.STATUS_FORCE_KILLED:
restartApp();
break;
caseAppStatusConstant.STATUS_NORMAL:
setUpViewAndData();
break;
}
protected abstract voidsetUpViewAndData();
protected voidrestartApp() {
Intent intent =newIntent(this,MainActivity.class);
intent.putExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_RESTART_APP);
startActivity(intent);
}
每一個繼承于父activity的都不要在oncreat中實現界面初始化和數據的初始化,因為如果被殺死之后,回來會走一次正常的生命流程的。
StartPageActivity配置(在oncreat()方法配置,并且在super()前):
AppStatusManager.getInstance().setAppStatus(AppStatusConstant.STATUS_NORMAL);//進入應用初始化設置成未登錄狀態
MainActivity(配置了singleTask的主界面)
@Override
protected voidrestartApp() {
Toast.makeText(getApplicationContext(),"應用被回收重啟",Toast.LENGTH_LONG).show();
startActivity(newIntent(this,StartPageActivity.class));
finish();
}
@Override
protected voidonNewIntent(Intent intent) {
super.onNewIntent(intent);
intaction = intent.getIntExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_BACK_TO_HOME);
switch(action) {
caseAppStatusConstant.ACTION_RESTART_APP:
restartApp();
break;
}
}
當應用打開的時候,啟動StartPageActivity,然后設置app的status為normal狀態,記住,一定要設置,因為默認的是被殺死的狀態的。
當應用被殺死之后,所有數據都會被回收,所以之前設置的app status也會置于默認狀態,即殺死狀態,所以再次打開app的時候,status為殺死狀態,就會走重啟的流程,這里為什么要先跳轉到MainActivity呢?就是因為MainActivity配置為了Sing了Task,當跳轉到這個界面時,MainActivity就會置于Activity Task的最上層,其他的Activity將會被默認銷毀掉,利用這種技巧去銷毀其他的Activity,最后才是重新啟動StartPageActivity。整個流程就是這樣了。
大致的實現就如上所述了,我所倡導的宗旨就是花最少的時間,寫最好的代碼,實現最好的體驗!之前也參考過很多網上大神們的實現方式,但是我覺得以上實現的應該是比較完整的一種了。
此文思路借鑒于stay4it