簡介:Android 是一款基於 Linux 核心,面向移動終端的作業系統。為適應其作為移動平臺作業系統的特殊需要,谷歌對其做了特別的設計與優化,使應用程式關閉但不退出,並由作業系統進行程式的回收管理。本文在 Application Framework 與 Linux 核心兩個層次上,以程式為粒度,對 Android 作業系統的程式資源回收機制進行了剖析。讀者可以從本文獲得對 Android 應用程式的生存週期的進一步理解,從而更加合理、高效地構建應用程式。
Android APP 的執行環境
Android 是一款基於 Linux 核心,面向移動終端的作業系統。為適應其作為移動平臺作業系統的特殊需要,谷歌對其做了特別的設計與優化,使得其程式排程與資源管理與其他平臺的 Linux 有明顯的區別。主要包含下面幾個層次:
1.Application Framework
Application Framework 將整個作業系統分隔成兩個部分。對應用開發者而言,所有 APP 都是執行在 Application Framework 之上,而並不需要關心繫統底層的情況。Application Framework 層為應用開發者提供了豐富的應用程式設計介面,如 Activity Manager,Content Provider,Notification Manager,以及各種視窗 Widget 資源等。在 Application Framework 層,Activity 是一個 APP 最基本的組成部分。一般每個 Activity 對應於螢幕上的一個檢視(或者說一屏),一個 APP 可以有一個或者多個 Activity。應用程式被打包成 .apk 格式的檔案,由 Dalvik VM 解釋執行。
2.Dalvik VM
Dalvik 虛擬機器採用暫存器架構,而不是 JVM 的棧結構。Java 程式編譯後的 .class 檔案並不能在 Dalvik 中解釋執行。因此 Google 提供了一個 dx 工具,用於將 .class 檔案轉換成 Dalivk 能夠識別的 .dex 格式。具體 Dalvik VM 的細節不是本文重點,以下不再討論。
3.Linux kernel
由上所述,所有的 APP 都是由 Java 程式碼編寫並在 Dalvik VM 中得到解釋執行。在 Android 作業系統中,每個 Dalvik VM 的每個 Instance 都對應於 Linux 核心中的一個程式。可以使用 adb shell 工具檢視系統中的當前程式。如下圖所示,Android2.3.3 啟動後核心中的程式列表
下面將從 Application Framework 和 Linux kernel 兩個層次分析 Android 作業系統的資源管理機制。
Android 之所以採用特殊的資源管理機制,原因在於其設計之初就是面向移動終端,所有可用的記憶體僅限於系統 RAM,必須針對這種限制設計相應的優化方案。當 Android 應用程式退出時,並不清理其所佔用的記憶體,Linux 核心程式也相應的繼續存在,所謂“退出但不關閉”。從而使得使用者呼叫程式時能夠在第一時間得到響應。當系統記憶體不足時,系統將啟用記憶體回收過程。為了不因記憶體回收影響使用者體驗(如殺死當前的活動程式),Android 基於程式中執行的元件及其狀態規定了預設的五個回收優先順序:
IMPORTANCE_FOREGROUND:
IMPORTANCE_VISIBLE:
IMPORTANCE_SERVICE:
IMPORTANCE_BACKGROUND:
IMPORTANCE_EMPTY:
這幾種優先順序的回收順序是 Empty process、Background process、Service process、Visible process、Foreground process。關於劃分原則參見 http://developer.android.com/guide/topics/fundamentals/processes-and-threads.html檔案中。
ActivityManagerService 集中管理所有程式的記憶體資源分配。所有程式需要申請或釋放記憶體之前必須呼叫 ActivityManagerService 物件,獲得其“許可”之後才能進行下一步操作,或者 ActivityManagerService 將直接“代勞”。類 ActivityManagerService 中涉及到記憶體回收的幾個重要的成員方法如下:trimApplications(),updateOomAdjLocked(),activityIdleInternal() 。這幾個成員方法主要負責 Android 預設的記憶體回收機制,若 Linux 核心中的記憶體回收機制沒有被禁用,則跳過預設回收。
Android 作業系統中的記憶體回收可分為兩個層次,即預設記憶體回收與核心級記憶體回收,本章重點對預設記憶體回收機制進行研究,Linux 核心層次的記憶體回收機制將在下一張介紹。 本章所有程式碼可參見 ActivityManagerService.java。
Android 系統中記憶體回收的觸發點大致可分為三種情況。第一,使用者程式呼叫 StartActivity(), 使當前活動的 Activity 被覆蓋;第二,使用者按 back 鍵,退出當前應用程式;第三,啟動一個新的應用程式。這些能夠觸發記憶體回收的事件最終呼叫的函式介面就是 activityIdleInternal()。當 ActivityManagerService 接收到非同步訊息 IDLE_TIMEOUT_MSG 或者 IDLE_NOW_MSG 時,activityIdleInternal() 將會被呼叫。程式碼如下:
1 2 3 4 5 |
case IDLE_NOW_MSG:{ IBinder token = (Ibinder)msg.obj; activityIdle(token, null); } break; |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
case IDLE_TIMEOUT_MSG: { if (mDidDexOpt) { mDidDexOpt = false; Message nmsg = mHandler.obtainMessage(IDLE_TIMEOUT_MSG); nmsg.obj = msg.obj; mHandler.sendMessageDelayed(nmsg, IDLE_TIMEOUT); return; } IBinder token = (IBinder)msg.obj; Slog.w(TAG, "Activity idle timeout for " + token); activityIdleInternal(token, true, null); } break; |
IDLE_NOW_MSG 由 Activity 的切換以及 Activiy 焦點的改變等事件引發,IDLE_TIMEOUT_MSG 在 Activity 啟動超時的情況下引發,一般這個超時時間設為 10s,如果 10s 之內一個 Activity 依然沒有成功啟動,那麼將傳送非同步訊息 IDLE_TIMEOUT_MSG 進行資源回收。activityIdleInternal() 的主要任務是改變系統中 Activity 的狀態資訊,並將其新增到不同狀態列表中。其主要工作如下:
首先,呼叫 scheduleAppGcsLocked() 方法通知所有進行中的任務進行垃圾回收。scheduleAppGcsLocked() 將進行排程 JVM 的 garbage collect,回收一部分記憶體空間,這裡僅僅是通知每個程式自行程式垃圾檢查並排程回收時間,而非同步回收。然後,取出 mStoppingActivities 和 mFinishigActivities 列表中的所有內容,暫存在臨時變數中。這兩個列表分別儲存了當前狀態為 stop 和 finishi 的 activity 物件。對於 stop 列表,如果其中的 activity 的 finish 狀態為 true,判斷是不是要立即停止,如果要立即停止則呼叫 destroyActivityLocked() 通知目標程式呼叫 onDestroy() 方法,否則,先呼叫 resumeTopActivity() 執行下一個 Activity。如果 finish 狀態為 false,則呼叫 stopActivityLocked() 通知客戶程式停止該 Activity,這種情況一般發生在呼叫 startActivity() 後。對於 finish 列表,直接呼叫 destroyActivityLocked() 通知客戶程式銷燬目標 Activity。
這裡的 destroyActivityLocked 等函式並沒有真正意義上改變記憶體的使用,只是將其狀態改變為“允許回收”,真正的回收在下面即將呼叫的 trimApplications() 函式中。
trimApplications() 函式的結構如下 :
清單 3. trimApplications 函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private final void trimApplications() { synchronized (this) { // First remove any unused application processes whose package // has been removed. for (i=mRemovedProcesses.size()-1; i>=0; i--) { (1)//kill process; } if (!updateOomAdjLocked()) { (2)//do something default } // Finally, if there are too many activities now running, try to // finish as many as we can to get back down to the limit. (3)do something } } |
清單 3 中的三個標序號的位置分別負責如下工作:
(1)當程式執行到 trimApplications() 之後,首先檢查 mRemovedProcesses 列表中的程式。mRemovedProcesses 列表中主要包含了 crash 的程式、5 秒內沒有響應並被使用者選在強制關閉的程式、以及應用開發這呼叫 killBackgroundProcess 想要殺死的程式。呼叫 Process.killProcess 將所有此類程式全部殺死。
(2)呼叫 updateOomAdjLocked() 函式,若成功返回,說明 Linux 核心支援 setOomAdj() 介面,updateOomAdjLocked 將修改 adj 的值並通知 linux 核心,核心根據 adj 值以及記憶體使用情況動態管理程式資源(lowmemorykiller 和 oom_killer)。若 updateOomAdjLocked() 返回為假,則表示當前系統不支援 setOomAdj() 介面,將在本地進行預設的資源回收。
(3)最後,如果當前依然執行了過多的 Activity,對多餘的 Activity 進行回收。 trimApplications() 的大多數的程式碼都在處理 Oom_killer 不存在情況下的預設資源回收,下面對其預設回收過程(即程式碼清單中標記(2)的位置)進行進一步分析。其回收過程可大致描述如下。
步驟一,獲取當前所有執行的程式 mLruProcesses,mLruProcesses 中的排序規則是按最近使用時間。對 mLruProcesses 中不能被關閉的程式進行計數,這些不能被關閉的程式包括執行 service 的程式,執行 broadcast receiver 的程式等,見如下程式碼。
清單 4. 計數不能被關閉的程式
1 2 3 4 5 6 7 |
if (app.persistent || app.services.size() != 0 || app.curReceiver != null || app.persistentActivities > 0) { // Don't count processes holding services against our // maximum process count. numServiceProcs++; } |
步驟二, 設當前最大執行程式數 curMaxProcs = curMaxProcs + numServiceProcs(即預設最大程式數與執行 Service 的程式數之和),如果當前程式的數量 mRemovedProcesses.size() 大於這個值,則遍歷所有當前執行的程式,殺死符合條件的那些程式並釋放記憶體。清理過程見清單 5(部分程式碼省略)。從清單 5 的程式碼中可以看出,程式被殺死的條件是:
● 必須是非 persistent 程式,即非系統程式;
● 必須是空程式,即程式中沒有任何 activity 存在。如果殺死存在 Activity 的程式,有可能關閉使用者正在使用的程式,或者使應用程式恢復的時延變大,從而影響使用者體驗;
● 必須無 broadcast receiver。執行 broadcast receiver 一般都在等待一個事件的發生,使用者並不希望此類程式被系統強制關閉;
● 程式中 service 的數量必須為 0。存在 service 的程式很有可能在為一個或者多個程式提供某種服務,如 GPS 定位服務。殺死此類程式將使其他程式無法正常服務。
以上條件缺一不可。
清單 5. 清理過程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
if (!app.persistent && app.activities.size() == 0 && app.curReceiver == null && app.services.size() == 0) { if (app.pid > 0 && app.pid != MY_PID) { Process.killProcess(app.pid); } else { try { app.thread.scheduleExit(); } catch (Exception e) { // Ignore exceptions. } } // todo: For now we assume the application is not buggy // or evil, and will quit as a result of our request. // Eventually we need to drive this off of the death // notification, and kill the process if it takes too long. cleanUpApplicationRecordLocked(app, false, i); i--; } |
步驟三,再次檢查當前執行的程式,如果 mRemovedProcesses.size() 仍然大於 curMaxProcs,則放寬條件再次進行回收。判斷條件見程式碼清單 6(部分程式碼省略)。下面程式碼中,布林變數 canQuit 的值為真時,那麼這個程式可以被回收。canQuit 的取值分兩個步驟,首先是根據程式的屬性賦值。 1. 必須是非 persistent 程式,即非系統程式;2. 必須無 broadcast receiver;3. 程式中 service 的數量必須為 0;4. persistent 型別的 activity 數量為 0。與步驟二唯一的不同在第 4 條,這裡不要求程式是空程式,只要程式中沒有 persistent 型別的 Activity 就可以(Activity 是否是 persistent 型別在開發階段指定)。這些條件都滿足時,再檢查程式中每個 Activity 的屬性,當該程式中所有的 Activity 都還必須滿足三個條件:Activity 的狀態已經儲存,當前處在不可見狀態並且 Activity 已經 Stop。這時殺掉程式只會降低下次呼叫程式時的載入速度,下次啟動時將恢復到關閉之前的狀態,並不會在使用者體驗上造成致命的影響,所以,canQuit 置位為真。這種情況與步驟二的回收方式也有所不同,由於程式中 Activity 的數量不是 0,下一步需要對每個 activity 執行 destroyActivityLocked() 銷燬,最後才殺死程式。
清單 6. 執行 destroyActivityLocked() 銷燬
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
boolean canQuit = !app.persistent && app.curReceiver == null && app.services.size() == 0 && app.persistentActivities == 0; int NUMA = app.activities.size(); for (j=0; j<NUMA && canQuit; j++) { HistoryRecord r = (HistoryRecord)app.activities.get(j); canQuit = (r.haveState || !r.stateNotNeeded) && !r.visible && r.stopped; } if (canQuit) { // Finish all of the activities, and then the app itself. for (j=0; j<NUMA; j++) { HistoryRecord r = (HistoryRecord)app.activities.get(j); if (!r.finishing) { destroyActivityLocked(r, false); } r.resultTo = null; } if (app.pid > 0 && app.pid != MY_PID) { Process.killProcess(app.pid); } cleanUpApplicationRecordLocked(app, false, i); i--; //dump(); } |
步驟四,上面 3 個過程都是針對整個 process 進行的資源回收。在以上過程執行完畢之後,將在更小的粒度上對 Activity 的資源進行回收。與上面所述類似,列表 mLRUActivities 儲存了當前所有執行中的 Activity,排序規則同樣為最少訪問原則。mLRUActivities.size() 返回系統中執行的 Activity 的數量,當其大於 MAX_ACTIVITIES(MAX_ACTIVITIES 是一個常量,一般值為 20,代表系統中最大允許同時存在的 Activity)時。將回收部分滿足條件的 Activity 以減少記憶體的使用。回收條件程式碼清單 7 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//Finally, if there are too many activities now running, try to // finish as many as we can to get back down to the limit. for ( i=0; i<mLRUActivities.size() && mLRUActivities.size() > curMaxActivities; i++) { final HistoryRecord r = (HistoryRecord)mLRUActivities.get(i); // We can finish this one if we have its icicle saved and // it is not persistent. if ((r.haveState || !r.stateNotNeeded) && !r.visible && r.stopped && !r.persistent && !r.finishing) { final int origSize = mLRUActivities.size(); destroyActivityLocked(r, true); if (origSize > mLRUActivities.size()) { i--; } } } |
這裡回收的只是 Activity 的記憶體資源,並不會殺死程式,也不會影響程式的執行。當程式需要呼叫被殺掉的 Activity 時,可以從儲存的狀態中回覆,當然可能需要相對長一點的時延。
上面提到,trimApplications() 函式中會執行一個叫做 updateOomAdjLocked() 的函式,如果返回 false,則執行預設回收,若返回 true 則不執行預設記憶體回收。updateOomAdjLocked 將針對每一個程式更新一個名為 adj 的變數,並將其告知 Linux 核心,核心維護一個包含 adj 的資料結構(即程式表),並通過 lowmemorykiller 檢查系統記憶體的使用情況,在記憶體不足的情況下殺死一些程式並釋放記憶體。下面將對這種 Android Framework 與 Linux 核心相配合的記憶體回收機制進行研究。
由於 Android 作業系統中的所有應用程式都執行在獨立的 Dalvik 虛擬機器環境中,Linux 核心無法獲知每個程式的執行狀態,也就無法為每個程式維護一個合適的 adj 值,因此,Android Application Framework 中必須提供一套機制以動態的更新每個程式的 adj。這就是 updateOomAdjLocked()。
updateOomAdjLocked() 位於 ActivityManagerService 中,其主要作用是為程式選擇一個合適的 adj 值,並通知 Linux 核心更新這個值。updateOomAdjLocked 首先呼叫 computeOomAdjLocked() 初步計算 adj 的值,然後回到 updateOomAdjLocked() 對其值進行進一步修正。估算流程可參見程式碼。
關於 adj,其定義在 task_struct->signal_struct->adj, 檔案 /kernel/include/linux/sched.h 中。實質為程式資料結構中的一個變數,用來表示發生 Out of Memory 時殺死程式的優先順序順序。lowmemorykiller 利用這個變數對程式的重要程度進行判斷,並在記憶體不足時釋放部分空間,其實現在檔案 /kernel/drivers/staging/android/lowmemorykiller.c 中。lowmemorykiller 定義了兩個陣列:lowmem_adj 和 lowmem_minfree。其中 lowmen_adj 定義了一系列 adj 鍵值,而 lowmem_minfree 的每個元素代表一個記憶體閾值。如下程式碼中四個閾值分別是 6MB,8MB,16MB 和 64MB,分別代表當記憶體小於 64MB 時,adj 大於或等於 12 的那些程式將被殺死並回收,記憶體小於 16MB 時,adj 大於等於 6 的那些程式將被殺死並回收,記憶體小於 8MB 時,adj 大於等於 1 的那些程式將被殺死並回收,記憶體小於 6MB 時,adj 大於等於 0 的所有程式將被殺死並回收。核心中的每個程式都持有一個 adj,取值範圍 -17 到 15,值越小代表程式的重要性越高,回收優先順序越低,其中 -17 代表禁用自動回收。Android 系統中,只有 0-15 被使用。
清單 8. 每個程式都持有一個 adj
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static int lowmem_adj[6] = { 0, 1, 6, 12, }; static int lowmem_adj_size = 4; static size_t lowmem_minfree[6] = { 3 * 512, /* 6MB */ 2 * 1024, /* 8MB */ 4 * 1024, /* 16MB */ 16 * 1024, /* 64MB */ }; static int lowmem_minfree_size = 4; |
lowmemorykiller 註冊一個 lowmem_shrinker,lowmem_shrinker 利用了標準 Linux 核心中的 Cache Shrinker 來實現,當空閒記憶體頁面不足時,核心執行緒 kswapd 將用已註冊的 lowmem_shrinker 來回收記憶體頁面。
清單 9. 用已註冊的 lowmem_shrinker 來回收記憶體頁面
1 2 3 4 5 6 7 8 9 10 11 |
static struct shrinker lowmem_shrinker = { .shrink = lowmem_shrink, .seeks = DEFAULT_SEEKS * 16 }; static int __init lowmem_init(void) { task_free_register(&task_nb); register_shrinker(&lowmem_shrinker); return 0; } |
lowmem_shrink 的程式碼在函式 lowmem_shrink 中,下面給出該函式的主要結構。lowmem_shrink 根據上述規則遍歷所有程式,選出需要結束的程式,通過傳送一個無法忽略的訊號 SIGKILL 強制結束這些程式
清單 10. 強制結束程式
1 2 3 4 5 6 7 8 9 10 11 12 |
static int lowmem_shrink(struct shrinker *s, int nr_to_scan, gfp_t gfp_mask) { for_each_process(p) { //Select processes to be forced } if (selected) { force_sig(SIGKILL, selected); rem -= selected_tasksize; } else rem = -1; return rem; } |
如果上述各種方法都無法釋放出足夠的記憶體空間,那麼當為新的程式分配應用程式時將發生 Out of Memory 異常,OOM_killer 將盡最後的努力殺掉一些程式來釋放空間。Android 中的 OOM_killer 繼承自標準 Linux 2.6 核心,用於分配記憶體時 Out of Memory 的處理。Android 並沒有對其實現方式進行修改。其位置在 linux/mm/oom_kill.c。 oom_killer 遍歷程式,並計算所有程式的 badness 值,選擇 badness 最大的那個程式將其殺掉。函式 badness 的宣告如下:
unsigned long badness(struct task_struct *p, unsigned long uptime) 函式 select_bad_process 返回將要殺掉的那個程式。
1 2 3 4 5 6 7 8 9 10 11 12 |
static struct task_struct *select_bad_process(unsigned long *ppoints, struct mem_cgroup *mem) { for_each_process(p) { points = badness(p, uptime.tv_sec); if (points > *ppoints || !chosen) { chosen = p; *ppoints = points; } } return chosen; } |
最後,和 lowmemorykiller 一樣,通過傳送 SIGKILL 結束選中的程式。由於 oom_killer 與標準 Linux 核心並無不同,這裡不再詳細研究。
本文研究了 Android 作業系統上的記憶體回收機制。主要包括 Application Framework 層的預設回收以及 Linux 核心中的 lowmemorykiller、OOM_killer。一般來說應用開發者並不需要控制或者修改系統的記憶體管理以及回收,但是深入理解這些系統級的管理機制還是必要的,尤其有助於更加合理地設計應用程式,使應用程式的程式在其生命週期內高效地執行。而系統級開發者如果想要對記憶體管理機制進行優化,對原有機制的理解則是必不可少的重要前提。