你說,你的APP要不要保活?
0、前言
Android 系統為了保持系統執行流暢,在記憶體吃緊的情況下,會將一些程序 kill ,以釋放一部分記憶體。然而,對於一些(如:IM-QQ 、微信,支付-支付寶等)比較重要、我們希望能及時收到訊息的 APP,需要保持程序持續活躍,那麼就需要實施一些保活措施來保證程序能夠持續存活,即 Android 程序保活 。 可參看前幾天的一篇文章: ofollow,noindex"> 《2018年Android的保活方案效果統計》
Android 程序保活,一般從兩個方面進行:
-
執行中保活:提高程序優先順序,降低被系統 kill 的概率
-
被 kill 後拉活:被系統 kill 後,將程序拉活(重啟)
在此之前,我們先來了解下 Android 程序的一些相關概念。
1、程序
預設情況下,同一 APP 的所有元件均執行在相同的程序中,但是也可以根據需要,通過在清單檔案中配置來控制某些元件的所屬程序。
記憶體不足的情況下,Android 系統會選擇 kill 某一程序來釋放該程序佔用的記憶體,供其它為使用者提供更為緊急服務的程序使用。在被關閉的程序中執行的元件也會隨著程序的關閉而銷燬。
決定 kill 哪個程序時,Android 系統將權衡所有程序對使用者的相對重要程度。例如:相對於託管可見 Activity 的程序而言,更有可能 kill 託管不可見 Activity 的程序。因此,是否終止 kill 某個程序取決於該程序中所執行元件的狀態。
2、Android 程序的生命週期
Android 系統會盡量長時間地保持 APP 程序的執行,但為了新建程序或者執行更重要的程序,最終要 kill 舊程序來回收記憶體。為了確定保留或者 kill 哪些程序,系統會根據程序中正在執行元件的狀態,將每個程序放入重要性層次結構中,必要時,系統會首先kill重要性最低的程序,其次kill重要性略低的程序,以此類推。
重要性層次結構一共有5級。以下列表按照重要程度列出了各類程序(第一類程序最重要,將是最後一類被終止的程序):
1、前臺程序
使用者當前操作的程序。一個程序滿足以下任一條件 ,即視為前臺程序:
-
託管使用者正在互動的 Activity(已呼叫 onResume() 方法)。
-
託管某個 Service ,且 Service 繫結到使用者正在互動的 Activity。
-
託管正在“前臺”執行的 Service(服務已呼叫startForeground())。
-
託管正在執行生命週期回撥的 Service( onCreate() 、 onStart() 或 onDestory() )。
-
託管正在執行 onReceive() 方法的 BroadcastReceiver。
通常,任意時間的前臺程序資料都不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會 kill 它們。
2、可見程序
沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程序。 如果一個程序滿足以下任一條件,即視為可見程序:
-
託管不在前臺、但仍對使用者可見的 Activity(已呼叫 onPause() 方法)。如:前臺 Activity 啟動了一個對話方塊,允許在其後面顯示上一個 Activity。
-
託管繫結到可見(或前臺)的 Activity 的 Service。
可見程序被視為及其重要的程序,除非為了維持所有前臺程序同時執行而必須終止,否則系統不會kill這些程序。
3、服務程序
正在執行已使用 startService() 方法啟動的 Service 且不屬於上述兩個更高類別程序的程序。
儘管服務程序與使用者可見內容沒有直接關聯,但是它們通常在執行一些使用者比較關心的操作(如:在後臺播放音樂或從網路下載資料等),因此,除非內部不足以維持所有前臺程序和可見程序同時執行,否則系統不會 kill 這些程序。
4、後臺程序
託管目前對使用者不可見的 Activity 的程序(已呼叫 Activity 的 onStop() 方法)。
後臺程序對使用者體驗沒有直接影響,系統可能隨時會 kill 它們,以回收記憶體提供給前臺程序、可見程序、服務程序使用。通常會有很多後臺程序同時執行,系統將它們儲存在 LRU(最近最少使用)列表中,以確保包含使用者最近檢視的 Activity 的程序最後一個被終止。
5、空程序
不包含任何活動元件的程序。
保留這種程序的唯一目的是快取,以縮短下次在其中執行的元件的啟動時間。為使系統總體資源在程序快取和底層核心快取之間保持平衡,系統往往會kill這些程序。
根據程序中當前活動的元件的重要程度,Android 系統會將程序評定為可能達到的最高級別。比如,託管服務和可見 Activity 的程序,系統會將其評定為可見程序,而不是服務程序。
此外,一個程序的級別可能會因為其他程序對其依賴而有所提高,即服務於另一程序的程序其級別不會低於其服務的程序。例如,程序 A 中的 Service 繫結到程序 B 中的元件,則程序 A 始終被視為至少和程序 B 同等級別。
由於執行 Service 的程序其級別高於託管後臺 Activity 的程序,因此在要做長時間後臺操作的 Activity 中最好為該操作啟動 Service,而不是簡單的建立子執行緒,當操作有可能比 Activity 更持久時更需如此。例如,需要上傳較大圖片或較大檔案的 Activity,應該啟動 Service 來執行上傳操作,這樣,即使 Activity 被銷燬,Service 仍能在後臺繼續執行上傳操作。使用 Service 執行較長耗時操作,可以保證不管 Activity 發生什麼情況,該操作至少有服務程序的優先順序。同理,使用廣播接收器時,也當如此。
以上程序生命週期內容參考Android官網文件(需要科學上網(ಥ _ ಥ))。
3、Android 程序回收策略
上文提到了,Android 系統在記憶體不足以建立新程序或執行更重要的程序時,會 kill 重要性低的程序來回收記憶體。也總結了 Android 系統程序的重要級別,那麼具體的程序回收策略是什麼呢?
Android 系統回收程序記憶體的機制叫 LMS ( Low Memory Killer )機制,是一種根據 oom_adj 閾值級別觸發相應力度的記憶體回收的機制。oom_adj 代表程序的優先順序,數值越高,優先順序越低,越容易被殺死。
關於 oom_adj 的說明如下:
OOM_ADJ
其中紅色部分代表比較容易被殺死的 Android 程序( OOM_ADJ>=4 ),綠色部分表示不容易被殺死的 Android 程序,其他表示非 Android 程序(純 Linux 程序)。在 LMS 回收記憶體時會根據程序的級別優先殺死 OOM_ADJ 比較大的程序,對於優先順序相同的程序則進一步受到程序所佔記憶體和程序存活時間的影響。
Android 中程序可能被殺死的情況如下:
Android程序可能被殺死情況
4、程序保活
重複下文章開頭說的 Android 程序保活的兩個方案:
-
執行中保活:提高程序優先順序,降低被系統 kill 的概率
-
被 kill 後拉活:被系統 kill 後,將程序拉活(重啟)
5、執行中保活
通過前面章節的論述,我們知道,假設 APP 程序能夠一直被認為是前臺程序,那麼系統就有可能永遠不會殺死該程序。當然,這是不可能的,當我們將 APP 退回到後臺,改 APP 所屬程序就不屬於 前臺程序了。但是上述假設也讓我們有了靈感不是,只要我們儘可能的提高程序的優先順序,不就可以最大概率的降低被系統 kill 的可能性了。
那麼,提高程序優先順序的方法有哪些呢?
1、利用 Activity 提高許可權
監聽手機鎖屏解鎖事件,鎖屏時啟動一個1畫素的 Activity ,解鎖時將該 Activity 銷燬。此方法能將程序在鎖屏狀態下提高到最高前臺程序( oom_adj 為 0 )的級別。避免出現一些讓使用者困擾(體驗不好)的情況,該 Activity 需設計成使用者無感知。
此方案主要解決為了達到省電目的,一些第三方應用或者系統管理工具在檢測到鎖屏之後一段時間(一般是 5 分鐘)內會殺死後臺程序。
下面是例項程式碼:
1畫素 Activity:
class KeepLiveActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
KeepLiveManager.keepLiveActivity = this
// 設定Activity在左上角
window.setGravity(Gravity.START)
// 設定window的畫素為1
window.attributes.run {
x = 0
y = 0
width = 1
height = 1
}
}
}
注意,這裡一定要設定啟動模式為 singleInstance,使該 Activity 單獨一個 Activity 回退棧,否則在鎖屏且 APP 在後臺執行時,啟動該 Activity 後,會程序帶入前臺,解鎖後顯示該APP介面,體驗不好
<activity
android:name=".KeepLiveActivity"
android:launchMode="singleInstance"/>
廣播接收者:
class KeepLiveReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("KeepLiveReceiver", "action = ${intent.action}")
when(intent.action) {
// 鎖屏
Intent.ACTION_SCREEN_OFF -> {
KeepLiveManager.startKeepLiveActivity(context)
}
// 解鎖
Intent.ACTION_USER_PRESENT -> {
KeepLiveManager.finishKeepLiveActivity()
}
}
}
}
管理單例:
object KeepLiveManager {
var keepLiveActivity: KeepLiveActivity? = null
fun startKeepLiveActivity(context: Context) {
val intent = Intent(context, KeepLiveActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
fun finishKeepLiveActivity() {
keepLiveActivity?.finish()
}
}
注:由於鎖屏、解鎖的動作頻率極高,該類廣播在清單檔案中註冊無效,需要啟動服務來註冊廣播,此部分程式碼和本章主題關係不大,就不貼程式碼了。思路就是:啟動 APP 時,啟動一個註冊服務,在服務的 onCreate() 方法中註冊廣播,在 onDestory() 方法中登出廣播。
以下是使用該方案保活前和保活後檢視 oom_adj 的對比截圖:
保活前
保活後
可見,保活後在鎖屏狀態,將程序的 oom_adj 由原來的7提高到了 0 。
附:檢視程序 oom_adj 的方法
在命令列中使用以下兩個命令
adb shell ps | greppackageName
adb shell cat /proc/PID/oom_adj
如:
G:AndroidGithubKeepLive>adb shell
shell@armani:/ $ ps | grep com.cy.keeplive
u0_a35991213541548 29976 ffffffff 00000000 S com.cy.keeplive
shell@armani:/ $ cat /proc/5991/oom_adj
0
2、利用 Notification 提升許可權
通過 setForeground() 方法可以將後臺 Service 設定為前臺 Service,可以將服務程序優先順序提升為與可見程序一致,這將有效提高程序的優先順序,從而大大降低程序被kill的概率。
通過 setForeground() 將後臺 Service 設定為前臺 Service 時,必須在系統的通知欄傳送一條通知,也就是說前臺 Service 必須繫結一條可見的通知。
在通知欄傳送一條通知,是使用者可以感知到的,這可能會對使用者造成一定的困擾。可以通過實現一個內部 Service,在外部和內部 Service 中同時傳送具有相同 ID 的 Notifacation ,然後將內部 Service 結束。隨著內部 Service 的結束,Notification 也會消失掉,但系統的優先順序仍然提高了。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
Notification notification = new Notification();
if (Build.VERSION.SDK_INT < 18) {
startForeground(NOTIFICATION_ID, notification);
} else {
startForeground(NOTIFICATION_ID, notification);
// start InnerService
startService(new Intent(this, InnerService.class));
}
} catch (Throwable e) {
e.printStackTrace();
}
return super.onStartCommand(intent, flags, startId);
}
然後在內部 Service 中也啟動一個相同 ID 的 Notifacation ,並呼叫 stopSelf() 方法,結束內部 Service:
@Override
public void onCreate() {
super.onCreate();
try {
startForeground(NOTIFICATION_ID, new Notification());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
stopSelf();
}
6、被 kill 後拉活
此類方法暫未實踐,後續補充。暫時簡單提一下前人研究的可行性方案,不過這類方案都多多少少存在限制條件或者版本相容性問題。
1、通過系統廣播拉活
簡單講就是監聽一些特定的系統廣播,當系統發出這些廣播時,即可相應事件拉活。
2、利用第三方應用廣播拉活
此方案和第1中方案類似,不同的時該方案接收第三方 TOP 應用廣播。通過反編譯第三方 TOP 應用(如 QQ、微信、支付寶等,以及個推、極光推送等推送 SDK ),找出它們外放的廣播進行監聽,響應相應廣播事件拉活。
3、利用系統Service機制拉活
將 Service 設定為 START_STICKY,利用系統機制在 Service 掛掉後拉活。
4、雙程序守護
通過雙程序的 Service 相互繫結,在一個程序被 kill 時,另一個程序將其拉活。
5、利用 JobScheduler 機制拉活
JobScheduler 允許在特定狀態與特定時間間隔週期執行任務。可以利用它的這個特點完成保活的功能,效果類似開啟一個定時器,與普通定時器不同的是其排程由系統完成。它是在 Android5.0 之後推出的,在 5.0 之前無法使用。
6、利用 Native 程序拉活
利用 Linux 中的 fork 機制建立 Native 程序,在 Native 程序中監控主程序的存活,當主程序掛掉後,在 Native 程序中立即對主程序進行拉活。
原文釋出時間為:2018-11-08
本文作者:cspecialy