Android進程永生技術終極揭祕:進程被殺底層原理、APP應對技巧

一、引言

上個月在知乎上發表的由「袁輝輝」分享的關於TIM進程永生方面的文章(即時通信網從新整理後的標題是:《史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術》),短期內受到大量關注,惋惜在短短的幾十個小時後,就在一股神祕力量的干預下被強行刪除了。。。php

 

▲ 該文在知乎上從發佈到刪除的時間歷程(中間省略了N條讀者的評論)html

在《史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術》一文從新整理髮布後的數小時內,做者田維術(博客名:Weishu)快速響應,針對TIM進程永生這個話題,對Android進程永生技術進行了終極揭密,從Android系統源碼層面,通俗易懂地講解了Andorid進程被殺的底層原理(也便是本文將要分享的內容),並詳細探討APP如何對抗系統被殺的技巧實踐(並同時提供了參考實現代碼)。java

本文的技術原理講解透徹、系統源碼分享到位、樣例代碼也頗有參考意義,但願能對有一樣興趣愛好的Android開發者、IM開發者、推送系統開發者等,帶來對於Android進程保活技術的深刻理解。node

* 鄭重申明:本文的技術研究和分析過程,僅供技術愛好者學習的用途,請勿用做非法用途。若有不妥,請聯繫做者。python

(本文同步發佈於:http://www.52im.net/thread-2921-1-1.htmlandroid

二、本文做者

田維術:90後,畢業於華中科技大學EE專業。曾就任於支付寶,作客戶端性能優化。現創業中。git

骨灰級Android開發,曾混跡於Donut史前時代。後陸續入坑J2EE, python, rails, C++, node。現專攻Android,業餘Haskell。程序員

做者博客:http://weishu.megithub

做者Github:https://github.com/tiann緩存

三、混亂的進程保活,一個黑暗的時代

一直以來,App 進程保活都是各大廠商,特別是頭部應用開發商永恆的追求。畢竟App 進程死了,就什麼也幹不了了。一旦 App 進程死亡,那就再也沒法在用戶的手機上開展任何業務,全部的商業模型在用戶側都沒有立錐之地了。

早期的 Android 系統不完善,致使 App 側有不少空子能夠鑽,所以它們有着有着各類各樣的姿式進行保活。

 

▲ 這臺手機,應該能勾起不少老Android程序員的回憶

譬如說在 Android 5.0 之前,App 內部經過 native 方式 fork 出來的進程是不受系統管控的,系統在殺 App 進程的時候,只會去殺 App 啓動的 Java 進程。所以誕生了一大批「毒瘤」,他們經過 fork native 進程,在 App 的 Java 進程被殺死的時候經過 am命令拉起本身從而實現永生。

那時候的 Android 可謂是魑魅橫行,羣魔亂舞,系統根本管不住應用,所以長期以來被人詬病耗電、卡頓。

好比如下這幾篇中介紹的Android保活方法:

應用保活終極總結(一):Android6.0如下的雙進程守護保活實踐

應用保活終極總結(二):Android6.0及以上的保活實踐(進程防殺篇)

應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)

微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)

同時,系統的軟弱致使了 Xposed 框架阻止運行綠色守護黑域冰箱等一系列管制系統後臺進程的框架和 App 出現。

四、限制進程保活,大勢所趨

不過,隨着 Android 系統的發展,這一切都在往好的方向演變。

 

Android 5.0 以上,系統殺進程以 uid 爲標識,經過殺死整個進程組來殺進程,所以 native 進程也躲不過系統的法眼。

Android 6.0 引入了待機模式(doze),一旦用戶拔下設備的電源插頭,並在屏幕關閉後的一段時間內使其保持不活動狀態,設備會進入低電耗模式,在該模式下設備會嘗試讓系統保持休眠狀態。

Android 7.0 增強了以前雞肋的待機模式(再也不要求設備靜止狀態),同時對開啓了 Project Svelte。Project Svelte 是專門用來優化 Android 系統後臺的項目,在 Android 7.0 上直接移除了一些隱式廣播,App 沒法再經過監聽這些廣播拉起本身。

Android 8.0 進一步增強了應用後臺執行限制:一旦應用進入已緩存狀態時,若是沒有活動的組件,系統將解除應用具備的全部喚醒鎖。另外,系統會限制未在前臺運行的應用的某些行爲,好比說應用的後臺服務的訪問受到限制,也沒法使用 Mainifest 註冊大部分隱式廣播。

Android 9.0 進一步改進了省電模式的功能並加入了應用待機分組,長時間不用的 App 會被打入冷宮。另外,系統監測到應用消耗過多資源時,系統會通知並詢問用戶是否須要限制該應用的後臺活動。

然而,道高一尺,魔高一丈。系統在不斷演進,保活方法也在不斷髮展。

大約在 4 年前出現過一個 MarsDaemon,這個庫經過雙進程守護的方式實現保活,一時間風頭無兩。不過好景不長,進入 Android 8.0 時代以後,這個庫就逐漸消亡。

這篇《全面盤點當前Android後臺保活方案的真實運行效果(截止2019年前)》,盤點了那些經典的保活方法的有效狀況。

而這篇《2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!》,則直接放棄了曾今的保活的黑科技,轉而順應Android系統的變化。

五、進程永生技術,後Andriod保活時代的產物

通常來講,Android 進程保活分爲兩個方面:

1)保持進程不被系統殺死;

2)進程被系統殺死以後,能夠從新復活。

隨着 Android 系統變得愈來愈完善,單單經過本身拉活本身逐漸變得不可能了。

所以,後面的所謂「保活」基本上是兩條路:

1)提高本身進程的優先級,讓系統不要輕易弄死本身;

2)App 之間互相結盟,一個兄弟死了其餘兄弟把它拉起來。

固然,還有一種終極方法,那就是跟各大系統廠商創建 PY 關係,把本身加入系統內存清理的白名單——好比說國民應用微信。固然這條路通常人是沒有資格走的。

大約一年之前,大神袁輝輝(gityuan)在其博客上公佈了 TIM 使用的一種能夠稱之爲「終極永生術」的保活方法(即從新整後的《史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術》一文)。

這種方法在當前 Android 內核的實現上能夠大大提高進程的存活率。本文做者研究了這種保活思路的實現原理,而且提供了一個參考實現:https://github.com/tiann/Leoric。而這些,正是本文接下來要分享的內容。

 

六、Android保活的底層技術原理

知己知彼,百戰不殆。既然咱們想要保活,那麼首先得知道咱們是怎麼死的。

通常來講,系統殺進程有兩種方法,這兩個方法都經過 ActivityManagerService 提供:

1)killBackgroundProcesses;

2)forceStopPackage。

在原生系統上,不少時候殺進程是經過第一種方式,除非用戶主動在 App 的設置界面點擊「強制中止」。

不過國內各廠商以及一加三星等 ROM 如今通常使用第二種方法。由於第一種方法太過溫柔,根本治不住想要搞事情的應用。第二種方法就比較強力了,通常來講被 force-stop 以後,App 就只能乖乖等死了。

所以,要實現保活,咱們就得知道 force-stop 究竟是如何運做的。

既然如此,咱們就跟蹤一下系統的 forceStopPackage 這個方法的執行流程。

首先是 ActivityManagerService裏面的 forceStopPackage 這方法:

public void forceStopPackage(final String packageName, int userId) {

    // .. 權限檢查,省略

    long callingId = Binder.clearCallingIdentity();

    try{

        IPackageManager pm = AppGlobals.getPackageManager();

        synchronized(this) {

            int[] users = userId == UserHandle.USER_ALL ? mUserController.getUsers() : newint[] { userId };

            for(intuser : users) {

                // 狀態判斷,省略..

                intpkgUid = -1;

                try{

                    pkgUid = pm.getPackageUid(packageName,MATCH_DEBUG_TRIAGED_MISSING, user);

                } catch(RemoteException e) {

                }

                if(pkgUid == -1) {

                    Slog.w(TAG, "Invalid packageName: "+ packageName);

                    continue;

                }

                try{

                    pm.setPackageStoppedState(packageName, true, user);

                } catch(RemoteException e) {

                } catch(IllegalArgumentException e) {

                    Slog.w(TAG, "Failed trying to unstop package "

                            + packageName + ": "+ e);

                }

                if(mUserController.isUserRunning(user, 0)) {

                        // 根據 UID 和包名殺進程

                    forceStopPackageLocked(packageName, pkgUid, "from pid "+ callingPid);

                    finishForceStopPackageLocked(packageName, pkgUid);

                }

            }

        }

    } finally{

        Binder.restoreCallingIdentity(callingId);

    }

}

 

在這裏咱們能夠知道,系統是經過 uid 爲單位 force-stop 進程的,所以不論你是 native 進程仍是 Java 進程,force-stop 都會將你通通殺死。

咱們繼續跟蹤forceStopPackageLocked 這個方法:

final boolean forceStopPackageLocked(String packageName, int appId,

        boolean callerWillRestart, boolean purgeCache, boolean doit,

        boolean evenPersistent, boolean uninstalling, int userId, String reason) {

    int i;

    // .. 狀態判斷,省略

    boolean didSomething = mProcessList.killPackageProcessesLocked(packageName, appId, userId,

            ProcessList.INVALID_ADJ, callerWillRestart, true/* allowRestart */, doit,

            evenPersistent, true /* setRemoved */,

            packageName == null? ("stop user "+ userId) : ("stop "+ packageName));

    didSomething |= mAtmInternal.onForceStopPackage(packageName, doit, evenPersistent, userId);

    // 清理 service

    // 清理 broadcastreceiver

    // 清理 providers

    // 清理其餘

    return didSomething;

}

 

這個方法實現很清晰:先殺死這個 App 內部的全部進程,而後清理殘留在 system_server 內的四大組件信息。

咱們關心進程是如何被殺死的,所以繼續跟蹤killPackageProcessesLocked,這個方法最終會調用到 ProcessList 內部的 removeProcessLocked 方法,removeProcessLocked 會調用 ProcessRecord 的 kill 方法。

咱們看看這個kill:

void kill(String reason, boolean noisy) {

    if(!killedByAm) {

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");

        if(mService != null && (noisy || info.uid == mService.mCurOomAdjUid)) {

            mService.reportUidInfoMessageLocked(TAG,"Killing "+ toShortString() + " (adj "+ setAdj + "): "+ reason, info.uid);

        }

        if(pid > 0) {

            EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason);

            Process.killProcessQuiet(pid);

            ProcessList.killProcessGroup(uid, pid);

        } else{

            pendingStart = false;

        }

        if(!mPersistent) {

            killed = true;

            killedByAm = true;

        }

        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

    }

}

 

這裏咱們能夠看到,首先殺掉了目標進程,而後會以uid爲單位殺掉目標進程組。若是隻殺掉目標進程,那麼咱們能夠經過雙進程守護的方式實現保活。

關鍵就在於這個killProcessGroup,繼續跟蹤以後發現這是一個 native 方法,它的最終實如今 libprocessgroup中。

代碼以下:

int killProcessGroup(uid_t uid, int initialPid, int signal) {

    return KillProcessGroup(uid, initialPid, signal, 40/*retries*/);

}

注意這裏有個奇怪的數字:40。

咱們繼續跟蹤:

static int KillProcessGroup(uid_t uid, int initialPid, int signal, int retries) {

    // 省略

    int retry = retries;

    int processes;

    while((processes = DoKillProcessGroupOnce(cgroup, uid, initialPid, signal)) > 0) {

        LOG(VERBOSE) << "Killed "<< processes << " processes for processgroup "<< initialPid;

        if(retry > 0) {

            std::this_thread::sleep_for(5ms);

            --retry;

        } else{

            break;

        }

    }

    // 省略

}

 

瞧瞧咱們的系統作了什麼騷操做?循環 40 遍不停滴殺進程,每次殺完以後等 5ms,循環完畢以後就算過去了。

看到這段代碼,我想任何人都會蹦出一個疑問:假設經歷連續 40 次的殺進程以後,若是 App 還有進程存在,那不就僥倖逃脫了嗎?

七、APP對抗被殺的實現思路

那麼,如何實現逃脫被殺呢?咱們看這個關鍵的 5ms。

假設:App 進程在被殺掉以後,可以以足夠快的速度(5ms 內)啓動一堆新的進程,那麼系統在一次循環殺掉老的全部進程以後,sleep 5ms 以後又會遇到一堆新的進程。

如此循環 40 次,只要咱們每次都可以拉起新的進程,那咱們的 App 就能逃過系統的追殺,實現永生。

是的:煉獄般的 200ms,只要咱們熬過 200ms 就能渡劫成功,得道飛昇。不知道你們有沒有玩過打地鼠這個遊戲,整個過程很是相似,按下去一個又冒出一個,只要每次都能足夠快地冒出來,咱們就贏了。

如今問題的關鍵就在於:如何在 5ms 內啓動一堆新的進程?

再回過頭來看原來的保活方式:它們拉起進程最開始經過am命令,這個命令其實是一個 java 程序,它會經歷啓動一個進程而後啓動一個 ART 虛擬機,接着獲取 ams 的 binder 代理,而後與 ams 進行 binder 同步通訊。這個過程實在是太慢了,在這與死神賽跑的 5ms 裏,它的速度的確是不敢恭維。

後來:MarsDaemon 提出了一種新的方式,它用 binder 引用直接給 ams 發送 Parcel,這個過程相比 am命令快了不少,從而大大提升了成功率。

其實這裏還有改進的空間,畢竟這裏仍是在 Java 層調用,Java 語言在這種實時性要求極高的場合有一個很是使人詬病的特性:垃圾回收(GC)。

雖然咱們在這 5ms 內直接碰上 gc 引起停頓的可能性很是小,可是因爲 GC 的存在,ART 中的 Java 代碼存在很是多的 checkpoint。想象一下你如今是一個信使有重要軍情要報告,可是在路上卻碰到不少關隘,並且極可能被勒令暫時中止一下,這種狀況是不可接受的。

所以,最好的方法是經過 native code 給 ams 發送 binder 調用。固然,若是再底層一點,咱們甚至能夠經過 ioctl 直接給 binder 驅動發送數據進而完成調用,可是這種方法的兼容性比較差,沒有用 native 方式省心。

經過在 native 層給 ams 發送 binder 消息拉起進程,咱們算是解決了「快速拉起進程」這個問題。可是這個仍是不夠。

仍是回到打地鼠這個遊戲,假設你摁下一個地鼠,會冒起一個新的地鼠,那麼你每次都能摁下去最後獲取勝利的機率仍是比較高的;但若是你每次摁下一個地鼠,其餘全部地鼠都能冒出來呢?這個難度係數但是要高多了。若是咱們的進程可以在任意一個進程死亡以後,都能讓把其餘全部進程所有拉起,這樣系統就很難殺死咱們了。

新的黑科技保活中經過 2 個機制來保證進程之間的互相拉起:

1)2 個進程經過互相監聽文件鎖的方式,來感知彼此的死亡;

2)經過 fork 產生子進程,fork 的進程同屬一個進程組,一個被殺以後會觸發另一個進程被殺,從而被文件鎖感知。

具體來講:

1)建立 2 個進程 p一、p2,這兩個進程經過文件鎖互相關聯,一個被殺以後拉起另一個;

2)同時 p1 通過 2 次 fork 產生孤兒進程 c1,p2 通過 2 次 fork 產生孤兒進程 c2,c1 和 c2 之間創建文件鎖關聯。

這樣假設 p1 被殺,那麼 p2 會立馬感知到,而後 p1 和 c1 同屬一個進程組,p1 被殺會觸發 c1 被殺,c1 死後 c2 立馬感覺到從而拉起 p1,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。

分析到這裏,這種方案的大體原理咱們已經清晰了。基於以上原理,我寫了一個簡單的驗證性代碼(代碼在下方)有興趣的能夠看一下。

本文內容所涉及的驗證性代碼演示下載地址:

主地址:https://github.com/tiann/Leoric

備地址:https://github.com/52im/Leoric

八、對抗被殺技術實現的改進空間

上節技術方案的原理仍是比較簡單直觀的,可是要實現穩定的保活,還須要不少細節要補充。特別是那與死神賽跑的 5ms,須要不計一切代價去優化才能提高成功率。

具體來講,就是當前的實現是在 Java 層用 binder 調用的,咱們應該在 native 層完成。筆者曾經實現過這個方案,可是這個庫本質上是有損用戶利益的,所以並不打算公開代碼。這裏簡單提一下實現思路供你們學習。

如何在 native 層進行 binder 通訊?

libbinder 是 NDK 公開庫,拿到對應頭文件,動態連接便可。

難點:依賴繁多,剝離頭文件是個體力活。

如何組織 binder 通訊的數據?

通訊的數據其實就是二進制流,具體表現就是 (C++/Java) Parcel 對象。native 層沒有對應的 Intent Parcel,兼容性差。

可行的方案:

1)Java 層建立 Parcel (含 Intent),拿到 Parcel 對象的 mNativePtr(native peer),傳到 Native 層;

2)native 層直接把 mNativePtr 強轉爲結構體指針;

3)fork 子進程,創建管道,準備傳輸 parcel 數據;

4)子進程讀管道,拿到二進制流,重組爲 parcel。

九、如何應對本文探討的進程永生技術?

今天我把這個實現原理公開,而且提供驗證代碼,並非鼓勵你們使用這種方式保活,而是但願各大系統廠商能感知到這種黑科技的存在,推進本身的系統完全解決這個問題。

兩年前我就知道了這個方案的存在,不過當時不爲人知。最近一個月我發現不少 App 都使用了這種方案,把個人 Android 手機折騰的慘不忍睹。畢竟本人手機上安裝了將近 800 個 App,假設每一個 App 都用這個方案保活,那這系統就無法用了。

系統如何應對?

若是咱們把系統殺進程比喻爲斬首,那麼這個保活方案的精髓在於能快速長出一個新的頭;所以應對之法也很簡單,只要咱們在斬殺一個進程的時候,讓別的進程老老實實呆着別搞事情就 OK 了。具體的實現方法多種多樣,不贅述。

用戶如何應對?

在廠商沒有推出解決方案以前,用戶能夠有一些方案來緩解使用這個方案進行保活的流氓 App。

這裏推薦兩個應用給你們:

1)冰箱

2)Island

經過冰箱的凍結和 Island 的深度休眠能夠完全阻止 App 的這種保活行爲。固然,若是你喜歡別的這種「凍結」類型的應用,好比小黑屋或者太極的陰陽之門也是能夠的。

其餘不是經過「凍結」這種機制來壓制後臺的應用理論上對這種保活方案的做用很是有限。

十、本文小結

對技術來講,黑科技沒有什麼黑的,不過是對系統底層原理的深刻了解從而反過來對抗系統的一種手段。不少人會說,瞭解系統底層有什麼用,本文應該能夠給出一個答案:能夠實現別人永遠也沒法實現的功能,經過技術推進產品,從而產生巨大的商業價值。

黑科技雖強,可是它不應存在於這世上。沒有規矩,不成方圓。黑科技黑的了一時,黑不了一世。要提高產品的存活率,終歸要落到產品自己上面來,尊重用戶,提高體驗方是正途。

附錄:有關IM/推送的進程保活/網絡保活方面的文章彙總

應用保活終極總結(一):Android6.0如下的雙進程守護保活實踐

應用保活終極總結(二):Android6.0及以上的保活實踐(進程防殺篇)

應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)

Android進程保活詳解:一篇文章解決你的全部疑問

Android端消息推送總結:實現原理、心跳保活、遇到的問題等

深刻的聊聊Android消息推送這件小事

爲什麼基於TCP協議的移動端IM仍然須要心跳保活機制?

微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)

微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)

移動端IM實踐:實現Android版微信的智能心跳機制

移動端IM實踐:WhatsApp、Line、微信的心跳策略分析

Android P正式版即將到來:後臺應用保活、消息推送的真正噩夢

全面盤點當前Android後臺保活方案的真實運行效果(截止2019年前)

一文讀懂即時通信應用中的網絡心跳包機制:做用、原理、實現思路等

融雲技術分享:融雲安卓端IM產品的網絡鏈路保活技術實踐

正確理解IM長鏈接的心跳及重連機制,並動手實現(有完整IM源碼)

2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!

史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術

Android進程永生技術終極揭密:進程被殺底層原理、APP對抗被殺技巧

>> 更多同類文章 ……

(本文同步發佈於:http://www.52im.net/thread-2921-1-1.html

相關文章
相關標籤/搜索