上個月在知乎上發表的由「袁輝輝」分享的關於TIM進程永生方面的文章,短期內受到大量關注,惋惜在短短的幾十個小時後,就在一股神祕力量的干預下被強行刪除了。。。html
▲ 該文在知乎上從發佈到刪除的時間歷程(中間省略了N條讀者的評論)java
在《史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術》一文從新整理髮布後的數小時內,做者快速響應,針對TIM進程永生這個話題,對Android進程永生技術進行了終極揭密,從Android系統源碼層面,通俗易懂地講解了Andorid進程被殺的底層原理(也便是本文將要分享的內容),並詳細探討APP如何對抗系統被殺的技巧實踐(並同時提供了參考實現代碼)。node
本文的技術原理講解透徹、系統源碼分享到位、樣例代碼也頗有參考意義,但願能對有一樣興趣愛好的Android開發者、IM開發者、推送系統開發者等,帶來對於Android進程保活技術的深刻理解。python
* 鄭重申明:本文的技術研究和分析過程,僅供技術愛好者學習的用途,請勿用做非法用途。若有不妥,請聯繫做者。android
田維術: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及以上的保活實踐(進程防殺篇)》
同時,系統的軟弱致使了 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系統的變化。
通常來講,Android 進程保活分爲兩個方面:
1)保持進程不被系統殺死;
2)進程被系統殺死以後,能夠從新復活。
隨着 Android 系統變得愈來愈完善,單單經過本身拉活本身逐漸變得不可能了。
所以,後面的所謂「保活」基本上是兩條路:
1)提高本身進程的優先級,讓系統不要輕易弄死本身;
2)App 之間互相結盟,一個兄弟死了其餘兄弟把它拉起來。
固然,還有一種終極方法,那就是跟各大系統廠商創建 PY 關係,把本身加入系統內存清理的白名單——好比說國民應用微信。固然這條路通常人是沒有資格走的。
大約一年之前,大神袁輝輝(gityuan)在其博客上公佈了 TIM 使用的一種能夠稱之爲「終極永生術」的保活方法(即從新整後的《史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術》一文)。
這種方法在當前 Android 內核的實現上能夠大大提高進程的存活率。本文做者研究了這種保活思路的實現原理,而且提供了一個參考實現:https://github.com/tiann/Leoric。而這些,正是本文接下來要分享的內容。
知己知彼,百戰不殆。既然咱們想要保活,那麼首先得知道咱們是怎麼死的。
通常來講,系統殺進程有兩種方法,這兩個方法都經過 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 還有進程存在,那不就僥倖逃脫了嗎?
那麼,如何實現逃脫被殺呢?咱們看這個關鍵的 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,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。
分析到這裏,這種方案的大體原理咱們已經清晰了。基於以上原理,我寫了一個簡單的驗證性代碼(代碼在下方)有興趣的能夠看一下。
本文內容所涉及的驗證性代碼演示下載地址:
上節技術方案的原理仍是比較簡單直觀的,可是要實現穩定的保活,還須要不少細節要補充。特別是那與死神賽跑的 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 的這種保活行爲。固然,若是你喜歡別的這種「凍結」類型的應用,好比小黑屋或者太極的陰陽之門也是能夠的。
其餘不是經過「凍結」這種機制來壓制後臺的應用理論上對這種保活方案的做用很是有限。
對技術來講,黑科技沒有什麼黑的,不過是對系統底層原理的深刻了解從而反過來對抗系統的一種手段。不少人會說,瞭解系統底層有什麼用,本文應該能夠給出一個答案:能夠實現別人永遠也沒法實現的功能,經過技術推進產品,從而產生巨大的商業價值。
黑科技雖強,可是它不應存在於這世上。沒有規矩,不成方圓。黑科技黑的了一時,黑不了一世。要提高產品的存活率,終歸要落到產品自己上面來,尊重用戶,提高體驗方是正途。(本文同步發佈於:http://www.52im.net/thread-2921-1-1.html)