一直以來,App 進程保活都是各大廠商,特別是頭部應用開發商永恆的追求。java
畢竟App 進程死了,就什麼也幹不了了;一旦 App 進程死亡,那就再也沒法在用戶的手機上開展任何業務,全部的商業模型在用戶側都沒有立錐之地了。android
早期的 Android 系統不完善,致使 App 側有不少空子能夠鑽,所以它們有着有着各類各樣的姿式進行保活。git
譬如說在 Android 5.0 之前,App 內部經過 native 方式 fork 出來的進程是不受系統管控的,系統在殺 App 進程的時候,只會去殺 App 啓動的 Java 進程。程序員
所以誕生了一大批「毒瘤」,他們經過 fork native 進程,在 App 的 Java 進程被殺死的時候經過 am命令拉起本身從而實現永生。github
那時候的 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,這個庫經過雙進程守護的方式實現保活,一時間風頭無兩。app
不過好景不長,進入 Android 8.0 時代以後,這個庫就逐漸消亡。
通常來講,Android 進程保活分爲兩個方面:
保持進程不被系統殺死。
進程被系統殺死以後,能夠從新復活。
隨着 Android 系統變得愈來愈完善,單單經過本身拉活本身逐漸變得不可能了;所以後面的所謂「保活」基本上是兩條路:
提高本身進程的優先級,讓系統不要輕易弄死本身;
App 之間互相結盟,一個兄弟死了其餘兄弟把它拉起來。
固然,還有一種終極方法,那就是跟各大系統廠商創建 PY 關係,把本身加入系統內存清理的白名單;好比說國民應用微信。固然這條路通常人是沒有資格走的。
大約一年之前,大神 gityuan 在其博客上公佈了 TIM 使用的一種能夠稱之爲「終極永生術」的保活方法;這種方法在當前 Android 內核的實現上能夠大大提高進程的存活率。筆者研究了這種保活思路的實現原理,而且提供了一個參考實現 Leoric。
接下來就給你們分享一下這個終極保活黑科技的實現原理。
知己知彼,百戰不殆。
既然咱們想要保活,那麼首先得知道咱們是怎麼死的。
通常來講,系統殺進程有兩種方法,這兩個方法都經過 ActivityManagerService 提供:
killBackgroundProcesses
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() : new int[] { userId }; for (int user : users) { // 狀態判斷,省略.. int pkgUid = -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 個機制來保證進程之間的互相拉起:
2 個進程經過互相監聽文件鎖的方式,來感知彼此的死亡。
經過 fork 產生子進程,fork 的進程同屬一個進程組,一個被殺以後會觸發另一個進程被殺,從而被文件鎖感知。
具體來講,建立 2 個進程 p1, p2,這兩個進程經過文件鎖互相關聯,一個被殺以後拉起另一個;同時 p1 通過 2 次 fork 產生孤兒進程 c1,p2 通過 2 次 fork 產生孤兒進程 c2,c1 和 c2 之間創建文件鎖關聯。這樣假設 p1 被殺,那麼 p2 會立馬感知到,而後 p1 和 c1 同屬一個進程組,p1 被殺會觸發 c1 被殺,c1 死後 c2 立馬感覺到從而拉起 p1,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。
分析到這裏,這種方案的大體原理咱們已經清晰了。
基於以上原理,我寫了一個簡單的 PoC,代碼在這裏:
https://github.com/tiann/Leoric
有興趣的能夠看一下。
爲了文章的嚴謹性(注一位讀者Rikka的回覆):
文章中說須要「在 5ms 內啓動一堆新的進程」,但其實並不須要。
AMS 在執行殺進程時是一個 ProcessRecord 一個地來的( https://android.googlesource.com/platform/frameworks/base/+/4f868ed/services/core/java/com/android/server/am/ActivityManagerService.java#5766),也就是最終會執行屢次 libprocessgroup 裏的 killProcessgroup。
這樣只要在殺死屬於某個 cgroup 的進程時,另外的進程只要成功啓動一次 android:process 是另外的的進程便可活下來。由於新對應新的 ProcessRecord,不會在上面那個循環裏被殺死。此外,循環四十次反而給了超長的時間來啓動新的,觀察 log 能夠發現 killProcessgroup 的間隔長達幾十到一百多 ms。
本方案的原理仍是比較簡單直觀的,可是要實現穩定的保活,還須要不少細節要補充;特別是那與死神賽跑的 5ms,須要不計一切代價去優化才能提高成功率。
具體來講,就是當前的實現是在 Java 層用 binder 調用的,咱們應該在 native 層完成。筆者曾經實現過這個方案,可是這個庫本質上是有損用戶利益的,所以並不打算公開代碼,這裏簡單提一下實現思路供你們學習:
如何在 native 層進行 binder 通訊?
libbinder 是 NDK 公開庫,拿到對應頭文件,動態連接便可。
難點:依賴繁多,剝離頭文件是個體力活。
如何組織 binder 通訊的數據?
通訊的數據其實就是二進制流;具體表現就是 (C++/Java) Parcel 對象。native 層沒有對應的 Intent Parcel,兼容性差。
方案:
Java 層建立 Parcel (含 Intent),拿到 Parcel 對象的 mNativePtr(native peer),傳到 Native 層。
native 層直接把 mNativePtr 強轉爲結構體指針。
fork 子進程,創建管道,準備傳輸 parcel 數據。
子進程讀管道,拿到二進制流,重組爲 parcel。
今天我把這個實現原理公開,而且提供 PoC 代碼,並非鼓勵你們使用這種方式保活,而是但願各大系統廠商能感知到這種黑科技的存在,推進本身的系統完全解決這個問題。
兩年前我就知道了這個方案的存在,不過當時不爲人知。
最近一個月我發現不少 App 都使用了這種方案,把個人 Android 手機折騰的慘不忍睹;畢竟本人手機上安裝了將近 800 個 App,假設每一個 App 都用這個方案保活,那這系統就無法用了。
系統如何應對?
若是咱們把系統殺進程比喻爲斬首,那麼這個保活方案的精髓在於能快速長出一個新的頭;所以應對之法也很簡單,只要咱們在斬殺一個進程的時候,讓別的進程老老實實呆着別搞事情就 OK 了。具體的實現方法多種多樣,不贅述。
用戶如何應對?
在廠商沒有推出解決方案以前,用戶能夠有一些方案來緩解使用這個方案進行保活的流氓 App。
這裏推薦兩個應用給你們:
冰箱
Island
經過冰箱的凍結和 Island 的深度休眠能夠完全阻止 App 的這種保活行爲。固然,若是你喜歡別的這種「凍結」類型的應用,好比小黑屋或者太極的陰陽之門也是能夠的。
其餘不是經過「凍結」這種機制來壓制後臺的應用理論上對這種保活方案的做用很是有限。
對技術來講,黑科技沒有什麼黑的,不過是對系統底層原理的深刻了解從而反過來對抗系統的一種手段。不少人會說,瞭解系統底層有什麼用,本文應該能夠給出一個答案:能夠實現別人永遠也沒法實現的功能,經過技術推進產品,從而產生巨大的商業價值。
黑科技雖強,可是它不應存在於這世上。沒有規矩,不成方圓。黑科技黑的了一時,黑不了一世。要提高產品的存活率,終歸要落到產品自己上面來,尊重用戶,提高體驗方是正途。
最後小編想說:不論之後選擇什麼方向發展,目前重要的是把Android方面的技術學好,畢竟其實對於程序員來講,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提高本身,歷來都是咱們去適應環境,而不是環境來適應咱們!
當程序員容易,當一個優秀的程序員是須要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每一個階段都須要掌握不一樣的能力。早早肯定本身的職業方向,才能在工做和能力提高中甩開同齡人。
想要拿高薪實現技術提高薪水獲得質的飛躍。最快捷的方式,就是有人能夠帶着你一塊兒分析,這樣學習起來最爲高效,因此爲了你們可以順利進階中高級、架構師,我特意爲你們準備了一套高手學習的源碼和框架視頻等精品Android架構師教程,保證你學了之後保證薪資上升一個臺階。(如下是一小部分,獲取更多其餘精講進階架構視頻資料能夠關注【個人主頁】或者【簡信我】獲取免費領取方式)
當你有了學習線路,學習哪些內容,也知道之後的路怎麼走了,理論看多了總要實踐的。
如下是今天給你們分享的一些獨家乾貨:
【Android開發核心知識點筆記】
【Android思惟腦圖(技能樹)】
【Android核心高級技術PDF文檔,BAT大廠面試真題解析】
【Android高級架構視頻學習資源】
Android精講視頻領取學習後更加是如虎添翼!進軍BATJ大廠等(備戰)!如今都說互聯網寒冬,其實無非就是你上錯了車,且穿的少(技能),要是你上對車,自身技術能力夠強,公司換掉的代價大,怎麼可能會被裁掉,都是淘汰末端的業務Curd而已!現現在市場上初級程序員氾濫,這套教程針對Android開發工程師1-6年的人員、正處於瓶頸期,想要年後突破本身漲薪的,進階Android中高級、架構師對你更是如魚得水,趕快領取吧!
【Android進階學習視頻】、【全套Android面試祕籍】【簡信我學習】查看免費領取方式!
分享不易!喜歡的朋友別忘了關注+點贊!
原文做者:鴻洋
原文連接:https://mp.weixin.qq.com/s/bkHP-BiwTeQhqKvze2jtdQ