Android程序保活-自“裁”或者耍流氓
本篇文章是後臺殺死系列的最後一篇,主要探討一下程序的保活,Android本身設計的時候是非常善良的,它希望程序在不可見或者其他一些場景下APP要懂得主動釋放,可是Android低估了”貪婪“,尤其是很多國產APP,只希望索取來提高自己的效能,不管其他APP或者系統的死活,導致了很嚴重的資源浪費,這也是Android被iOS詬病的最大原因。本文的保活手段也分兩種:遵紀守法的程序保活與流氓手段換來的程序保活。
宣告:堅決反對流氓手段實現程序保活 堅決反對流氓程序保活 堅決反對流氓程序保活 “請告訴產品:無法進入白名單”
- 正常守法的程序保活:記憶體裁剪(好學生APP要使用)
- 流氓的程序保活,提高優先順序(好學生APP別用)
- 流氓的程序保活,雙Service程序相互喚醒(binder訃告原理)(好學生APP別用)
針對LowmemoryKiller所做的程序保活
LowmemoryKiller會在記憶體不足的時候掃描所有的使用者程序,找到不是太重要的程序殺死,至於LowmemoryKiller殺程序夠不夠狠,要看當前的記憶體使用情況,記憶體越少,下手越狠。在核心中,LowmemoryKiller.c定義了幾種記憶體回收等級如下:(也許不同的版本會有些不同)
static short lowmem_adj[6] = { 0, 1, 6, 12, }; static int lowmem_adj_size = 4; static int lowmem_minfree[6] = { 3 * 512, /* 6MB */ 2 * 1024, /* 8MB */ 4 * 1024, /* 16MB */ 16 * 1024, /* 64MB */ }; static int lowmem_minfree_size = 4;
lowmem_adj中各項數值代表閾值的警戒級數,lowmem_minfree代表對應級數的剩餘記憶體,兩者一一對應,比如當系統的可用記憶體小於6MB時,警戒級數為0;當系統可用記憶體小於8M而大於6M時,警戒級數為1;當可用記憶體小於64M大於16MB時,警戒級數為12。LowmemoryKiller就是根據當前系統的可用記憶體多少來獲取當前的警戒級數,如果程序的oom_adj大於警戒級數並且佔記憶體最大,將會被優先殺死, 具有相同omm_adj的程序,則殺死佔用記憶體較多的。omm_adj越小,代表程序越重要。一些前臺的程序,oom_adj會比較小,而後臺的服務,omm_adj會比較大,所以當記憶體不足的時候,Lowmemorykiller先殺掉的是後臺服務而不是前臺的程序。對於LowmemoryKiller的殺死,這裡有一句話很重要,就是: 具有相同omm_adj的程序,則殺死佔用記憶體較多的
static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
...
<!--關鍵點1 獲取free記憶體狀況-->
int other_free = global_page_state(NR_FREE_PAGES);
int other_file = global_page_state(NR_FILE_PAGES);
<!--關鍵點2 找到min_adj -->
for(i = 0; i < array_size; i++) {
if (other_free < lowmem_minfree[i] &&
other_file < lowmem_minfree[i]) {
min_adj = lowmem_adj[i];
break;
}
}
<!--關鍵點3 找到p->oomkilladj>min_adj並且oomkilladj最大,記憶體最大的程序-->
for_each_process(p) {
// 找到第一個大於等於min_adj的,也就是優先順序比閾值低的
if (p->oomkilladj < min_adj || !p->mm)
continue;
// 找到tasksize這個是什麼呢
tasksize = get_mm_rss(p->mm);
if (tasksize <= 0)
continue;
if (selected) {
// 找到優先順序最低,並且記憶體佔用大的
if (p->oomkilladj < selected->oomkilladj)
continue;
if (p->oomkilladj == selected->oomkilladj &&
tasksize <= selected_tasksize)
continue;
}
selected = p;
selected_tasksize = tasksize;
lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
p->pid, p->comm, p->oomkilladj, tasksize);
}
if(selected != NULL) {...
force_sig(SIGKILL, selected);
}
return rem;
}
這裡先看一下關鍵點1,這裡是核心獲取當前的free記憶體狀況,並且根據當前空閒記憶體計算出當前後臺殺死的等級(關鍵點2),之後LowmemoryKiller會遍歷所有的程序,找到優先順序低並且記憶體佔用較大的程序,如果這個程序的p->oomkilladj>min_adj,就表示這個程序可以殺死,LowmemoryKiller就會送過傳送SIGKILL訊號殺死就程序,注意,lmkd會先找優先順序低的程序,如果多個程序優先順序相同,就優先殺死記憶體佔用高的程序,這樣就為我們提供了兩種程序包活手段:
- 1、提高程序的優先順序,其實就是減小程序的p->oomkilladj(越小越重要)
- 2、降低APP的記憶體佔用量,在oom_adj相同的時候,會優先幹掉記憶體消耗大的程序
不過大多數情況下,Android對於程序優先順序的管理都是比較合理,即使某些場景需要特殊手段提高優先順序,Android也是給了參考方案的,比如音訊播放,UI隱藏的時候,需要將Sevice程序設定成特定的優先順序防止被後臺殺死,比如一些備份的程序也需要一些特殊處理,但是這些都是在Android允許的範圍內的,所以絕大多數情況下,Android是不建議APP自己提高優先順序的,因為這會與Android自身的的程序管理相悖,換句話說就是耍流氓。這裡先討論第二種情況,通過合理的釋放記憶體降低被殺的風險,地主不想被殺,只能交公糧,自裁保身,不過這裡也要看自裁的時機,什麼時候瘦身比較划算,O(∩_∩)O哈哈~!這裡就牽扯到有一個onTrimeMemory函式,該函式是一個系統回撥函式,主要是Android系統經過綜合評估,給APP一個記憶體裁剪的等級,比如當記憶體還算充足,APP退回後臺時候,會收到TRIM_MEMORY_UI_HIDDEN等級的裁剪,就是告訴APP,釋放一些UI資源,比如大量圖片記憶體,一些引入圖片瀏覽快取的場景,可能就更加需要釋放UI資源,下面來看下onTrimeMemory的回撥時機及APP應該做出相應處理。
onTrimeMemory的回撥時機及記憶體裁剪等級
OnTrimMemory是在Android 4.0引入的一個回撥介面,其主要作用就是通知應用程式在不同的場景下進行自我瘦身,釋放記憶體,降低被後臺殺死的風險,提高使用者體驗,由於目前APP的適配基本是在14之上,所以不必考慮相容問題。onTrimeMemory支援不同裁剪等級,比如,APP通過HOME建進入後臺時,其優先順序(oom_adj)就發生變化,從未觸發onTrimeMemory回撥,這個時候系統給出的裁剪等級一般是TRIM_MEMORY_UI_HIDDEN,意思是,UI已經隱藏,UI相關的、佔用記憶體大的資源就可以釋放了,比如大量的圖片快取等,當然,還會有其他很多場景對應不同的裁剪等級。因此,需要弄清楚兩個問題:
- 1、不同的裁剪等級是如何生成的,其意義是什麼
- 2、APP如何根據不同的裁剪等級釋放記憶體資源,(自裁的程度)
先看下ComponentCallbacks2中定義的不同裁剪等級的意義:這裡一共定義了4+3共7個裁剪等級,為什麼說是4+3呢?因為有4個是針對後臺程序的,還有3個是針對前臺(RUNNING)程序的,目標物件不同,具體看下分析
裁剪等級 | 數值 | 目標程序 |
---|---|---|
TRIM_MEMORY_COMPLETE | 80 | 後臺程序 |
TRIM_MEMORY_MODERATE | 60 | 後臺程序 |
TRIM_MEMORY_BACKGROUND | 40 | 後臺程序 |
TRIM_MEMORY_UI_HIDDEN | 20 | 後臺程序 |
TRIM_MEMORY_RUNNING_CRITICAL | 15 | 前臺RUNNING程序 |
TRIM_MEMORY_RUNNING_LOW | 10 | 前臺RUNNING程序 |
TRIM_MEMORY_RUNNING_MODERATE | 5 | 前臺RUNNING程序 |
其意義如下:
TRIM_MEMORY_UI_HIDDEN 當前應用程式的所有UI介面不可見,一般是使用者點選了Home鍵或者Back鍵,導致應用的UI介面不可見,這時應該釋放一些UI相關資源,TRIM_MEMORY_UI_HIDDEN是使用頻率最高的裁剪等級。官方文件:the process had been showing a user interface, and is no longer doing so. Large allocations with the UI should be released at this point to allow memory to be better managed
TRIM_MEMORY_BACKGROUND 當前手機目前記憶體吃緊(後臺程序數量少),系統開始根據LRU快取來清理程序,而該程式位於LRU快取列表的頭部位置,不太可能被清理掉的,但釋放掉一些比較容易恢復的資源能夠提高手機執行效率,同時也能保證恢復速度。官方文件:the process has gone on to the LRU list. This is a good opportunity to clean up resources that can efficiently and quickly be re-built if the user returns to the app
TRIM_MEMORY_MODERATE 當前手機目前記憶體吃緊(後臺程序數量少),系統開始根據LRU快取來清理程序,而該程式位於LRU快取列表的中間位置,應該多釋放一些記憶體提高執行效率。官方文件:the process is around the middle of the background LRU list; freeing memory can help the system keep other processes running later in the list for better overall performance.
TRIM_MEMORY_COMPLETE 當前手機目前記憶體吃緊 (後臺程序數量少),系統開始根據LRU快取來清理程序,而該程式位於LRU快取列表的最邊緣位置,系統會先殺掉該程序,應盡釋放一切可以釋放的記憶體。官方文件:the process is nearing the end of the background LRU list, and if more memory isn’t found soon it will be killed.
以下三個等級針對前臺執行應用
TRIM_MEMORY_RUNNING_MODERATE 表示該程序是前臺或可見程序,正常執行,一般不會被殺掉,但是目前手機有些吃緊(後臺及空程序存量不多),系統已經開始清理記憶體,有必要的話,可以釋放一些記憶體。官方文件:the process is not an expendable background process, but the device is running moderately low on memory. Your running process may want to release some unneeded resources for use elsewhere。
TRIM_MEMORY_RUNNING_LOW 表示該程序是前臺或可見程序,正常執行,一般不會被殺掉,但是目前手機比較吃緊(後臺及空程序被全乾掉了一大波),應該去釋放掉一些不必要的資源以提升系統性能。 官方文件:the process is not an expendable background process, but the device is running low on memory. Your running process should free up unneeded resources to allow that memory to be used elsewhere.
TRIM_MEMORY_RUNNING_CRITICAL 表示該程序是前臺或可見程序,但是目前手機比較記憶體十分吃緊(後臺及空程序基本被全乾掉了),這時應當儘可能地去釋放任何不必要的資源,否則,系統可能會殺掉所有快取中的程序,並且殺一些本來應當保持執行的程序。官方文件:the process is not an expendable background process, but the device is running extremely low on memory and is about to not be able to keep any background processes running. Your running process should free up as many non-critical resources as it can to allow that memory to be used elsewhere. The next thing that will happen after this is called to report that nothing at all can be kept in the background, a situation that can start to notably impact the user.
以上抽象的說明了一下Android既定引數的意義,下面看一下onTrimeMemory回撥的時機及原理,這裡採用6.0的程式碼分析,因為6.0比之前4.3的程式碼清晰很多:當用戶的操作導致APP優先順序發生變化,就會呼叫updateOomAdjLocked去更新程序的優先順序,在更新優先順序的時候,會掃描一遍LRU程序列表, 重新計算程序的oom_adj,並且參考當前系統狀況去通知程序裁剪記憶體(這裡只是針對Android Java層APP),這次操作一般發生在開啟新的Activity介面、退回後臺、應用跳轉切換等等,updateOomAdjLocked程式碼大概600多行,比較長,儘量精簡後如下,還是比較長,這裡拆分成一段段梳理:
final void updateOomAdjLocked() {
final ActivityRecord TOP_ACT = resumedAppLocked();
<!--關鍵點1 找到TOP——APP,最頂層顯示的APP-->
final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
final long oldTime = SystemClock.uptimeMillis() - ProcessList.MAX_EMPTY_TIME;
mAdjSeq++;
mNewNumServiceProcs = 0;
final int emptyProcessLimit;
final int hiddenProcessLimit;
<!--關鍵點2 找到TOP——APP,最頂層顯示的APP-->
// 初始化一些程序數量的限制:
if (mProcessLimit <= 0) {
emptyProcessLimit = hiddenProcessLimit = 0;
} else if (mProcessLimit == 1) {
emptyProcessLimit = 1;
hiddenProcessLimit = 0;
} else {
// 空程序跟後臺非空快取繼承的比例
emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
cachedProcessLimit = mProcessLimit - emptyProcessLimit;
}
<!--關鍵點3 確定下程序槽 3個槽->
int numSlots = (ProcessList.HIDDEN_APP_MAX_ADJ - ProcessList.HIDDEN_APP_MIN_ADJ + 1) / 2;
// 後臺程序/前臺程序/空程序
int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
int emptyFactor = numEmptyProcs/numSlots;
if (emptyFactor < 1) emptyFactor = 1;
int hiddenFactor = (mNumHiddenProcs > 0 ? mNumHiddenProcs : 1)/numSlots;
if (hiddenFactor < 1) hiddenFactor = 1;
int stepHidden = 0;
int stepEmpty = 0;
int numHidden = 0;
int numEmpty = 0;
int numTrimming = 0;
mNumNonHiddenProcs = 0;
mNumHiddenProcs = 0;
int i = mLruProcesses.size();
// 優先順序
int curHiddenAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
// 初始化的一些值
int nextHiddenAdj = curHiddenAdj+1;
// 優先順序
int curEmptyAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
// 有意思
int nextEmptyAdj = curEmptyAdj+2;
這前三個關鍵點主要是做了一些準備工作,關鍵點1 是單獨抽離出TOP_APP,因為它比較特殊,系統只有一個前天程序,關鍵點2主要是根據當前的配置獲取後臺快取程序與空程序的數目限制,而關鍵點3是將後臺程序分為三備份,無論是後臺程序還是空程序,會間插的均分6個優先順序,一個優先順序是可以有多個程序的,而且並不一定空程序的優先順序小於HIDDEN程序優先順序。
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
if (!app.killedByAm && app.thread != null) {
app.procStateChanged = false;
<!--關鍵點4 計算程序的優先順序或者快取程序的優先順序->
// computeOomAdjLocked計算程序優先順序,但是對於後臺程序和empty程序computeOomAdjLocked無效,這部分優先順序是AMS自己根據LRU原則分配的
computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
//還未最終確認,有些程序的優先順序,比如只有後臺activity或者沒有activity的程序,
<!--關鍵點5 計算程序的優先順序或者快取程序的優先順序->
if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
switch (app.curProcState) {
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
app.curRawAdj = curCachedAdj;
<!--關鍵點6 根據LRU為後臺程序分配優先順序-->
if (curCachedAdj != nextCachedAdj) {
stepCached++;
if (stepCached >= cachedFactor) {
stepCached = 0;
curCachedAdj = nextCachedAdj;
nextCachedAdj += 2;
if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
}
}
}
break;
default:
<!--關鍵點7 根據LRU為後臺程序分配優先順序-->
app.curRawAdj = curEmptyAdj;
app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
if (curEmptyAdj != nextEmptyAdj) {
stepEmpty++;
if (stepEmpty >= emptyFactor) {
stepEmpty = 0;
curEmptyAdj = nextEmptyAdj;
nextEmptyAdj += 2;
if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
}
}
}
break;
}
}
<!--關鍵點8 設定優先順序-->
applyOomAdjLocked(app, true, now, nowElapsed);
上面的這幾個關鍵點主要是為所有程序計算出其優先順序oom_adj之類的值,對於非後臺程序,比如HOME程序 服務程序,備份程序等都有自己的獨特的計算方式,而剩餘的後臺程序就根據LRU三等分配優先順序。
<!--關鍵點9 根據快取程序的數由AMS選擇性殺程序,後臺程序太多-->
switch (app.curProcState) {
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
mNumCachedHiddenProcs++;
numCached++;
if (numCached > cachedProcessLimit) {
app.kill("cached #" + numCached, true);
}
break;
case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
if (numEmpty > ProcessList.TRIM_EMPTY_APPS
&& app.lastActivityTime < oldTime) {
app.kill("empty for "
+ ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
/ 1000) + "s", true);
} else {
numEmpty++;
if (numEmpty > emptyProcessLimit) {
app.kill("empty #" + numEmpty, true);
}
}
break;
default:
mNumNonCachedProcs++;
break;
}
<!--關鍵點10 計算需要裁剪程序的數目-->
if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
&& !app.killedByAm) {
// 比home高的都需要裁剪,不包括那些等級高的程序
numTrimming++;
}
}
}
上面的兩個關鍵點是看當前後臺程序是否過多或者過老,如果存在過多或者過老的後臺程序,AMS是有權利殺死他們的。之後才是我們比較關心的存活程序的裁剪:
final int numCachedAndEmpty = numCached + numEmpty;
int memFactor;
<!--關鍵點11 根據後臺程序數目確定當前系統的記憶體使用狀況 ,確立記憶體裁剪等級(記憶體因子)memFactor,android的理念是准許存在一定數量的後臺程序,並且只有記憶體不夠的時候,才會縮減後臺程序-->
if (numCached <= ProcessList.TRIM_CACHED_APPS
&& numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
// 等級高低 ,殺的越厲害,越少,需要約緊急的時候才殺
if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {//3
memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
} else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) { //5
memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
} else {
memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
}
} else {
// 後臺程序數量足夠說明記憶體充足
memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
}
<!--關鍵點12 根據記憶體裁剪等級裁剪記憶體 Android認為後臺程序不足的時候,記憶體也不足-->
if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
if (mLowRamStartTime == 0) {
mLowRamStartTime = now;
}
int step = 0;
int fgTrimLevel;
// 記憶體不足的時候,也要通知前臺或可見程序進行縮減
switch (memFactor) {
case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
break;
case ProcessStats.ADJ_MEM_FACTOR_LOW:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
break;
default:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
break;
}
int factor = numTrimming/3;
int minFactor = 2;
if (mHomeProcess != null) minFactor++;
if (mPreviousProcess != null) minFactor++;
if (factor < minFactor) factor = minFactor;
int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
關鍵點11這裡不太好理解:Android系統根據後臺程序的數目來確定當前系統記憶體的狀況,後臺程序越多,越說明記憶體並不緊張,越少,說明越緊張,回收等級也就越高,如果後臺程序的數目較多,記憶體裁剪就比較寬鬆是ProcessStats.ADJ_MEM_FACTOR_NORMAL,如果不足,則再根據快取數目劃分等級。以6.0原始碼來說:
- 如果後臺程序數量(包含空程序)< 3 ,就說明記憶體非常緊張,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_CRITICAL
- 如果後臺程序數量(包含空程序)< 5 ,就說明記憶體非常緊張,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_LOW
- 如果比上面兩個多,但是仍然不足正常的後臺數目 ,記憶體裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_MODERATE
與之對應的關鍵點12,是確立前臺RUNNING程序(也不一定是前臺顯示)的裁剪等級。
- ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
- ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
- ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
之後就真正開始裁剪APP,這裡先看後臺程序不足的情況的裁剪,這部分相對複雜一些:
<!--裁剪後臺程序-->
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
if (allChanged || app.procStateChanged) {
setProcessTrackerStateLocked(app, trackerMemFactor, now);
app.procStateChanged = false;
}
// PROCESS_STATE_HOME = 12;
//PROCESS_STATE_LAST_ACTIVITY = 13; 退到後臺的就會用
// 優先順序比較低,回收等級比較高ComponentCallbacks2.TRIM_MEMORY_COMPLETE
// 當curProcState > 12且沒有被am殺掉的情況;上面的update的時候,在kill的時候,是會設定app.killedByAm的
//裁剪的話,如果 >= ActivityManager.PROCESS_STATE_HOME,老的裁剪等級較高,不重要,越新鮮的程序,裁剪等級越低
if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
&& !app.killedByAm) {
// 先清理最陳舊的 ,最陳舊的那個遭殃
if (app.trimMemoryLevel < curLevel && app.thread != null) {
try {
app.thread.scheduleTrimMemory(curLevel);
} catch (RemoteException e) {
}
}
app.trimMemoryLevel = curLevel;
step++;
// 反正一共就三個槽,將來再次重新整理的 時候,要看看是不是從一個槽裡面移動到另一個槽,
// 沒有移動,就不需要再次裁剪,等級沒變
if (step >= factor) {
step = 0;
switch (curLevel) {
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
break;
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
break;
}
}
}
上面的這部分是負責 app.curProcState >= ActivityManager.PROCESS_STATE_HOME這部分程序裁剪,主要是針對後臺快取程序,一般是oom_adj在9-11之間的程序,根據LRU確定不同的裁減等級。
else {
if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
|| app.systemNoUi) && app.pendingUiClean) {
// 釋放UI
final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
if (app.trimMemoryLevel < level && app.thread != null) {
try {
app.thread.scheduleTrimMemory(level);
} catch (RemoteException e) {
}
}
app.pendingUiClean = false;
}
// 啟動的時候會回撥一遍,如果有必要,啟動APP的時候,app.trimMemoryLevel=0
if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {
try {
app.thread.scheduleTrimMemory(fgTrimLevel);
} catch (RemoteException e) {
}
}
app.trimMemoryLevel = fgTrimLevel;
}
}
}
而這裡的裁剪主要是一些優先順序較高的程序,其裁剪一般是 ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ,由於這部分程序比較重要,裁剪等級較低,至於前臺程序的裁剪,一般是在啟動的時候,這個時候app.pendingUiClean==false,只會裁剪當前程序:
else {
<!--關鍵點13 記憶體充足的時候,程序的裁剪-->
...
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
// 在resume的時候,都是設定成true,所以退回後臺的時候app.pendingUiClean==true是滿足的,
// 因此縮減一次,但是不會再次走這裡的分支縮減即使優先順序變化,但是已經縮減過
// 除非走上面的後臺流程,那個時候這個程序的等級已經很低了,
if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
|| app.systemNoUi) && app.pendingUiClean) {
if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
&& app.thread != null) {
try {
app.thread.scheduleTrimMemory(
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
} catch (RemoteException e) {
}
}
// clean一次就弄成false
app.pendingUiClean = false;
}
// 基本算沒怎麼裁剪
app.trimMemoryLevel = 0;
}
}
}
最後這部分是後臺程序數量充足的時候,系統只會針對app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND的程序進行裁剪,而裁剪等級也較低:ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,因此根據裁剪等級APP可以大概知道系統當前的記憶體狀況,同時也能知道系統希望自己如何裁剪,之後APP做出相應的瘦身即可。不過,上面的程序裁剪的優先順序是完全根據後臺程序數量來判斷的,但是,不同的ROM可能進行了改造,所以裁剪等級不一定完全準確,比如在開發者模式開啟限制後臺程序數量的選項,限制後臺程序數目不超過2個,那麼這個時候的裁剪等級就是不太合理的,因為記憶體可能很充足,但是由於限制了後臺程序的數量,導致裁剪等級過高。因此在使用的時候,最好結合裁剪等級與當前記憶體數量來綜合考量。
通過“流氓”手段提高oom_adj,降低被殺風險,化身流氓程序
程序優先順序的計算Android是有自己的一條準則的,某些特殊場景的需要額外處理程序的oom_adj Android也是給了參考方案的。但是,那對於流氓來說,並沒有任何約束效力。 “流氓”仍然能夠參照oom_adj(優先順序)的計算規則,利用其漏洞,提高程序的oom_adj,以降低被殺的風險。如果單單降低被殺風險還好,就怕那種即不想死,又想佔用資源的APP,累積下去就會導致系統記憶體不足,導致整個系統卡頓。
優先順序的計算邏輯比較複雜,這裡只簡述非快取程序,因為一旦淪為快取程序,其優先順序就只能依靠LRU來計算,不可控。而流氓是不會讓自己淪為快取程序的,非快取程序是以下程序中的一種,並且,優先順序越高(數值越小),越不易被殺死:
ADJ優先順序 | 優先順序 | 程序型別 |
---|---|---|
SERVICE_ADJ | 5 | 服務程序(Service process) |
HEAVY_WEIGHT_APP_ADJ | 4 | 後臺的重量級程序,system/rootdir/init.rc檔案中設定 |
BACKUP_APP_ADJ | 3 | 備份程序(這個不太瞭解) |
PERCEPTIBLE_APP_ADJ | 2 | 可感知程序,比如後臺音樂播放 ,通過startForeground設定的程序 |
VISIBLE_APP_ADJ | 1 | 可見程序(可見,但是沒能獲取焦點,比如新程序僅有一個懸浮Activity,其後面的程序就是Visible process) |
FOREGROUND_APP_ADJ | 0 | 前臺程序(正在展示是APP,存在互動介面,Foreground process) |
* 第一種提高到FOREGROUND_APP_ADJ
我們從低到高看:如何讓程序程式設計FOREGROUND_APP_ADJ程序,也就是前臺程序,這個沒有別的辦法,只有TOP activity程序才能是算前臺程序。正常的互動邏輯下,這個是無法實現的,鎖屏的時候倒是可以啟動一個Activity,但是需要螢幕點亮的時候再隱藏,容易被使用者感知,得不償失,所以基本是無解,所以之前傳說的QQ通過一個畫素來保活的應該不是這種方案,而通過WindowManager往主螢幕新增View的方式也並未阻止程序被殺,到底是否通過一畫素實現程序包活,個人還未得到解答,希望能有人解惑。
- 第二種,提高到VISIBLE_APP_ADJ或者PERCEPTIBLE_APP_ADJ(不同版本等級可能不同 “4.3 = PERCEPTIBLE_APP_ADJ” 而 “> 5.0 = VISIBLE_APP_ADJ”)
先看一下原始碼中對兩種優先順序的定義,VISIBLE_APP_ADJ是含有可見但是非互動Activity的程序,PERCEPTIBLE_APP_ADJ是使用者可感知的程序,如後臺音樂播放等
// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;
// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;
這種做法是相對溫和點的,Android官方曾給過類似的方案,比如音樂播放時後,通過設定前臺服務的方式來保活,這裡就為流氓程序提供了入口,不過顯示一個常住服務會在通知欄上有個執行狀態的圖示,會被使用者感知到。但是Android恰恰還有個漏洞可以把該圖示移除,真不知道是不是Google故意的。這裡可以參考微信的保活方案:雙Service強制前臺程序保活。
startForeground(ID, new Notification()),可以將Service變成前臺服務,所在程序就算退到後臺,優先順序只會降到PERCEPTIBLE_APP_ADJ或者VISIBLE_APP_ADJ,一般不會被殺掉,Android的有個漏洞,如果兩個Service通過同樣的ID設定為前臺程序,而其一通過stopForeground取消了前臺顯示,結果是保留一個前臺服務,但不在狀態列顯示通知,這樣就不會被使用者感知到耍流氓,這種手段是比較常用的流氓手段。優先順序提高後,AMS的killBackgroundProcesses已經不能把程序殺死了,它只會殺死oom_adj大於ProcessList.SERVICE_ADJ的程序,而最近的任務列表也只會清空Activity,無法殺掉程序。 因為後臺APP的優先順序已經提高到了PERCEPTIBLE_APP_ADJ或ProcessList.VISIBLE_APP_ADJ,可謂流氓至極,如果再佔據著記憶體不釋放,那就是潑皮無賴了,這裡有個遺留疑問:startForeground看原始碼只會提升到PERCEPTIBLE_APP_ADJ,但是在5.0之後的版本提升到了VISIBLE_APP_ADJ,這裡看原始碼,沒找到原因,希望有人能解惑。具體做法如下:
public class RogueBackGroundService extends Service {
private static int ROGUE_ID = 1;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
Intent intent = new Intent(this, RogueIntentService.class);
startService(intent);
startForeground(ROGUE_ID, new Notification());
}
public static class RogueIntentService extends IntentService {
//流氓相互喚醒Service
public RogueIntentService(String name) {
super(name);
}
public RogueIntentService() {
super("RogueIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
}
@Override
public void onCreate() {
super.onCreate();
startForeground(ROGUE_ID, new Notification());
}
@Override
public void onDestroy() {
stopForeground(true);//這裡不寫也沒問題,好像會自動停止
super.onDestroy();
}
}
}
雙Service守護程序保活(這個也很流氓,不過如果不提高優先順序(允許被殺),也算稍微良心)
前文我們分析過Android Binder的訃告機制:如果Service Binder實體的程序掛掉,系統會向Client傳送訃告,而這個訃告系統就給程序保活一個可鑽的空子。可以通過兩個程序中啟動兩個binder服務,並且互為C/S,一旦一個程序掛掉,另一個程序就會收到訃告,在收到訃告的時候,喚起被殺程序。邏輯如下下:
首先編寫兩個binder實體服務PairServiceA ,PairServiceB,並且在onCreate的時候相互繫結,並在onServiceDisconnected收到訃告的時候再次繫結。
public class PairServiceA extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new AliveBinder();
}
@Override
public void onCreate() {
super.onCreate();
bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
ToastUtil.show("bind A");
}
};
與之配對的B
public class PairServiceB extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new AliveBinder();
}
@Override
public void onCreate() {
super.onCreate();
bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
}
};
}
之後再Manifest中註冊,注意要程序分離
<service android:name=".service.alive.PairServiceA"/>
<service
android:name=".service.alive.PairServiceB"
android:process=":alive"/>
之後再Application或者Activity中啟動一個Service即可。
startService(new Intent(MainActivity.this, PairServiceA.class));
這個方案一般都沒問題,因為Binder訃告是系統中Binder框架自帶的,除非一次性全部殺死所有父子程序,這個沒測試過。
廣播或者Service原地復活的程序保活
還有一些比較常見的程序保活手段是通過註冊BroadcastReceiver來實現的比如:
- 開機
- 網路狀態切換
- 相機
- 一些國內推送SDK(內含一些)
另外也能依靠Service的自啟動特性,通過onStartCommand的START_STICKY來實現,相比上面的不死,這些算比較柔和的啟動了,畢竟這兩種都是允許後臺殺死的前提下啟動的:
public class BackGroundService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
}
總結
所有流氓手段的程序保活,都是下策,建議不要使用,本文只是分析實驗用。當APP退回後臺,優先順序變低,就應該適時釋放記憶體,以提高系統流暢度,依賴流氓手段提高優先順序,還不釋放記憶體,保持不死的,都是作死。
僅供參考,歡迎指正