android進程常駐、保活研究


1、產品需求

一說到進程常駐,立馬就有很多人開始吐槽,什麼流氓軟件啊,什麼流氓技術啊之類的。但是技術不分好壞,只有看做產品的人怎麼使用了(但一般情況是一個牛逼的程序擁有着一羣牛逼的技術,卻被一個流氓產品驅動着)。
我們經常會遇到一些必須保證進程常駐的需求,比如聊天軟件要時刻監聽着是否其他人發消息;跑步軟件,總不能一直點亮屏幕;個性鬧鐘,也許一不小心用戶就殺死了進程,第二天,睡到11點未響拿起手機,看看以爲是晚上11點,繼續睡了。所以進程常駐技術有時也是一個不可或缺的技術。常見代表,QQ,微信,360等等。

2、資源管理機制

Android 之所以採用特殊的資源管理機制,原因在於其設計之初就是面向移動終端,所有可用的內存僅限於系統 RAM,必須針對這種限制設計相應的優化方案。當 Android 應用程序退出時,並不清理其所佔用的內存,Linux 內核進程也相應的繼續存在,所謂“退出但不關閉”。從而使得用戶調用程序時能夠在第一時間得到響應。在系統內存不足的情況下,系統開始依據自身的一套進程回收機制來判斷要kill掉哪些進程,以騰出內存來供給需要的app,這套殺進程回收內存的機制就叫Low Memory Killer ,它是基於Linux內核的 OOM Killer機制誕生。

既然說道內存回收機制,我們再來說說進程的優先級。
Android 基於進程中運行的組件及其狀態規定了默認的五個回收優先級:
1.前臺進程(Foreground process)
2.可見進程(Visible process)
3.服務進程(Service process)
4.後臺進程(Background process)
5.空進程(Empty process)
前臺進程的重要性最高,依次遞減,空進程重要性最低,更容易被回收。參考文檔如下:

前臺進程 —— Foreground process
用戶當前操作所必需的進程。通常在任意給定時間前臺進程都爲數不多。只有在內存不足以支持它們同時繼續運行這一萬不得已的情況下,系統纔會終止它們。
A. 擁有用戶正在交互的 Activity(已調用 onResume()
B. 擁有某個 Service,後者綁定到用戶正在交互的 Activity
C. 擁有正在“前臺”運行的 Service(服務已調用 startForeground()
D. 擁有正執行一個生命週期回調的 Service(onCreate()onStart() 或 onDestroy()
E. 擁有正執行其 onReceive() 方法的 BroadcastReceiver
可見進程 —— Visible process
沒有任何前臺組件、但仍會影響用戶在屏幕上所見內容的進程。可見進程被視爲是極其重要的進程,除非爲了維持所有前臺進程同時運行而必須終止,否則系統不會終止這些進程。
A. 擁有不在前臺、但仍對用戶可見的 Activity(已調用 onPause())。
B. 擁有綁定到可見(或前臺)Activity 的 Service
服務進程 —— Service process
儘管服務進程與用戶所見內容沒有直接關聯,但是它們通常在執行一些用戶關心的操作(例如,在後臺播放音樂或從網絡下載數據)。因此,除非內存不足以維持所有前臺進程和可見進程同時運行,否則系統會讓服務進程保持運行狀態。
A. 正在運行 startService() 方法啓動的服務,且不屬於上述兩個更高類別進程的進程。
後臺進程 —— Background process
後臺進程對用戶體驗沒有直接影響,系統可能隨時終止它們,以回收內存供前臺進程、可見進程或服務進程使用。 通常會有很多後臺進程在運行,因此它們會保存在 LRU 列表中,以確保包含用戶最近查看的 Activity 的進程最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並保存了其當前狀態,則終止其進程不會對用戶體驗產生明顯影響,因爲當用戶導航回該 Activity 時,Activity 會恢復其所有可見狀態。
A. 對用戶不可見的 Activity 的進程(已調用 Activity的onStop() 方法)
空進程 —— Empty process
保留這種進程的的唯一目的是用作緩存,以縮短下次在其中運行組件所需的啓動時間。 爲使總體系統資源在進程緩存和底層內核緩存之間保持平衡,系統往往會終止這些進程。
A. 不含任何活動應用組件的進程

再科普一下oom_adj。什麼是oom_adj?它是linux內核分配給每個系統進程的一個值,代表進程的優先級,進程回收機制就是根據這個優先級來決定是否進行回收。對於oom_adj的作用,你只需要記住以下幾點即可:
  • 進程的oom_adj越大,表示此進程優先級越低,越容易被殺回收;越小,表示進程優先級越高,越不容易被殺回收
  • 普通app進程的oom_adj>=0,系統進程的oom_adj纔可能<0
如何查看進程的oom_adj呢,我們可以使用如下的兩個命令
ps | grep 包名//獲取你指定的進程信息主要是爲了獲取進程id
查看指定進程信息
cat /proc/進程ID/oom_adj
根據進程id查看oom_adj
com.tencent.mm是騰訊微信的包名,我們可以看到無論是微信還是他的push進程,oom_adj的值都是1,當然在我手機上,我以爲它是關閉的。

oom_adj值的意義如下:
/**
 * Activity manager code dealing with processes.
 */
final class ProcessList {
    ...
    // OOM adjustments for processes in various states:

    // Adjustment used in certain places where we don't know it yet.
    // (Generally this is something that is going to be cached, but we
    // don't know the exact value in the cached range to assign yet.)
    static final int UNKNOWN_ADJ = 16;

    // This is a process only hosting activities that are not visible,
    // so it can be killed without any disruption.
    static final int CACHED_APP_MAX_ADJ = 15;
    static final int CACHED_APP_MIN_ADJ = 9;

    // The B list of SERVICE_ADJ -- these are the old and decrepit
    // services that aren't as shiny and interesting as the ones in the A list.
    static final int SERVICE_B_ADJ = 8;

    // This is the process of the previous application that the user was in.
    // This process is kept above other things, because it is very common to
    // switch back to the previous app.  This is important both for recent
    // task switch (toggling between the two top recent apps) as well as normal
    // UI flow such as clicking on a URI in the e-mail app to view in the browser,
    // and then pressing back to return to e-mail.
    static final int PREVIOUS_APP_ADJ = 7;

    // This is a process holding the home application -- we want to try
    // avoiding killing it, even if it would normally be in the background,
    // because the user interacts with it so much.
    static final int HOME_APP_ADJ = 6;

    // This is a process holding an application service -- killing it will not
    // have much of an impact as far as the user is concerned.
    static final int SERVICE_ADJ = 5;

    // This is a process with a heavy-weight application.  It is in the
    // background, but we want to try to avoid killing it.  Value set in
    // system/rootdir/init.rc on startup.
    static final int HEAVY_WEIGHT_APP_ADJ = 4;

    // This is a process currently hosting a backup operation.  Killing it
    // is not entirely fatal but is generally a bad idea.
    static final int BACKUP_APP_ADJ = 3;

    // 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;

    // This is the process running the current foreground app.  We'd really
    // rather not kill it!
    static final int FOREGROUND_APP_ADJ = 0;

    // This is a process that the system or a persistent process has bound to,
    // and indicated it is important.
    static final int PERSISTENT_SERVICE_ADJ = -11;

    // This is a system persistent process, such as telephony.  Definitely
    // don't want to kill it, but doing so is not completely fatal.
    static final int PERSISTENT_PROC_ADJ = -12;

    // The system process runs at the default adjustment.
    static final int SYSTEM_ADJ = -16;

    // Special code for native processes that are not being managed by the system (so
    // don't have an oom adj assigned by the system).
    static final int NATIVE_ADJ = -17;
    ... 
}
借用騰訊張興華同學的一張圖:
oom_adj值
其中紅色部分代表比較容易被殺死的 Android 進程(OOM_ADJ>=4),綠色部分表示不容易被殺死的 Android 進程,其他表示非 Android 進程(純 Linux 進程)。在 Lowmemorykiller 回收內存時會根據進程的級別優先殺死 OOM_ADJ 比較大的進程,對於優先級相同的進程則進一步受到進程所佔內存和進程存活時間的影響。

3、進程死亡情況分析

Android 手機中進程被殺死可能有如下情況:
進程死亡的情況
綜上,可以得出減少進程被殺死概率無非就是想辦法提高進程優先級,減少進程在內存不足等情況下被殺死的概率。

4、進程保活的手段

進程保活無非是兩種方式:
  • 優化進程的oom_adj的值,確保進程更高的優先級,從而達到不會被Low Memory Killer 回收。
  • 進程被殺死之後,進行拉活。

4.1 提升進程優先級

4.1.1 利用Activity提升權限

設計思想
監聽手機的鎖屏事件,在屏幕鎖屏的時候,啓動一個透明的1像素的Activity,再監聽用戶解鎖事件,在解鎖的時候,銷燬這個1像素的Activity。通過這個方案用戶無感知,且能將進程在鎖屏的期間的優先級由4或6提升到優先級0。

適用範圍
適用場景:主要用在第三方應用以及系統管理工具在檢測到鎖屏事件後一段時間(一般爲5分鐘以內)內會殺死後臺進程,已達到省電的目的問題。
適用版本:所有版本

代碼實現
首先要在MainActivity中註冊黑屏和解鎖事件的廣播。這裏注意了黑屏的廣播只能在代碼進行註冊。清單文件註冊的話,黑屏和亮屏的廣播是接收不到的。底層增加了判斷,Intent.ACTION_USER_PRESENT清單文件裏註冊時可以接受的。
MainActivity.java
MainActivity註冊黑屏和解鎖廣播
在廣播接收的地方判斷,保活Activity的啓動與銷燬時機,代碼如下:
廣播接收判斷Activity的啓動和銷燬時機
接下來就要看看我們的1像素的Activity定義了:
1像素的Activity定義
在清單文件裏面設置其透明,且不在最近程序( RecentTask )中顯示。
1像素Activity在清單文件裏面的聲明
styles定義透明沒title的theme
到這裏已經可以了,只有在應用開啓的時候纔能有作用哦。代碼在KeepLiveActivity工程中,文章最後提供下載地址。

方案結果
鎖屏的時候查看應用的oom_adj:
KeepLiveActivity的oom_adj的結果

4.1.2 利用前臺Service提升權限

設計思想
在Android中Service的oom_adj的值爲4,通過startForeground設置爲前臺Service,oom_adj的值就可以從4上升到1。僅低於正在交互的進程。
這個方案看上去很完美,但是卻有一個嚴重的問題,在Android2.3之後,調用startForeground將後臺進程設置爲前臺進程的,必須在系統的通知欄發送一條通知,也就是前臺 Service 與一條可見的通知時綁定在一起的。對於不需要常駐通知欄的應用來說,該方案雖好,但卻是用戶感知的,無法直接使用。

在這裏我們可以通過一個灰色技術來解決這個問題,android的一個bug。發送兩個具有相同 ID 的 Notification。我們再結束後一個Service,這時Notification就會消失不見,但是我們通過查看進程中的Service,發現前一個Service依然存在,且也是前臺Service。再查看oom_adj依然是1。

適用範圍
目前所知所有版本。

代碼實現
這個方案的主要實現代碼都是在KeepLiveService裏面,通過開啓一個內部的Service並設置同一個id的Notification,並關閉內部service來隱藏通知欄,代碼如下:
KeepLiveService內部實現
至此,這部分代碼也差不多了,該部分代碼在KeepLiveService工程中。

方案結果
最後隱藏的前臺Service方案結果如何呢?截圖如下:
KeepLiveService方案結果

更多
我們可以使用如下命令查看Services的使用細節 
adb shell dumpsys activity services 包名
查看Services命令
其中有一個isForeground = true代表這個Service是前臺線程。我們再來看看別的一些應用哈。
qq的services

qq的service的信息

4.2 進程拉活
4.2.1 通過系統廣播拉活進程
設計思想
當發生一些特定的系統事件時,系統會發出相應的廣播,在清單文件中靜態註冊對應的廣播接收者,即可在發生相應時間時進行拉活。
適用範圍
1)在一些深度定製的廠商,手機在關閉情況下接受到系統廣播。如小米。
2)在3.1之後,系統的package manager增加了對處於“stopped state”應用的管理。指的是安裝後從來沒有啓動過和被用戶手動強制停止的應用,與此同時系統增加了2個Flag:FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES ,來標識一個intent是否激活處於“stopped state”的應用。當2個Flag都不設置或者都進行設置的時候,採用的是FLAG_INCLUDE_STOPPED_PACKAGES的效果。
Android 3.1開始,系統底層向所有Intent的廣播添加了FLAG_EXCLUDE_STOPPED_PACKAGES標誌。
3)系統廣播事件不可控,只能保證發生事件時拉活進程,但無法保證進程掛掉後立即拉活。
因此,該方案主要作爲備用手段。

4.2.2 通過第三方應用的廣播拉活
設計思想
通過反編譯第三方知名的應用,如:手機QQ、微信、支付寶、UC瀏覽器等,以及友盟、信鴿、個推等 SDK。找出它們外發的廣播,在應用中進行監聽,這樣當這些應用發出廣播時,就會將我們的應用拉活。
這和上一個方法的區別是,這裏面的廣播往往設置FLAG_INCLUDE_STOPPED_PACKAGES。
適用範圍
除了和系統廣播方案一樣的一些制約以外,還有一些其他因素:
  • 反編譯分析的第三方應用的多少,和用戶是否安裝。
  • 第三方應用的廣播的頻率,以及後續版本是否還繼續存在等各種因素。

4.2.3 守護進程相互拉活
設計思想
當你的多款應用被使用的時候,你可以讓兩個進程不停的通過廣播來相互拉起。這類方法在BAT類公司經常使用。
適用範圍
當你一個進程被殺死後你可以拉起,但多個進程同時被殺死就沒有辦法。而且還受拉起方法約束,比如通過廣播進行拉活,但是小米強制沒有開啓的進程不能收到廣播。

4.2.4 利用Service的機制拉活
設計思想
將Service設置爲START_STICKY,利用系統機制在Service掛掉後自動拉活的原理。
適用範圍
如下情況無法拉活:
  • Service第一次被異常殺死後會在5秒內重啓,第二次被殺死會在10秒內重啓,第三次會在20秒內重啓,一旦在短時間內Service被殺死達到5次,則系統不再拉起。
  • 進程被取得Root權限的管理工具或者系統工具通過forestop停止掉,無法重啓。
代碼實現
只需將onStartCommand()函數的返回值設爲Service.START_STICKY即可,如下:
利用START_STICKY來重啓Service

4.2.5 利用JobScheduler機制拉活
設計思想
系統在Android5.0以上版本提供了JobScheduler接口,系統會定時調用該進程以使應用進行一些邏輯操作。
適用範圍
適用於Android5.0版本以上,7.0以下的手機。且不受forestop影響,被強制停止的應用依然可以拉活。在該這些版本範圍內,暫只發現小米手機出現無法拉活。
代碼實現
首先定義一個類KeepLiveService繼承JobService,在代碼onStartJob()函數中執行你的操作:
JobScheduler中service實現
在清單文件裏註冊,注意要添加屬性權限:
聲明即添加權限
KeepLiveService定義好,接下來就是要看怎麼調用JobService了,首先和其他Service一樣開啓Service,然後再啓動JobScheduler拉活。
JobScheduler中mainActivity的實現

方案結果
經各種機型測試結果如下:
三星Galaxy s6 android6.0.1系統 拉活成功
LG nexus5x android7.0系統 拉活失敗
華爲 G7-TL00 android4.4.4系統 api不支持 拉活失敗(可以自己封裝JobScheduler兼容5.0以下版本)
華爲 nexus6P android6.0.1系統 拉活成功
小米4 android6.0.1系統 不成功

4.2.6 利用賬號同步機制拉活
設計思想
android系統的賬號同步機制會定期進行同步賬號,利用這個機制進行拉活。
適用範圍
  • 據說使用於Android的所有版本,但是本人小米手機不可以進行拉活。(小米手機:只有在應用開啓的時候,纔會執行同步的函數。)
  • 各大廠商的修改,導致有些手機賬號同步功能關閉的,需要手動打開。(小米只有騰訊產品是默認打開的。)
  • SyncAdapter時間進度不高,往往手機處於休眠狀態,時間往後調整,同步間隔最低爲1分鐘。還有一些產品爲半小時。
代碼實現
賬號同步api如何使用,這裏就不贅述了。大家可以自行百度一下,或參考之後的代碼。這裏主要說一下部分重要的代碼。
在Authenticator.java的addAccount()中直接添加賬號,會導致手機卡主,不知道是否是沒有調用底層的onActivityForResult()還是什麼原因。所以我還是按照官方的示例,在addAccount()跳轉到登陸界面。如下:
addAccount代碼
這裏將跳轉的intent包含在bundle傳遞底層。接下來我在AuthenticatorActivity中看一下添加賬號和設置同步週期的代碼。
AuthenticatorActivity
最後執行同步會執行SyncAdapter.java裏面的OnPerformSync()函數裏面的代碼。如下:
OnPerformSync
最後再放上在清單文件裏面註冊的代碼:
清單文件註冊
在清單文件裏面注意添加對應的權限,之前就是參考其他人blog而沒注意權限,導致耽誤了很長時間沒有調試出來。
至此賬戶同步拉活也說完了,代碼在文章後面鏈接的KeepLiveAccount工程中。

4.2.7 利用Native進程拉活(網上,暫沒實現)
設計思想
主要思想:
利用 Linux 中的 fork 機制創建 Native 進程,在 Native 進程中監控主進程的存活,當主進程掛掉後,在 Native 進程中立即對主進程進行拉活。

主要原理:
在 Android 中所有進程和系統組件的生命週期受 ActivityManagerService 的統一管理。而且,通過 Linux 的 fork 機制創建的進程爲純 Linux 進程,其生命週期不受 Android 的管理。


方案實現挑戰
挑戰一:在 Native 進程中如何感知主進程死亡。
要在 Native 進程中感知主進程是否存活有兩種實現方式:
  • 在 Native 進程中通過死循環或定時器,輪訓判斷主進程是否存活,檔主進程不存活時進行拉活。該方案的很大缺點是不停的輪詢執行判斷邏輯,非常耗電。
  • 在主進程中創建一個監控文件,並且在主進程中持有文件鎖。在拉活進程啓動後申請文件鎖將會被堵塞,一旦可以成功獲取到鎖,說明主進程掛掉,即可進行拉活。由於 Android 中的應用都運行於虛擬機之上,Java 層的文件鎖與 Linux 層的文件鎖是不同的,要實現該功能需要封裝 Linux 層的文件鎖供上層調用。
封裝 Linux 文件鎖的代碼如下:
封裝Linux文件鎖
Native 層中堵塞申請文件鎖的部分代碼:
堵塞申請文件鎖的部分代碼

挑戰二:在 Native 進程中如何拉活主進程。
通過 Native 進程拉活主進程的部分代碼如下,即通過 am 命令進行拉活。通過指定“–include-stopped-packages”參數來拉活主進程處於 forestop 狀態的情況。
通過am命令進行拉活

挑戰三:如何保證 Native 進程的唯一。
從可擴展性和進程唯一等多方面考慮,將 Native 進程設計層 C/S 結構模式,主進程與 Native 進程通過 Localsocket 進行通信。在Native進程中利用 Localsocket 保證 Native 進程的唯一性,不至於出現創建多個 Native 進程以及 Native 進程變成殭屍進程等問題。
保證native進程唯一

適用範圍
該方案主要適用於 Android5.0 以下版本手機。該方案不受 forcestop 影響,被強制停止的應用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。
對於Android5.0以上手機,系統雖然會將native進程內的所有進程都殺死,這裏其實就是系統“依次”殺死進程時間與拉活邏輯執行時間賽跑的問題,如果可以跑的比系統邏輯快,依然可以有效拉起。記得網上有人做過實驗,該結論是成立的,在某些 Android 5.0 以上機型有效。

5、其他的拉活方式

還可以通過應用內的push來拉活應用:
  • 國外版本:接入Google的GCM。
  • 國內版本:根據手機不通,小米手機接入小米推送,華爲手機接入華爲推送;其他手機可以嘗試JPush。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章