關於 Android 程式保活,你所需要知道的一切

D_clock愛吃蔥花發表於2016-04-19

早前,我在知乎上回答了這樣一個問題:怎麼讓 Android 程式一直後臺執行,像 QQ 一樣不被殺死?。關於 Android 平臺的程式保活這一塊,想必是所有 Android 開發者矚目的內容之一。你到網上搜 Android 程式保活,可以搜出各種各樣神乎其技的做法,絕大多數都是極其不靠譜。前段時間,Github還出現了一個很火的“黑科技”程式保活庫,聲稱可以做到程式永生不死

懷著學習和膜拜的心情進去Github圍觀,結果發現很多人提了 Issue 說各種各樣的機子無法成功保活。

看到這裡,我瞬間就放心了。坦白的講,我是真心不希望有這種黑科技存在的,它只會滋生更多的流氓應用,拖垮我大 Android 平臺的流暢性。

扯了這麼多,接下來就直接進入本文的正題,談談關於程式保活的知識。提前宣告以下四點

  • 本文是本人開發 Android 至今綜合各方資料所得

  • 不以節能來維持程式保活的手段,都是耍流氓

  • 本文不是教你做永生不死的程式,如果指望實現程式永生不死,請忽略本文

  • 本文有錯誤的地方,歡迎留下評論互相探討(拍磚請輕拍)

保活手段

當前業界的Android程式保活手段主要分為 黑、白、灰 三種,其大致的實現思路如下:

黑色保活:不同的app程式,用廣播相互喚醒(包括利用系統提供的廣播進行喚醒)

白色保活:啟動前臺Service

灰色保活:利用系統的漏洞啟動前臺Service

黑色保活

所謂黑色保活,就是利用不同的app程式使用廣播來進行相互喚醒。舉個3個比較常見的場景:

場景1:開機,網路切換、拍照、拍視訊時候,利用系統產生的廣播喚醒app

場景2:接入第三方SDK也會喚醒相應的app程式,如微信sdk會喚醒微信,支付寶sdk會喚醒支付寶。由此發散開去,就會直接觸發了下面的 場景3

場景3:假如你手機裡裝了支付寶、淘寶、天貓、UC等阿里系的app,那麼你開啟任意一個阿里系的app後,有可能就順便把其他阿里系的app給喚醒了。(只是拿阿里打個比方,其實BAT系都差不多)

沒錯,我們的Android手機就是一步一步的被上面這些場景給拖卡機的。

針對場景1,估計Google已經開始意識到這些問題,所以在最新的Android N取消了 ACTION_NEW_PICTURE(拍照),ACTION_NEW_VIDEO(拍視訊),CONNECTIVITY_ACTION(網路切換)等三種廣播,無疑給了很多app沉重的打擊。我猜他們的心情是下面這樣的

而開機廣播的話,記得有一些定製ROM的廠商早已經將其去掉。

針對場景2場景3,因為呼叫SDK喚醒app程式屬於正常行為,此處不討論。但是在藉助LBE分析app之間的喚醒路徑的時候,發現了兩個問題:

  1. 很多推送SDK也存在喚醒app的功能

  2. app之間的喚醒路徑真是多,且錯綜複雜

我把自己使用的手機測試結果給大家圍觀一下(我的手機是小米4C,刷了原生的Android5.1系統,且已經獲得Root許可權才能檢視這些喚醒路徑

blob.png

15組相互喚醒路徑

blob.png

全部喚醒路徑

我們直接點開 簡書 的喚醒路徑進行檢視

blob.png

簡書喚醒路徑

可以看到以上3條喚醒路徑,但是涵蓋的喚醒應用總數卻達到了23+43+28款,數目真心驚人。請注意,這只是我手機上一款app的喚醒路徑而已,到了這裡是不是有點細思極恐。

當然,這裡依然存在一個疑問,就是LBE分析這些喚醒路徑和互相喚醒的應用是基於什麼思路,我們不得而知。所以我們也無法確定其分析結果是否準確,如果有LBE的童鞋看到此文章,不知可否告知一下思路呢?但是,手機開啟一個app就喚醒一大批,我自己可是親身體驗到這種酸爽的......

白色保活

白色保活手段非常簡單,就是呼叫系統api啟動一個前臺的Service程式,這樣會在系統的通知欄生成一個Notification,用來讓使用者知道有這樣一個app在執行著,哪怕當前的app退到了後臺。如下方的LBE和QQ音樂這樣:

blob.png

灰色保活

灰色保活,這種保活手段是應用範圍最廣泛。它是利用系統的漏洞來啟動一個前臺的Service程式,與普通的啟動方式區別在於,它不會在系統通知欄處出現一個Notification,看起來就如同執行著一個後臺Service程式一樣。這樣做帶來的好處就是,使用者無法察覺到你執行著一個前臺程式(因為看不到Notification),但你的程式優先順序又是高於普通後臺程式的。那麼如何利用系統的漏洞呢,大致的實現思路和程式碼如下:

  • 思路一:API < 18,啟動前臺Service時直接傳入new Notification();

  • 思路二:API >= 18,同時啟動兩個id相同的前臺Service,然後再將後啟動的Service做stop處理;

public class GrayService extends Service {

    private final static int GRAY_SERVICE_ID = 1001;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Build.VERSION.SDK_INT < 18) {
            startForeground(GRAY_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隱藏Notification上的圖示
        } else {
            Intent innerIntent = new Intent(this, GrayInnerService.class);
            startService(innerIntent);
            startForeground(GRAY_SERVICE_ID, new Notification());
        }

        return super.onStartCommand(intent, flags, startId);
    }

    ...
    ...

    /**
     * 給 API >= 18 的平臺上用的灰色保活手段
     */
    public static class GrayInnerService extends Service {

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            startForeground(GRAY_SERVICE_ID, new Notification());
            stopForeground(true);
            stopSelf();
            return super.onStartCommand(intent, flags, startId);
        }

    }
}

程式碼大致就是這樣,能讓你神不知鬼不覺的啟動著一個前臺Service。其實市面上很多app都用著這種灰色保活的手段,什麼?你不信?好吧,我們來驗證一下。流程很簡單,開啟一個app,看下系統通知欄有沒有一個 Notification,如果沒有,我們就進入手機的adb shell模式,然後輸入下面的shell命令

dumpsys activity services PackageName

列印出指定包名的所有程式中的Service資訊,看下有沒有 isForeground=true 的關鍵資訊。如果通知欄沒有看到屬於app的 Notification 且又看到 isForeground=true 則說明了,此app利用了這種灰色保活的手段。

下面分別是我手機上微信、qq、支付寶、陌陌的測試結果,大家有興趣也可以自己驗證一下。

blob.png

微信

blob.png

手Q

blob.png

支付寶

blob.png

陌陌

其實Google察覺到了此漏洞的存在,並逐步進行封堵。這就是為什麼這種保活方式分 API >= 18 和 API < 18 兩種情況,從Android5.0的ServiceRecord類的postNotification函式原始碼中可以看到這樣的一行註釋

blob.png

當某一天 API >= 18 的方案也失效的時候,我們就又要另謀出路了。需要注意的是,使用灰色保活並不代表著你的Service就永生不死了,只能說是提高了程式的優先順序。如果你的app程式佔用了大量的記憶體,按照回收程式的策略,同樣會幹掉你的app。感興趣於灰色保活是如何利用系統漏洞不顯示 Notification 的童鞋,可以研究一下系統的 ServiceRecord、NotificationManagerService 等相關原始碼,因為不是本文的重點,所以不做詳述。

嘮叨的分割線

到這裡基本就介紹完了 黑、白、灰 三種實現方式,僅僅從程式碼層面去講保活是不夠的,我希望能夠通過系統的程式回收機制來理解保活,這樣能夠讓我們更好的避免踩到程式被殺的坑。

程式回收機制

熟悉Android系統的童鞋都知道,系統出於體驗和效能上的考慮,app在退到後臺時系統並不會真正的kill掉這個程式,而是將其快取起來。開啟的應用越多,後臺快取的程式也越多。在系統記憶體不足的情況下,系統開始依據自身的一套程式回收機制來判斷要kill掉哪些程式,以騰出記憶體來供給需要的app。這套殺程式回收記憶體的機制就叫 Low Memory Killer ,它是基於Linux核心的 OOM Killer(Out-Of-Memory killer)機制誕生。

瞭解完 Low Memory Killer,再科普一下oom_adj。什麼是oom_adj?它是linux核心分配給每個系統程式的一個值,代表程式的優先順序,程式回收機制就是根據這個優先順序來決定是否進行回收。對於oom_adj的作用,你只需要記住以下幾點即可:

  • 程式的oom_adj越大,表示此程式優先順序越低,越容易被殺回收;越小,表示程式優先順序越高,越不容易被殺回收

  • 普通app程式的oom_adj>=0,系統程式的oom_adj才可能<0

那麼我們如何檢視程式的oom_adj值呢,需要用到下面的兩個shell命令

ps | grep PackageName //獲取你指定的程式資訊

blob.png

這裡是以我寫的demo程式碼為例子,紅色圈中部分別為下面三個程式的ID

UI程式:com.clock.daemon
普通後臺程式:com.clock.daemon:bg
灰色保活程式:com.clock.daemon:gray

當然,這些程式的id也可以通過AndroidStudio獲得

blob.png

接著我們來再來獲取三個程式的oom_adj

cat /proc/程式ID/oom_adj

blob.png

從上圖可以看到UI程式和灰色保活Service程式的oom_adj=0,而普通後臺程式oom_adj=15。到這裡估計你也能明白,為什麼普通的後臺程式容易被回收,而前臺程式則不容易被回收了吧。但明白這個還不夠,接著看下圖

blob.png

上面是我把app切換到後臺,再進行一次oom_adj的檢驗,你會發現UI程式的值從0變成了6,而灰色保活的Service程式則從0變成了1。這裡可以觀察到,app退到後臺時,其所有的程式優先順序都會降低。但是UI程式是降低最為明顯的,因為它佔用的記憶體資源最多,系統記憶體不足的時候肯定優先殺這些佔用記憶體高的程式來騰出資源。所以,為了儘量避免後臺UI程式被殺,需要儘可能的釋放一些不用的資源,尤其是圖片、音視訊之類的

從Android官方文件中,我們也能看到優先順序從高到低列出了這些不同型別的程式:Foreground processVisible processService processBackground processEmpty process。而這些程式的oom_adj分別是多少,又是如何掛鉤起來的呢?推薦大家閱讀下面這篇文章:

http://www.cnblogs.com/angeldevil/archive/2013/05/21/3090872.html

總結

絮絮叨叨寫完了這麼多,最後來做個小小的總結。迴歸到開篇提到QQ程式不死的問題,我也曾認為存在這樣一種技術。可惜我把手機root後,殺掉QQ程式之後就再也起不來了。有些手機廠商把這些知名的app放入了自己的白名單中,保證了程式不死來提高使用者體驗(如微信、QQ、陌陌都在小米的白名單中)。如果從白名單中移除,他們終究還是和普通app一樣躲避不了被殺的命運,為了儘量避免被殺,還是老老實實去做好優化工作吧。

所以,程式保活的根本方案終究還是回到了效能優化上,程式永生不死終究是個徹頭徹尾的偽命題!

文章到此結束,相關簡單的實踐程式碼請看

https://github.com/D-clock/AndroidDaemonService

文/D_clock(簡書作者)
原文連結:http://www.jianshu.com/p/63aafe3c12af  

相關文章