歡迎光臨
每天分享高質量文章

Android 行程保活方案

作者:cspecialy

連結:https://www.jianshu.com/p/a407a9b1a3e6

0、前言

Android 系統為了保持系統執行流暢,在記憶體吃緊的情況下,會將一些行程 kill ,以釋放一部分記憶體。然而,對於一些(如:IM-QQ 、微信,支付-支付寶等)比較重要、我們希望能及時收到訊息的 APP,需要保持行程持續活躍,那麼就需要實施一些保活措施來保證行程能夠持續存活,即 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 | grep  packageName
adb shell cat /proc/PID/oom_adj

如:
G:AndroidGithubKeepLive>adb shell
shell@armani:/ $ ps |
 grep com.cy.keeplive
u0_a3     5991  213   541548 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 行程中立即對主行程進行拉活。


●編號355,輸入編號直達本文

●輸入m獲取到文章目錄

推薦↓↓↓

Java程式設計

更多推薦18個技術類公眾微信

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂