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

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
[email protected]:/ $ ps |
 grep com.cy.keeplive
u0_a3     5991  213   541548 29976 ffffffff 00000000 S com.cy.keeplive
[email protected]:/ $ 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)

分享創造快樂