生還是死?Android 進程優先級詳解
英文原文:Who lives and who dies? Process priorities on Android
作者:Ian Lake,Google Android 推廣工程師;翻譯:Guokai Han。
讓我們面對現實:移動設備上沒有無限的內存、無限的電池或者其它無限的資源。這對應用而言意味著你應該把進程死亡作為應用生命周期的一個自然過程對待。最重要的是確保殺死進程及內存回收不會對用戶造成負面影響。事實上,Android 中的多數進程架構都是為了確保特定的順序而特別設計的,并按重要性層次遵循一組模式。
Android 進程層次
你會發現最重要的進程被稱為前臺進程,然后依次是任何可見進程、服務進程、后臺進程,最后是空進程。這個文檔中有詳細描述,這里我們將進一步展開。
注意,當我們談論特定組件(服務、activity)時,Android 只殺死進程,而不是組件。當然,這不會阻止通常的垃圾回收進程(它要回收沒有任何引用的對象的內存),不過這是另一個主題了。
前臺進程
你會想正在與用戶交互的東西是最重要的需要保證活著的,這應該完全正確。但是“正在與用戶交互”這個定義有點模糊。當前的前臺 Activity 毫無爭議屬于這一類,它是已經調用了 onResume() 方法但還沒有收到 onPause() 調用的 Activity 。
一些 activity 在依靠他們自己的同時,也可能依賴 bound service 。任何進程,如果它持有一個綁定到前臺 activity 的服務,那么它也被賦予了同樣的前臺優先級。這完全符合直覺,如果前臺 activity 認為和那個服務保持持久連接很重要,那么保持這個服務活著就對 activity 和 Android 很重要。對于正在與前臺服務交互的 content provider 也是如此。
但是誰說用戶能察覺到的只有 activity ?如果正在播放的音樂突然停止或導航方向突然消失,我一定會很惱火。幸好,Android 可以讓服務使用 startForeground() 方法成為高優先級前臺服務。這絕對是媒體播放的最佳實踐,但是這里要問一個重要問題“如果服務停止了,用戶會立刻察覺到嗎?”。前臺服務應該僅被用于關鍵的、可被立刻察覺的場景。
注意:要成為前臺服務需要在服務中包含一個通知以便讓用戶注意到這個服務正在運行。如果你覺得你的使用場景不需要這個通知,那么前臺服務對你可能不是正確的選擇(是的,成為前臺服務并不要求一定運行在后臺,見下文)。
在接收關鍵生命周期方法時會讓一個進程臨時提升為前臺進程,包括任何服務的生命周期方法(onCreate(),onStartCommand() 和 onDestroy()) 和任何廣播接收器 onReceive() 方法。這樣做確保了這些組件的操作是有效的原子操作,每個組件都能執行完成而不被殺掉。
可見進程
等下,我想我已經談到了當前的 activity?你會發現 activity 可見的時候不一定在前臺。一個簡單的例子是前臺的 activity 使用對話框啟動了一個新的 activity 或者一個透明 activity 。另一個例子是當你調用運行時權限對話框時(事實上它就是一個 activity!)。
在收到 onStart() 和收到 onStop() 方法期間的 activity 是可見 activity 。在這兩個方法調用之間,你可以做所有可見 activity 能做的事情(實時更新屏幕等)。
和前臺 activity 類似,可見 activity 的 bound service 和 content provider 也處于可見進程狀態。這同樣是為了保證使用中的 activity 所依賴的進程不會被過早地殺掉。
但請記住,只是可見并不意味著不能被殺掉。如果來自前臺進程的內存壓力過大,可見進程仍有可能被殺掉。從用戶的角度看,這意味著當前 activity 背后的可見 activity 會被黑屏代替。當然,如果你正確地重建你的 activity ,在前臺 activity 關閉之后你的進程和 activity 會立刻恢復而沒有數據損失。
注意:你的 activity 和進程即使可見也可能被殺掉是因為 startActivityForResult()+onActivityResult()或 requestPermissions()+onRequestPermissionsResult() 流程沒有獲得回調類的實例。如果你的整個進程死了,那么所有的回調類實例也死了。如果你看到使用回調方式的庫,你應該意識到這在低內存壓力情況下無法完成。
服務進程
如果你的進程不屬于以上兩種類別,而你有一個啟動的服務(started service),那么它被看作是一個服務進程。對于許多在后臺做處理(如加載數據)而沒有立即成為前臺服務的應用都屬于這種情況。
這沒有問題!絕大多數情況,這是后臺處理的最佳方式。這種進程只有在前面講的可見進程和前臺進程做了太多事情需要更多資源的時候才會被殺掉。
請特別注意從 onStartCommand() 返回的常量,如果你的服務由于內存壓力被殺掉,它表示控制什么發生什么:
- START_STICKY 表示你希望系統可用的時候自動重啟你的服務,但你不關心是否能獲得最后一次的 Intent (例如,你可以重建自己的狀態或者控制自己的 start/stop 生命周期)。
- START_REDELIVER_INTENT 是為那些在被殺死之后重啟時重新獲得 Intent 的服務的,直到你用傳遞給 onStartCommand() 方法的 startId 參數調用 stopSelf() 為止。這里你會使用 Intent 和 startId 作為隊列完成工作。
- START_NOT_STICKY 用于那些殺掉也沒關系的服務。這適合那些管理周期性任務的服務,它們只是等待下一個時間窗口工作。
后臺進程
比如說你的 Activity 一開始是前臺 Activity,但是用戶點了 home 鍵導致 onStop() 方法被調用。假設你之前一直是高優先級進程類別,這時你的進程將變為后臺進程類別。在一般操作場景下,設備上的許多內存就是用在這上面的,讓你重新回到之前打開過的某個 activity 。
Android 不是為了殺而殺的(記住:從頭啟動是有代價的),所以這些進程會保留一段時間,直到更高優先級進程需要內存的時候才被回收,并且是按照最近最少使用順序(最老的會被優先回收)。然而,當他們被殺掉的時候和可見 activity 處理情況一樣,你應該能夠在不丟失用戶狀態的情況下重建這些 activity 。
空進程
在任何層次中,空進程都是最低優先級的。如果不屬于以上類別,那它就是這種。這里沒有活躍的組件,只是出于緩存的目的而被保留(為了更加有效地使用內存而不是完全釋放掉),只要 Android 需要可以隨時殺掉它們。
注意事項
當我們談論進程優先級的時候是以 activity、service 這樣的組件來說的,但請記住這些優先級是在進程的級別上,不是組件級別上。只要一個組件(比如一個前臺服務)就會將整個進程變為前臺進程。絕大多數應用是單進程的,如果你有生命周期差異很大的不同部分或者某個部分非常重量型,那么強烈建議你把它們分為不同的進程,讓重量級進程盡早被回收。
同樣重要的是,你的進程屬于什么類別是組件層面發生的事情決定的。這意味著把非常重要的長時間運行的操作放在 activity 所在進程的一個獨立線程中的做法,在進程突然變成后臺進程的時候可能會遇到問題。使用你能用到的工具(一個服務或基于優先級的前臺服務)來確保系統知道你在做什么。
與別人友好相處,把用戶放在心里
整個系統這樣工作都是為了用戶。做個好公民,做好你的應用,始終讓自己工作在合適的優先級上。請記住,作為一個開發者,你使用的手機可能比你用戶的最差手機快得多得多,你可能從來不會看到可見進程被殺死,遠少于服務進程,但是這不意味著它不會發生!
我仍然建議你購買一個非常低端的 Android 設備用于測試,同時你也可以在高端設備上測試被殺掉時應用是如何響應的。要在包級別殺掉應用,請使用:
adb shell am force-stop com.example.packagename
如果你有多個進程,可以在第二欄找到進程 id(PID)(如,下面第一個數字):
adb shell ps | grep com.example.packagename
然后這樣殺掉:
adb shell kill PID
不論內存壓力多大,確保你的應用在盡可能多的設備上良好運行的第一步是測試你的應用在被殺掉時是如何響應的。