解讀Android進程優先順序ADJ演算法
來自專欄 Android達摩院24 人贊了文章
原文來自我的微信公眾號: Android達摩院,歡迎大家關注。 本文基於最新的Android P源碼來解讀進程優先順序ADJ原理,基於篇幅會精鍊部分代碼
一、概述
1.1 進程
Android框架對進程創建與管理進行了封裝,對於APP開發者只需知道Android四大組件的使用。當Activity, Service, ContentProvider, BroadcastReceiver任一組件啟動時,當其所承載的進程存在則直接使用,不存在則由框架代碼自動調用startProcessLocked創建進程。一個APP可以擁有多個進程,多個APP也可以運行在同一個進程,通過配置Android:process屬性來決定。所以說對APP來說進程幾乎是透明的,但了解進程對於深刻理解Android系統是至關關鍵的。
1.2 優先順序
Android系統的設計理念正是希望應用進程能盡量長時間地存活,以提升用戶體驗。應用首次打開比較慢,這個過程有進程創建以及Application等信息的初始化,所以應用在啟動之後,即便退到後台並非立刻殺死,而是存活一段時間,這樣下次再使用則會非常快。對於APP同樣希望自身儘可能存活更長的時間,甚至探索各種保活黑科技。物極必反,系統處於低內存的狀態下,手機性能會有所下降;系統繼續放任所有進程一直存活,系統內存很快就會枯竭而亡,那麼需要合理地進程回收機制。
到底該回收哪個進程呢?系統根據進程的組件狀態來決定每個進程的優先順序值ADJ,系統根據一定策略先殺優先順序最低的進程,然後逐步殺優先順序更低的進程,依此類推,以回收預期的可用系統資源,從而保證系統正常運轉。
談到優先順序,可能有些人會想到Linux進程本身有nice值,這個能決定CPU資源調度的優先順序;而本文介紹Android系統中的ADJ,主要決定在什麼場景下什麼類型的進程可能會被殺,影響的是進程存活時間。ADJ與nice值兩者定位不同,不過也有一定的聯繫,優先順序很高的進程,往往也是用戶不希望被殺的進程,是具有有一定正相關性。
1.3 ADJ級別
從Android 7.0開始,ADJ採用100、200、300,老版本採用的是個位數。為了防止剩餘內存過低,Android在內核空間有lowmemorykiller(簡稱LMK),LMK是通過註冊shrinker來觸發低內存回收的,這個機制並不太優雅,可能會拖慢Shrinkers內存掃描速度,已從內核4.12中移除,後續會採用用戶空間的LMKD + memory cgroups機制,這裡先不展開LMK講解。
進程剛啟動時ADJ等於INVALID_ADJ,當執行完attachApplication(),該該進程的curAdj和setAdj不相等,則會觸發執行setOomAdj()將該進程的節點/proc/pid
/oom_score_adj寫入oomadj值。閾值設置過程會根據手機屏幕尺寸、內存大小來調整,下圖參數為64位機器的Android原生閾值圖:
LMK殺進程過程:當系統剩餘空閑內存低於某閾值(比如147MB),則從ADJ大於或等於相應閾值(比如900)的進程中,選擇ADJ值最大的進程,如果存在多個ADJ相同的進程,則選擇內存最大的進程,讓向目標進程發出signal 9來殺掉該進程。
二、解讀ADJ
接下來,解讀每個ADJ值都對應著怎樣條件的進程,包括正在運行的組件以及這些組件的狀態幾何。這裡重點介紹上圖標紅的ADJ級別所對應的進程。
Android系統中計算各進程ADJ演算法的核心方法:
- updateOomAdjLocked:更新adj,當目標進程為空或者被殺則返回false;否則返回true;
- computeOomAdjLocked:計算adj,返回計算後RawAdj值;
- applyOomAdjLocked:應用adj,當需要殺掉目標進程則返回false;否則返回true。
當Android四大組件狀態改變時會updateOomAdjLocked()來同步更新相應進程的ADJ優先順序。這裡需要說明一下,當同一個進程有多個決定其優先順序的組件狀態時,取優先順序最高的ADJ作為最終的ADJ。另外,進程會通過設置maxAdj來限定ADJ的上限。
關於分析進程ADJ相關信息,常用命令如下:
- dumpsys meminfo,
- dumpsys activity o
- dumpsys activity p
本文重點介紹上面ADJ圖中標紅的級別,對於非標紅級別這裡簡單說明一下:
ADJ<0優先順序說明:
- NATIVE_ADJ(-1000):是由init進程fork出來的Native進程,並不受system管控;
- SYSTEM_ADJ(-900):是指system_server進程;
- PERSISTENT_PROC_ADJ(-800): 是指在AndroidManifest.xml中申明android:persistent=」true」的系統(即帶有FLAG_SYSTEM標記)進程,persistent進程一般情況並不會被殺,即便被殺或者發生Crash系統會立即重新拉起該進程。
- PERSISTENT_SERVICE_ADJ(-700):是由startIsolatedProcess()方式啟動的進程,或者是由system_server或者persistent進程所綁定(並且帶有BIND_ABOVE_CLIENT或者BIND_IMPORTANT)的服務進程
其他優先順序:
- BACKUP_APP_ADJ(300):執行bindBackupAgent()過程的進程
- HEAVY_WEIGHT_APP_ADJ(400): realStartActivityLocked()過程,當應用的privateFlags標識PRIVATE_FLAG_CANT_SAVE_STATE的進程;
- HOME_APP_ADJ(600):當類型為ACTIVITY_TYPE_HOME的應用,比如桌面APP
- PREVIOUS_APP_ADJ(700):用戶上一個使用的APP進程
2.1 FOREGROUND_APP_ADJ(0)
場景1:滿足以下任一條件的進程都屬於FOREGROUND_APP_ADJ(0)優先順序:
- 正處於resumed狀態的Activity
- 正執行一個生命周期回調的Service(比如執行onCreate,onStartCommand, onDestroy等)
- 正執行onReceive()的廣播接收者
- 通過startInstrumentation()啟動的進程
源碼如下:
場景2: 當客戶端進程activity裡面調用bindService()方法時flags帶有BIND_ADJUST_WITH_ACTIVITY參數,並且該activity處於可見狀態,則當前服務進程也屬於前台進程,源碼如下:
provider客戶端
場景3: 對於provider進程,還有以下兩個條件能成為前台進程:
- 當Provider的客戶端進程ADJ<=FOREGROUND_APP_ADJ時,則Provider進程ADJ等於FOREGROUND_APP_ADJ
- 當Provider有外部(非框架)進程依賴,也就是調用了getContentProviderExternal()方法,則ADJ至少等於FOREGROUND_APP_ADJ
2.2 VISIBLE_APP_ADJ(100)
可見進程:當ActivityRecord的visible=true,也就是Activity可見的進程。
從Android P開始,進一步細化ADJ級別,增加了VISIBLE_APP_LAYER_MAX(99),是指VISIBLE_APP_ADJ(100)跟PERCEPTIBLE_APP_ADJ(200)之間有99個槽,則可見級別ADJ的取值範圍為[100,199]。 演算法會根據其所在task的mLayerRank來調整其ADJ,100加上mLayerRank就等於目標ADJ,layer越大,則ADJ越小。 關於TaskRecord的mLayerRank的計算方式是ASS的rankTaskLayersIfNeeded()方法,當TaskRecord頂部的ActivityRecord為空或者結束或者不可見時,則設置該TaskRecord的mLayerRank等於-1; 每個ActivityDisplay的baseLayer都是從0開始,從最上面的TaskRecord開始,第一個ADJ=100,從上至下依次加1,直到199為上限。
service客戶端
ServiceRecord的成員變數startRequested=true,是指被顯式調用了startService()方法。當service被stop或kill會將其置為false。
一般情況下,即便客戶端進程處於前台進程(ADJ=0)級別,服務進程只會提升到可見(ADJ=1)級別。以下flags是由調用bindService()過程所傳遞的flags來決定的。
作為工程師很多時候可能還是想看看源碼,show me the code。但是關於ADJ計算這一塊源碼場景computeOomAdjLocked(),Google真心寫得比較亂,為了更清晰地說明客戶端進程如何影響服務進程,在保證不失去原意的情況下重寫了這塊部分邏輯:
這個過程主要根據service本身、client端情況以及activity狀態分別來調整adj和schedGroup
上段代碼說明服務端進程優先順序(adj)不會低於客戶端進程優先順序(newAdj),而newAdj的上限受限於flags,具體服務端進程受客戶端進程影響的ADJ上限如下:
- BIND_ABOVE_CLIENT或BIND_IMPORTANT的情況下,ADJ上限為PERSISTENT_SERVICE_ADJ;
- BIND_NOT_VISIBLE的情況下, ADJ上限為PERCEPTIBLE_APP_ADJ;
- 否則,一般情況下,ADJ上限為VISIBLE_APP_ADJ;
由此,可見當bindService過程帶有BIND_ABOVE_CLIENT或者BIND_IMPORTANT flags的同時,客戶端進程ADJ小於或等於PERSISTENT_SERVICE_ADJ的情況下,該進程則為PERSISTENT_SERVICE_ADJ。另外,即便是啟動過Activity的進程,當客戶端進程ADJ<=200時,還是可以提升該服務進程的優先順序。
2.3 PERCEPTIBLE_APP_ADJ(200)
可感知進程:當該進程存在不可見的Activity,但Activity正處於PAUSING、PAUSED、STOPPING狀態,則為PERCEPTIBLE_APP_ADJ
滿足以下任一條件的進程也屬於可感知進程:
- foregroundServices非空:前台服務進程,執行startForegroundService()方法
- app.forcingToImportant非空:執行setProcessImportant()方法,比如Toast彈出過程。
- hasOverlayUi非空:非activity的UI位於屏幕最頂層,比如顯示類型TYPE_APPLICATION_OVERLAY的窗口。
2.4 SERVICE_ADJ(500)
服務進程:沒有啟動過Activity,並且30分鐘之內活躍過的服務進程。 startRequested為true,則代表執行startService()且沒有stop的進程。
2.5 SERVICE_B_ADJ(800)
進程由SERVICE_ADJ(500)降低到SERVICE_B_ADJ(800),有以下兩種情況:
- A類Service佔比過高:當A類Service個數 > Service總數的1/3時,則加入到B類Service。換句話說,B Service的個數至少是A Service的2倍。
- 內存緊張&&A類Service佔用內存較高:當系統內存緊張級別(mLastMemoryLevel)高於ADJ_MEM_FACTOR_NORMAL,且該應用所佔內存lastPss大於或等於CACHED_APP_MAX_ADJ級別所對應的內存閾值的1/3(默認值閾值約等於110MB)。
源碼如下:
ADJ_MEM_FACTOR
這裡順便一下,內存因子ADJ_MEM_FACTOR共有4個級別,當前處於哪個內存因子級別,取決於當前進程中cached進程和空進程的個數。
ADJ內存因子:決定允許後台運行Jobs的最大上限,以及決定TrimMemory的級別(包括ThreadedRenderer的回收級別),再進一步來看看內存因子:
再來看看cached和empty進程:
用於限制empty或cached進程的上限為16個,並且empty超過8個時會清理掉30分鐘沒有活躍的進程。 cached和empty主要是區別是否有Activity。
2.6 CACHED_APP_MIN_ADJ(900)
緩存進程優先順序從CACHED_APP_MIN_ADJ(900)到 CACHED_APP_MAX_ADJ(906)。
ADJ的轉換演算法:先計算出從900~906之間,cached進程和empty的numSlots=3,也就是各有3個槽; 然後分別根據cached進程和empty進程個數除以numSlots來確定單個槽的進程個數,也就是emptyFactor和cachedFactor,計算過程 每次adj加2,分布如下圖:
則cached進程的adj分布在900, 901, 903, 905,empty進程的adj分布在900, 902, 904, 906。
cached進程是指:
- 存在Activity且該Activity窗口不可見,並且不處於PAUSING、PAUSED、STOPPING的任一狀態的情況下,則設置該進程為PROCESS_STATE_CACHED_ACTIVITY;
- 當該進程Service的客戶端進程存在Activity或者是treatLikeActivity的進程
empty進程往往是指沒有activity的低優先順序進程。
三、總結
Android進程優先順序ADJ的每一個ADJ級別往往都有多種場景,使用adjType完美地區分相同ADJ下的不同場景; 不同ADJ進程所對應的schedGroup不同,從而分配的CPU資源也不同,schedGroup大體分為TOP(T)、前台(F)、後台(B); ADJ跟AMS中的procState有著緊密的聯繫。
- adj:通過調整oom_score_adj來影響進程壽命(LMK殺進程策略);
- schedGroup:影響進程的CPU資源調度與分配;
- procState:從進程所包含的四大組件運行狀態來評估進程狀態,影響framework的內存控制策略。比如控制緩存進程和空進程個數上限依賴於procState,再比如控制APP執行handleLowMemory()的觸發時機等。
為了說明整體關係,以ADJ為中心來講解跟adjType,schedGroup,procState的對應關係,下面以一幅圖來詮釋整個ADJ演算法的精髓,幾乎涵蓋了ADJ演算法調整的絕大多數場景。
CPU調度組:
- 常說的前台進程與後台進程,其實是從CPU調度角度來劃分的前台與後台;為了讓用戶正在使用的TOP進程能分配到更多的CPU資源,從Android 6.0開始新增了TOP進程組,CPU調度優先分配給當前正在跟用戶交互的APP,提升用戶體驗。
- 上圖adjType=」broadcast」的CPU調度組的選擇取決於廣播隊列,當receiver接收到的廣播來自於前台廣播隊列則採用前台進程組,當receiver接收到的廣播來自於後台廣播隊列則採用後台進程組。前後台廣播隊列的CPU資源調度優先順序不同,所以前台廣播超時10秒就會ANR,而後台廣播超時60秒才會ANR。更少的CPU資源分配就需要更長的時間來完成執行,這也就是為何兩個廣播隊列定義了不同的超時閾值。
- 上圖adjType=」exec-service」的CPU調度組的選擇取決於caller, 當發起bindService或者startService的調用者caller屬於後台進程組,callerFg=false,則Service的生命周期回調運行在後台進程組,非常很少的CPU資源;當caller屬於前台或者TOP進程組,則Service的生命周期回調運行在前台進程組,分配較多的CPU資源。
- 上圖adjType=」service」也有機會選擇TOP組, 前提條件是在bindService的時候帶有BIND_IMPORTANT的flags,用於標記該服務對於客戶端進程很重要。
最後,給廣大應用開發者一些友好的建議:
- UI進程與Service進程一定要分離,因為對於包含activity的service進程,一旦進入後台就成為」cch-started-ui-services」類型的cache進程(ADJ>=900),隨時可能會被系統回收;而分離後的Service進程服務屬於SERVICE_ADJ(500),被殺的可能性相對較小。尤其是系統允許自啟動的服務進程必須做UI分離,避免消耗系統較大內存。
- 只有真正需要用戶可感知的應用,才調用startForegroundService()方法來啟動前台服務,此時ADJ=PERCEPTIBLE_APP_ADJ(200),常駐內存,並且會在通知欄常駐通知提醒用戶,比如音樂播放,地圖導航。切勿為了常駐而濫用前台服務,這會嚴重影響用戶體驗。
- 進程中的Service工作完成後,務必主動調用stopService或stopSelf來停止服務,避免佔據內存,浪費系統資源;
- 不要長時間綁定其他進程的service或者provider,每次使用完成後應立刻釋放,避免其他進程常駐於內存;
- APP應該實現介面onTrimMemory()和onLowMemory(),根據TrimLevel適當地將非必須內存在回調方法中加以釋放。當系統內存緊張時會回調該介面,減少系統卡頓與殺進程頻次。
推薦閱讀:
※對WiFi工作原理和信號範圍的一些問題?
※為何越來越多的安卓軟體不再支持低版本的安卓系統?
※數庫科技 Android 開發準則
※現在有哪些型號的電視支持airplay同屏播放?
※Android App 中支持 GCM 推送的有哪些?