這篇文章主要介紹APP在安卓系統中是怎么被殺死的,按照怎樣的一個策略去釋放進程;同時介紹一些延長應用存活時間的方案,雖然這個在現在安卓系統上越來越難實現了,但是也是可以稍微了解下,主要也是通過這些hack的方案更好的了解安卓系統對進程的管理。
進程是怎么被殺死的?
我們知道,安卓系統里的所有APP都是被系統所托管的,也就是說,安卓系統負責APP進程的創建和回收。
進程創建很簡單,就是在APP啟動的時候,由Zygote進程fork出一個新的進程出來(注:系統服務system server也是由Zygote所創建),這個不是我們這邊文章的重點,后面會有專門的文章來敘述。
進程的回收發生在如下幾種情況:
進程Crash掉了
用戶主動的退出(殺進程,不殺進程的app還是在系統中的,這樣是為了能快速的再次啟動~)
內存緊張,并且進程已經不在可見進程了
前面2種是用戶行為或APP本身問題導致的進程回收,而第3種是系統行為,也是我們做APP保活可以做的地方。在內存緊張時,由Low Memory Killer根據進程的優先級來按順序的進行回收。
Low Memory Killer(LMK)
Low Memory Killer是基于Linux的Out Of Memory Killer(OOMKiller)優化的一種內存回收機制,相對與OOMKiller,它發生的時機要稍微早一點。
為什么要有這樣的改進?
一般的內存分配的邏輯是:程序向OS申請一段連續的內存空間,然后返回這段空間的起始地址。如果沒有足夠的空間可分配,則返回null回來,表示系統沒有可分配的內存。
Linux的內存分配則更加的積極:它假設應用申請了內存空間后并不會立即去使用它,所以允許超剩余內存的申請,當應用真的需要使用它的時候,操作系統可能已經通過回收了其他應用的內存空間而變得有能力去滿足這個應用的需求,簡單的說,就是允許應用申請比實際可分配空間(包括物理內存和Swap)更多的內存,這個特性稱為OverCommit。
如果申請的內存在需要使用的時候還沒有被釋放掉,那么就會觸發OOM Killer,直接干掉這個進程,這個可能不是用戶想要得到的結果。而LMK則將內存回收的時間提前,選擇殺死那么優先級最低的進程來釋放內存,同時設置了不同的內存大小觸發時機,這樣更加的靈活。
LMK的執行原理
安卓內核會每隔一段時間會檢查當前系統的空閑內存是否低于某個預置,如果是,則按照oom_adj的值按照從大到小的順序殺死進程,直到釋放的內存足夠。
所以這里有2個最直接相關的值:
內存閾值
oom_adj值
1、LMK之內存閾值
LMK是個多層次的內存回收器,它會根據內存的不同的閾值進行內存的回收,而具體的內存的的閾值是寫在系統文件里的,位置在/sys/module/lowmemorykiller/parameters/minfree
(必須在root下查看):上面數字的單位是頁(page),1page=4KB。內存閾值在不同的手機上時不一樣的,那么這個值是怎么來確定的呢?這個可以看下
ProcessList
的代碼:
private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
// Scale buckets from avail memory: at 300MB we use the lowest values to
// 700MB or more for the top values.
float scaleMem = ((float)(mTotalMemMb-350))/(700-350);
// Scale buckets from screen size.
int minSize = 480*800; // 384000
int maxSize = 1280*800; // 1024000 230400 870400 .264
float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
if (false) {
Slog.i("XXXXXX", "scaleMem=" + scaleMem);
Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth
+ " dh=" + displayHeight);
}
float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
if (scale < 0) scale = 0;
else if (scale > 1) scale = 1;
int minfree_adj = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
int minfree_abs = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
if (false) {
Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs);
}
final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
for (int i=0; iint low = mOomMinFreeLow[i];
int high = mOomMinFreeHigh[i];
if (is64bit) {
// Increase the high min-free levels for cached processes for 64-bit
if (i == 4) high = (high*3)/2;
else if (i == 5) high = (high*7)/4;
}
mOomMinFree[i] = (int)(low + ((high-low)*scale));
}
if (minfree_abs >= 0) {
for (int i=0; i mOomMinFree[i] = (int)((float)minfree_abs * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
}
}
if (minfree_adj != 0) {
for (int i=0; i mOomMinFree[i] += (int)((float)minfree_adj * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
if (mOomMinFree[i] < 0) {
mOomMinFree[i] = 0;
}
}
}
// The maximum size we will restore a process from cached to background, when under
// memory duress, is 1/3 the size we have reserved for kernel caches and other overhead
// before killing background processes.
mCachedRestoreLevel = (getMemLevel(ProcessList.CACHED_APP_MAX_ADJ)/1024) / 3;
// Ask the kernel to try to keep enough memory free to allocate 3 full
// screen 32bpp buffers without entering direct reclaim.
int reserve = displayWidth * displayHeight * 4 * 3 / 1024;
int reserve_adj = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAdjust);
int reserve_abs = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAbsolute);
if (reserve_abs >= 0) {
reserve = reserve_abs;
}
if (reserve_adj != 0) {
reserve += reserve_adj;
if (reserve < 0) {
reserve = 0;
}
}
if (write) {
ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
buf.putInt(LMK_TARGET);
for (int i=0; i buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
buf.putInt(mOomAdj[i]);
}
writeLmkd(buf);
SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
}
// GB: 2048,3072,4096,6144,7168,8192
// HC: 8192,10240,12288,14336,16384,20480
}
上面的代碼主要做了如下幾件事情:
根據手機的內存值,計算一個內存的比例值scaleMem(具體意義不明白~~)
根據傳入的window的displayWidth/displayHeight,計算一個視圖的比例值scaleDisp
在scaleMem和scaleDisp中選擇最大的一個
計算內存閾值,這里會經過3個步驟的計算(具體略)
傳給LMKD守護進程(進程間通信采用了socket方式),并且寫入到minfree文件里
這個方法是在什么時候觸發的呢?
updateOomLevels()
是在AMS的updateConfiguration()
方法里調用的,也就是說在設備配置變化的時候就會觸發。
為什么是6個值?
這個就是安卓系統做的分層次回收,它定義了6個層級的回收閾值,分別對應到了不同的進程狀態。
具體的定義:
private final int[] mOomAdj = new int[] {
FOREGROUND_APP_ADJ,
VISIBLE_APP_ADJ,
PERCEPTIBLE_APP_ADJ,
BACKUP_APP_ADJ,
CACHED_APP_MIN_ADJ,
CACHED_APP_MAX_ADJ
};
FOREGROUND_APP_ADJ
:前臺進程該進程擁有一個正在和用戶交互的activity
該進程擁有一個service,該service與用戶正在交互的Activity綁定
該進程擁有一個service,該service通過startForeground()稱為了前臺服務
某個進程擁有一個BroadcastReceiver,并且該BroadcastReceiver正在執行其onReceive()方法
VISIBLE_APP_ADJ
:可見進程該進程擁有不在前臺但是用戶任可見的activity(比如支付時拉起的第三方支付浮窗)
該進程擁有綁定到可見或前臺activity的service
PERCEPTIBLE_APP_ADJ
:可感知進程播放音樂進程
計步進程
BACKUP_APP_ADJ
:備份進程做備份操作的進程
CACHED_APP_MIN_ADJ
:不可見進程最小adj值CACHED_APP_MAX_ADJ
:不可見進程最大adj值
這6個adj值被寫入到了/sys/module/lowmemorykiller/parameters/adj
里(root手機可以查看),android 7.0之前這些值都是各位數的,android 7.0之后,這些值都被重新賦值了,adj值越大優先級越低。一般以0作為系統進程和應用進程的分界線,小于0的是系統進程,LMK一般不會回收。
2、oom_adj值
每個應用進程的adj值是怎么計算和存儲的呢?
核心方法(都在AMS里):
updateOomAdjLocked():觸發更新adj
computeOomAdjLocked():計算當前進程的adj值
applyOomAdjLocked():應用adj值
更為詳細的關于何時觸發更新?怎么計算的?請參考文章Android進程調度之adj算法。
總結下來,可以用一張流程圖表示:
我們看下oom_adj
值和oom_score_adj
值。oom_adj
是指示出當前進程是屬于哪一類的進程,這個值在computeOomAdjLocked()
里會計算出來;oom_score_adj
這個是當前進程的所打的一個分數(具體怎么打分的不清楚,但是不同進程同一oom_adj
值,oom_score_adj
是一樣的)
他們也是寫入到文件里的,具體在/proc//
下:
/proc//oom_adj
/proc//oom_score_adj
如何找到一個APP進程的pid值呢?
adb shell ps | grep
第二個數字23826就是當前進程的pid值。
3、總結&示例說明
安卓系統每隔一段時間(具體不清楚~)會檢查下當前內存的空閑情況,看看是否存在低于minfree列表中的某個閾值。
我們假設有這么個表格:
oom_adj | 0 | 100 | 200 | 300 | 900 | 906 |
oom_score_adj | 0 | 69 | 117 | 190 | 529 | 1000 |
minfree | 12288 | 18432 | 24576 | 36864 | 43008 | 49152 |
如果剩余內存少于24576(約98M),那么安卓系統就會找出當前在/proc下oom_adj
的值大于等于2進程,按照oom_adj
的值大小逆序排列;然后LMK會從最大的oom_adj
的值的進程開始kill,釋放他們的內存,直到內存的閾值超過了24576;如果有oom_adj
的值相等的進程,則優先kill占用內存大的進程。
整個過程描述:
進程與進程優先級
在Android中,應用進程劃分5級:
前臺進程(Foreground process)
可見進程(Visible process)
服務進程(Service process)
后臺進程(Background process)
空進程(Empty process)
不含任何活動應用組件的進程。保留這種進程的的唯一目的是用作緩存,以縮短下次在其中運行組件所需的啟動時間,這就是所謂熱啟動。為了使系統資源在進程緩存和底層內核緩存之間保持平衡,系統往往會終止這些進程。
根據進程中當前活動組件的重要程度,Android會對進程的優先級進行評定,這個可以在ProcessList.java
里可以看到,具體為(基于android 8.0):
adj | adj值 | 解釋 |
---|---|---|
UNKNOWN_ADJ | 1001 | 無法確定的進程,一般發生在緩存的時候 |
CACHED_APP_MAX_ADJ | 906 | 空進程,最大adj值 |
CACHED_APP_MIN_ADJ | 900 | 空進程,最小adj值 |
SERVICE_B_ADJ | 800 | 不活躍進程 |
PREVIOUS_APP_ADJ | 700 | 上一次交互的進程,比如對個應用切換 |
HOME_APP_ADJ | 600 | Launcher進程,盡量不要kill它 |
SERVICE_ADJJ | 500 | 程序里的service進程 |
HEAVY_WEIGHT_APP_ADJ | 400 | 重型進程,一般運行在后臺,盡量不要kill它 |
BACKUP_APP_ADJ | 300 | 備份進程 |
PERCEPTIBLE_APP_ADJ | 200 | 可感知進程 |
VISIBLE_APP_ADJ | 100 | 可見進程 |
FOREGROUND_APP_ADJ | 0 | 前臺進程 |
PERSISTENT_SERVICE_ADJ | -700 | 系統進程,或持續存在進程附著的服務進程 |
PERSISTENT_PROC_ADJ | -800 | 系統持續性進程,如電話 |
SYSTEM_ADJ | -900 | system進程 |
NATIVE_ADJ | -1000 | native進程,不屬于JVM內存處理范圍 |
如何讓APP活的久一點?
讓app活的久一點,可以從兩個方面來優化:
盡量保證不被系統殺死
能夠自我復活
如何保證不被殺?
核心就是提高adj的值,讓系統覺得不能殺。最厲害的就是將自己的應用弄成系統應用,但是這個不在技術討論的范疇。
1、進程拆分
內存的大小也是系統殺死進程的一個考量,所以通過進程拆分來來減少整個app的內存大小。如webview單進程,push模塊單進程。
2、onTrimeMemory的回調
OnTrimMemory()
也是從內存的角度來保活的方案,通過對自我的瘦身來降低內存,降低被后臺殺死的風險。我們知道,OnTrimMemory()
可以做到不同級別額裁剪,這個就給上層更合理的去做裁剪。
3、開啟前臺Service
前臺service可以提高應用的adj值,降低被系統回收的概率。
操作就是:在應用啟動的時候,啟動service,并通過使用startForeground()
將當前service設置為前臺service來提高優先級。這里需要注意android系統的不同版本對于開啟前臺服務的控制:
API < 18:直接startForeground()即可
API >= 18:startForeground()必須給用戶一個可見的notification
API >= 26:在Android8.0之后, google對后臺啟動service進行了更加嚴格的限制,但是還是可以通過
ContextCompat.startForegroundService()
創建前臺服務
對于會出現可見notification的情況,可以專門開啟一個服務去關閉它。
示例:
public class CancelNoticeService extends Service {
private Handler handler;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
handler = new Handler();
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(ForegroundService.NOTICE_ID,builder.build());
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopForeground(true);
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.cancel(ForegroundService.NOTICE_ID);
stopSelf();
}
}, 300);
}
}
4、1像素Activity
這個主要是在鎖屏后的保活,通過在鎖屏后(app已經被退到后臺)打開一個1像素的Activity。
public class SinglePixelActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window mWindow = getWindow();
mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams params = mWindow.getAttributes();
params.x = 0;
params.y = 0;
params.height = 1;
params.width = 1;
mWindow.setAttributes(params);
SinglePixelManager.instance().setSingleActivity(this);
}
}
這里是不是1像素不重要,因為屏幕開啟后,會關閉這個Activity。并且不能一直讓這個1像素一直占住屏幕,因為會導致Launcher無法點擊。
5、循環播放無聲音頻
這個就是比較極端的一種方式了,可能會帶來耗電方面的損耗。同時,在某些手機上,用戶是知道你在播放的,如下圖
可以看到播放的波浪,這個效果很好,在某些手機上連一鍵清理都無法清理掉,但是在產品中使用還是得慎重。
如何復活?
防止app不會系統回收可以做的方案比較少,而且隨著安卓系統的升級,對這方面的控制越來越嚴格。
那么我們還可以從復活的角度來思考app存活的問題。隨著系統的升級,復活的可能性也是越來越低,下面大致說一下可以嘗試的方式(其實很多也沒啥卵用~):
1、START_STICKY
如果service進程被kill掉,保留service的狀態為開始狀態,但不保留啟動時候傳來的intent對象。隨后系統會嘗試重新創建service,由于服務狀態為開始狀態,所以創建服務后一定會調用onStartCommand(Intent,int,int)方法。如果在此期間沒有任何啟動命令被傳遞到service,那么參數Intent將為null。
這個主要是針對系統資源不足而導致的服務被關閉。其他情況下的app殺死是沒啥效果的。
2、JobScheduler
JobScheduler是Android 5.0引入的允許在將來的某個時刻在達到預先定義的條件的情況下執行指定的任務的API。通常情況下,即使APP被系統停止,預定的任務仍然會被執行。
JobScheduler工作原理:
首先在一個實現了JobService的子類的onStartJob方法中執行這項任務,使用JobInfo的Builder方法來設定條件并和實現了JobService的子類的組件名綁定,然后調用系統服務JobScheduler的schedule方法。這樣,即便在執行任務之前應用程序進程被殺,也不會導致任務不會執行,因為系統服務JobScheduler會使用bindServiceAsUser的方法把實現了JobService的子類服務啟動起來,并執行它的onStartJob方法。
JobScheduler只有在5.0以上才能使用,對于5.0以下的怎么辦呢?可以參考https://github.com/evant/JobSchedulerCompat?(這個項目作者已經很久不維護了,但是可以在它的基礎上去做完善和修改,同時也可以作為我們了解安卓CS架構的一個好的實例)
3、安卓賬號自同步
利用Android系統提供的賬號和同步機制實現。安卓會定期喚醒賬戶更新服務,我們可以自己設定同步的事件間隔,且發起更新的是系統,不會受到任何限制。
關于安卓賬號詳細介紹請移步:https://developer.android.com/training/sync-adapters/
該方法局限性還是很大的,用戶會發現莫名出現一個賬戶,并且同步是必須聯網的。
4、網絡連接保活
GCM
公共的第三方push通道(信鴿等)
自身跟服務器通過輪詢,或者長連接
更多的細節可以參考微信團隊分享的網絡保活的方案http://www.52im.net/thread-209-1-1.html。
5、系統廣播
這個意義也不太大了。
參考文獻
http://www.52im.net/thread-1135-1-1.html
http://www.52im.net/thread-210-1-1.html
https://github.com/Marswin/MarsDaemon
https://juejin.im/entry/5b22663df265da59bf79f25b/
https://juejin.im/post/58cf80abb123db3f6b45525d
作者簡介:payne, 天天P圖AND工程師
文章后記:
天天P圖是由騰訊公司開發的業內領先的圖像處理,相機美拍的APP。歡迎掃碼或搜索關注我們的微信公眾號:“天天P圖攻城獅”,那上面將陸續公開分享我們的技術實踐,期待一起交流學習!
? ??
加入我們
天天P圖技術團隊長期招聘:(1)?深度學習(圖像處理)研發工程師(上海)
工作職責
開展圖像/視頻的深度學習相關領域研究和開發工作;
負責圖像/視頻深度學習算法方案的設計與實現;
支持社交平臺部產品前沿深度學習相關研究。
工作要求
計算機等相關專業碩士及以上學歷,計算機視覺等方向優先;
掌握主流計算機視覺和機器學習/深度學習等相關知識,有相關的研究經歷或開發經驗;
具有較強的編程能力,熟悉C/C++、python;
在人臉識別,背景分割,體態跟蹤等技術方向上有研究經歷者優先,熟悉主流和前沿的技術方案優先;
寬泛的技術視野,創造性思維,富有想象力;
思維活躍,能快速學習新知識,對技術研發富有激情。
(2) AND / iOS 開發工程師?
(3) 圖像處理算法工程師?
期待對我們感興趣或者有推薦的技術牛人加入我們(base 上海)!聯系方式:ttpic_dev@qq.com