詳解App?;顚?shí)現(xiàn)原理
概述
早期的 Android 系統(tǒng)不完善,導(dǎo)致 App 側(cè)有很多空子可以鉆,因此它們有著有著各種各樣的姿勢進(jìn)行?;?。譬如說在 Android 5.0 以前,App 內(nèi)部通過 native 方式 fork 出來的進(jìn)程是不受系統(tǒng)管控的,系統(tǒng)在殺 App 進(jìn)程的時(shí)候,只會去殺 App 啟動的 Java 進(jìn)程;因此誕生了一大批“毒瘤”,他們通過 fork native 進(jìn)程,在 App 的 Java 進(jìn)程被殺死的時(shí)候通過am命令拉起自己從而實(shí)現(xiàn)永生。那時(shí)候的 Android 可謂是魑魅橫行,群魔亂舞;系統(tǒng)根本管不住應(yīng)用,因此長期以來被人詬病耗電、卡頓。同時(shí),系統(tǒng)的軟弱導(dǎo)致了 Xposed 框架、阻止運(yùn)行、綠色守護(hù)、黑域、冰箱等一系列管制系統(tǒng)后臺進(jìn)程的框架和 App 出現(xiàn)。
不過,隨著 Android 系統(tǒng)的發(fā)展,這一切都在往好的方向演變。
Android 5.0 以上,系統(tǒng)殺進(jìn)程以uid為標(biāo)識,通過殺死整個(gè)進(jìn)程組來殺進(jìn)程,因此 native 進(jìn)程也躲不過系統(tǒng)的法眼。
Android 6.0 引入了待機(jī)模式(doze),一旦用戶拔下設(shè)備的電源插頭,并在屏幕關(guān)閉后的一段時(shí)間內(nèi)使其保持不活動狀態(tài),設(shè)備會進(jìn)入低電耗模式,在該模式下設(shè)備會嘗試讓系統(tǒng)保持休眠狀態(tài)。
Android 7.0 加強(qiáng)了之前雞肋的待機(jī)模式(不再要求設(shè)備靜止?fàn)顟B(tài)),同時(shí)對開啟了 Project Svelte,Project Svelte 是專門用來優(yōu)化 Android 系統(tǒng)后臺的項(xiàng)目,在 Android 7.0 上直接移除了一些隱式廣播,App 無法再通過監(jiān)聽這些廣播拉起自己。
Android 8.0 進(jìn)一步加強(qiáng)了應(yīng)用后臺執(zhí)行限制:一旦應(yīng)用進(jìn)入已緩存狀態(tài)時(shí),如果沒有活動的組件,系統(tǒng)將解除應(yīng)用具有的所有喚醒鎖。另外,系統(tǒng)會限制未在前臺運(yùn)行的應(yīng)用的某些行為,比如說應(yīng)用的后臺服務(wù)的訪問受到限制,也無法使用 Mainifest 注冊大部分隱式廣播。
Android 9.0 進(jìn)一步改進(jìn)了省電模式的功能并加入了應(yīng)用待機(jī)分組,長時(shí)間不用的 App 會被打入冷宮;另外,系統(tǒng)監(jiān)測到應(yīng)用消耗過多資源時(shí),系統(tǒng)會通知并詢問用戶是否需要限制該應(yīng)用的后臺活動。
然而,道高一尺,魔高一丈。系統(tǒng)在不斷演進(jìn),?;罘椒ㄒ苍诓粩喟l(fā)展。大約在 4 年前出現(xiàn)過一個(gè)MarsDaemon,這個(gè)庫通過雙進(jìn)程守護(hù)的方式實(shí)現(xiàn)?;睿粫r(shí)間風(fēng)頭無兩。不過好景不長,進(jìn)入 Android 8.0 時(shí)代之后,這個(gè)庫就逐漸消亡。
一般來說,Android 進(jìn)程?;罘譃閮蓚€(gè)方面:
- 保持進(jìn)程不被系統(tǒng)殺死。
- 進(jìn)程被系統(tǒng)殺死之后,可以重新復(fù)活。
隨著 Android 系統(tǒng)變得越來越完善,單單通過自己拉活自己逐漸變得不可能了;因此后面的所謂「保活」基本上是兩條路:1. 提升自己進(jìn)程的優(yōu)先級,讓系統(tǒng)不要輕易弄死自己;2. App 之間互相結(jié)盟,一個(gè)兄弟死了其他兄弟把它拉起來。
當(dāng)然,還有一種終極方法,那就是跟各大系統(tǒng)廠商建立 PY 關(guān)系,把自己加入系統(tǒng)內(nèi)存清理的白名單;比如說國民應(yīng)用微信。當(dāng)然這條路一般人是沒有資格走的。
大約一年以前,大神 gityuan 在其博客上公布了 TIM 使用的一種可以稱之為「終極永生術(shù)」的保活方法;這種方法在當(dāng)前 Android 內(nèi)核的實(shí)現(xiàn)上可以大大提升進(jìn)程的存活率。筆者研究了這種?;钏悸返膶?shí)現(xiàn)原理,并且提供了一個(gè)參考實(shí)現(xiàn)Leoric。接下來就給大家分享一下這個(gè)終極?;詈诳萍嫉膶?shí)現(xiàn)原理。
?;畹牡讓蛹夹g(shù)原理
知己知彼,百戰(zhàn)不殆。既然我們想要?;?,那么首先得知道我們是怎么死的。一般來說,系統(tǒng)殺進(jìn)程有兩種方法,這兩個(gè)方法都通過 ActivityManagerService 提供:
1.killBackgroundProcesses
2.forceStopPackage
在原生系統(tǒng)上,很多時(shí)候殺進(jìn)程是通過第一種方式,除非用戶主動在 App 的設(shè)置界面點(diǎn)擊「強(qiáng)制停止」。不過國內(nèi)各廠商以及一加三星等 ROM 現(xiàn)在一般使用第二種方法。第一種方法太過溫柔,根本治不住想要搞事情的應(yīng)用。第二種方法就比較強(qiáng)力了,一般來說被 force-stop 之后,App 就只能乖乖等死了。
因此,要實(shí)現(xiàn)?;睿覀兙偷弥?force-stop 到底是如何運(yùn)作的。既然如此,我們就跟蹤一下系統(tǒng)的forceStopPackage這個(gè)方法的執(zhí)行流程:
首先是ActivityManagerService里面的forceStopPackage這方法:
public void forceStopPackage(final String packageName, int userId) { // .. 權(quán)限檢查,省略 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) { // 狀態(tài)判斷,省略.. 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)) { // 根據(jù) UID 和包名殺進(jìn)程 forceStopPackageLocked(packageName, pkgUid, "from pid " + callingPid); finishForceStopPackageLocked(packageName, pkgUid); } } } } finally { Binder.restoreCallingIdentity(callingId); } }
在這里我們可以知道,系統(tǒng)是通過uid為單位 force-stop 進(jìn)程的,因此不論你是 native 進(jìn)程還是 Java 進(jìn)程,force-stop 都會將你統(tǒng)統(tǒng)殺死。我們繼續(xù)跟蹤forceStopPackageLocked這個(gè)方法:
final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, int userId, String reason) { int i; // .. 狀態(tà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; }
這個(gè)方法實(shí)現(xiàn)很清晰:先殺死這個(gè) App 內(nèi)部的所有進(jìn)程,然后清理殘留在 system_server 內(nèi)的四大組件信息;我們關(guān)心進(jìn)程是如何被殺死的,因此繼續(xù)跟蹤killPackageProcessesLocked,這個(gè)方法最終會調(diào)用到ProcessList內(nèi)部的removeProcessLocked方法,removeProcessLocked會調(diào)用ProcessRecord的kill方法,我們看看這個(gè)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); } }
這里我們可以看到,首先殺掉了目標(biāo)進(jìn)程,然后會以uid為單位殺掉目標(biāo)進(jìn)程組。如果只殺掉目標(biāo)進(jìn)程,那么我們可以通過雙進(jìn)程守護(hù)的方式實(shí)現(xiàn)保活;關(guān)鍵就在于這個(gè)killProcessGroup,繼續(xù)跟蹤之后發(fā)現(xiàn)這是一個(gè) native 方法,它的最終實(shí)現(xiàn)在libprocessgroup中,代碼如下:
int killProcessGroup(uid_t uid, int initialPid, int signal) { return KillProcessGroup(uid, initialPid, signal, 40 /*retries*/); }
注意這里有個(gè)奇怪的數(shù)字:40。我們繼續(xù)跟蹤:
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; } } // 省略 }
瞧瞧我們的系統(tǒng)做了什么騷操作?循環(huán) 40 遍不停滴殺進(jìn)程,每次殺完之后等 5ms,循環(huán)完畢之后就算過去了。
看到這段代碼,我想任何人都會蹦出一個(gè)疑問:假設(shè)經(jīng)歷連續(xù) 40 次的殺進(jìn)程之后,如果 App 還有進(jìn)程存在,那不就僥幸逃脫了嗎?
實(shí)現(xiàn)方法
那么,如何實(shí)現(xiàn)這個(gè)目的呢?我們看這個(gè)關(guān)鍵的5ms。假設(shè),App 進(jìn)程在被殺掉之后,能夠以足夠快的速度(5ms 內(nèi))啟動一堆新的進(jìn)程,那么系統(tǒng)在一次循環(huán)殺掉老的所有進(jìn)程之后,sleep 5ms 之后又會遇到一堆新的進(jìn)程;如此循環(huán) 40 次,只要我們每次都能夠拉起新的進(jìn)程,那我們的 App 就能逃過系統(tǒng)的追殺,實(shí)現(xiàn)永生。是的,煉獄般的 200ms,只要我們熬過 200ms 就能渡劫成功,得道飛升。不知道大家有沒有玩過打地鼠這個(gè)游戲,整個(gè)過程非常類似,按下去一個(gè)又冒出一個(gè),只要每次都能足夠快地冒出來,我們就贏了。
現(xiàn)在問題的關(guān)鍵就在于:如何在 5ms 內(nèi)啟動一堆新的進(jìn)程?
再回過頭來看原來的?;罘绞?,它們拉起進(jìn)程最開始通過am命令,這個(gè)命令實(shí)際上是一個(gè) java 程序,它會經(jīng)歷啟動一個(gè)進(jìn)程然后啟動一個(gè) ART 虛擬機(jī),接著獲取 ams 的 binder 代理,然后與 ams 進(jìn)行 binder 同步通信。這個(gè)過程實(shí)在是太慢了,在這與死神賽跑的 5ms 里,它的速度的確是不敢恭維。
后來,MarsDaemon 提出了一種新的方式,它用 binder 引用直接給 ams 發(fā)送 Parcel,這個(gè)過程相比am
命令快了很多,從而大大提高了成功率。其實(shí)這里還有改進(jìn)的空間,畢竟這里還是在 Java 層調(diào)用,Java 語言在這種實(shí)時(shí)性要求極高的場合有一個(gè)非常令人詬病的特性:垃圾回收(GC);雖然我們在這 5ms 內(nèi)直接碰上 gc 引發(fā)停頓的可能性非常小,但是由于 GC 的存在,ART 中的 Java 代碼存在非常多的 checkpoint;想象一下你現(xiàn)在是一個(gè)信使有重要軍情要報(bào)告,但是在路上卻碰到很多關(guān)隘,而且很可能被勒令暫時(shí)停止一下,這種情況是不可接受的。因此,最好的方法是通過 native code 給 ams 發(fā)送 binder 調(diào)用;當(dāng)然,如果再底層一點(diǎn),我們甚至可以通過ioctl直接給 binder 驅(qū)動發(fā)送數(shù)據(jù)進(jìn)而完成調(diào)用,但是這種方法的兼容性比較差,沒有用 native 方式省心。
通過在 native 層給 ams 發(fā)送 binder 消息拉起進(jìn)程,我們算是解決了「快速拉起進(jìn)程」這個(gè)問題。但是這個(gè)還是不夠。還是回到打地鼠這個(gè)游戲,假設(shè)你摁下一個(gè)地鼠,會冒起一個(gè)新的地鼠,那么你每次都能摁下去最后獲取勝利的概率還是比較高的;但如果你每次摁下一個(gè)地鼠,其他所有地鼠都能冒出來呢?這個(gè)難度系數(shù)可是要高多了。如果我們的進(jìn)程能夠在任意一個(gè)進(jìn)程死亡之后,都能讓把其他所有進(jìn)程全部拉起,這樣系統(tǒng)就很難殺死我們了。
新的黑科技保活中通過 2 個(gè)機(jī)制來保證進(jìn)程之間的互相拉起:
1.2 個(gè)進(jìn)程通過互相監(jiān)聽文件鎖的方式,來感知彼此的死亡。
2.通過 fork 產(chǎn)生子進(jìn)程,fork 的進(jìn)程同屬一個(gè)進(jìn)程組,一個(gè)被殺之后會觸發(fā)另外一個(gè)進(jìn)程被殺,從而被文件鎖感知。
具體來說,創(chuàng)建 2 個(gè)進(jìn)程 p1, p2,這兩個(gè)進(jìn)程通過文件鎖互相關(guān)聯(lián),一個(gè)被殺之后拉起另外一個(gè);同時(shí) p1 經(jīng)過 2 次 fork 產(chǎn)生孤兒進(jìn)程 c1,p2 經(jīng)過 2 次 fork 產(chǎn)生孤兒進(jìn)程 c2,c1 和 c2 之間建立文件鎖關(guān)聯(lián)。這樣假設(shè) p1 被殺,那么 p2 會立馬感知到,然后 p1 和 c1 同屬一個(gè)進(jìn)程組,p1 被殺會觸發(fā) c1 被殺,c1 死后 c2 立馬感受到從而拉起 p1,因此這四個(gè)進(jìn)程三三之間形成了鐵三角,從而保證了存活率。
分析到這里,這種方案的大致原理我們已經(jīng)清晰了?;谝陨显恚覍懥艘粋€(gè)簡單的 PoC,代碼在這里:https://github.com/tiann/Leoric有興趣的可以看一下。
改進(jìn)空間
本方案的原理還是比較簡單直觀的,但是要實(shí)現(xiàn)穩(wěn)定的?;?,還需要很多細(xì)節(jié)要補(bǔ)充;特別是那與死神賽跑的 5ms,需要不計(jì)一切代價(jià)去優(yōu)化才能提升成功率。具體來說,就是當(dāng)前的實(shí)現(xiàn)是在 Java 層用 binder 調(diào)用的,我們應(yīng)該在 native 層完成。筆者曾經(jīng)實(shí)現(xiàn)過這個(gè)方案,但是這個(gè)庫本質(zhì)上是有損用戶利益的,因此并不打算公開代碼,這里簡單提一下實(shí)現(xiàn)思路供大家學(xué)習(xí):
如何在 native 層進(jìn)行 binder 通信
libbinder 是 NDK 公開庫,拿到對應(yīng)頭文件,動態(tài)鏈接即可。
難點(diǎn):依賴繁多,剝離頭文件是個(gè)體力活。
如何組織 binder 通信的數(shù)據(jù)?
通信的數(shù)據(jù)其實(shí)就是二進(jìn)制流;具體表現(xiàn)就是 (C++/Java) Parcel 對象。native 層沒有對應(yīng)的 Intent Parcel,兼容性差。
方案:
1.Java 層創(chuàng)建 Parcel (含 Intent),拿到 Parcel 對象的 mNativePtr(native peer),傳到 Native 層。
2.native 層直接把 mNativePtr 強(qiáng)轉(zhuǎn)為結(jié)構(gòu)體指針。
3.fork 子進(jìn)程,建立管道,準(zhǔn)備傳輸 parcel 數(shù)據(jù)。
4.子進(jìn)程讀管道,拿到二進(jìn)制流,重組為 parcel。
如何應(yīng)對
今天我把這個(gè)實(shí)現(xiàn)原理公開,并且提供 PoC 代碼,并不是鼓勵大家使用這種方式保活,而是希望各大系統(tǒng)廠商能感知到這種黑科技的存在,推動自己的系統(tǒng)徹底解決這個(gè)問題。
兩年前我就知道了這個(gè)方案的存在,不過當(dāng)時(shí)鮮為人知。最近一個(gè)月我發(fā)現(xiàn)很多 App 都使用了這種方案,把我的 Android 手機(jī)折騰的慘不忍睹;畢竟本人手機(jī)上安裝了將近 800 個(gè) App,假設(shè)每個(gè) App 都用這個(gè)方案?;?,那這系統(tǒng)就沒法用了。
系統(tǒng)如何應(yīng)對
如果我們把系統(tǒng)殺進(jìn)程比喻為斬首,那么這個(gè)?;罘桨傅木柙谟谀芸焖匍L出一個(gè)新的頭;因此應(yīng)對之法也很簡單,只要我們在斬殺一個(gè)進(jìn)程的時(shí)候,讓別的進(jìn)程老老實(shí)實(shí)呆著別搞事情就 OK 了。具體的實(shí)現(xiàn)方法多種多樣,不贅述。
用戶如何應(yīng)對
在廠商沒有推出解決方案之前,用戶可以有一些方案來緩解使用這個(gè)方案進(jìn)行?;畹牧髅?App。這里推薦兩個(gè)應(yīng)用給大家:
- 冰箱
- Island
通過冰箱的凍結(jié)和 Island 的深度休眠可以徹底阻止 App 的這種?;钚袨椤.?dāng)然,如果你喜歡別的這種“凍結(jié)”類型的應(yīng)用,比如小黑屋或者太極的陰陽之門也是可以的。
其他不是通過“凍結(jié)”這種機(jī)制來壓制后臺的應(yīng)用理論上對這種?;罘桨傅淖饔梅浅S邢蕖?/p>
總結(jié)
1.對技術(shù)來說,黑科技沒有什么黑的,不過是對系統(tǒng)底層原理的深入了解從而反過來對抗系統(tǒng)的一種手段。很多人會說,了解系統(tǒng)底層有什么用,本文應(yīng)該可以給出一個(gè)答案:可以實(shí)現(xiàn)別人永遠(yuǎn)也無法實(shí)現(xiàn)的功能,通過技術(shù)推動產(chǎn)品,從而產(chǎn)生巨大的商業(yè)價(jià)值。
2.黑科技雖強(qiáng),但是它不該存在于這世上。沒有規(guī)矩,不成方圓。黑科技黑的了一時(shí),黑不了一世。要提升產(chǎn)品的存活率,終歸要落到產(chǎn)品本身上面來,尊重用戶,提升體驗(yàn)方是正途。
以上就是詳解App?;顚?shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于App?;顚?shí)現(xiàn)原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于如何使用Flutter開發(fā)執(zhí)行操作系統(tǒng)shell命令的工具詳解
本文主要介紹如何在Flutter應(yīng)用中開發(fā)一個(gè)Android終端命令行工具,包括終端命令行頁面的布局設(shè)計(jì)、與Shell通信的基本原理、輸入輸出處理的基本技巧等,以及如何在具體應(yīng)用中利用終端命令行工具來執(zhí)行系統(tǒng)命令和與用戶進(jìn)行交互2023-06-06Android仿微信清理內(nèi)存圖表動畫(解決surfaceView屏幕閃爍問題)demo實(shí)例詳解
本文通過實(shí)例代碼給大家講解android仿微信清理內(nèi)存圖表動畫(解決surfaceView屏幕閃爍問題)的相關(guān)資料,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09解決Android Studio4.1沒有Gsonfomat插件,Plugin “GsonFormat” is inco
這篇文章主要介紹了解決Android Studio4.1沒有Gsonfomat插件,Plugin “GsonFormat” is incompatible (supported only in IntelliJ IDEA)的問題 ,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-12-12Android列表實(shí)現(xiàn)(1)_數(shù)組列表實(shí)例介紹
最近開始學(xué)習(xí)android的ui,先上幾個(gè)相關(guān)的例子,后續(xù)還會有更新,感興趣的朋友可以研究下2012-12-12Android進(jìn)階CameraX與Camera2使用比對詳解
這篇文章主要為大家介紹了Android進(jìn)階CameraX與Camera2使用比示例對詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01解析:繼承ViewGroup后的子類如何重寫onMeasure方法
本篇文章是對繼承ViewGroup后的子類如何重寫onMeasure方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06Android 圖像處理(類型轉(zhuǎn)換,比例縮放,倒影,圓角)的小例子
Android 圖像處理(類型轉(zhuǎn)換,比例縮放,倒影,圓角)的小例子,需要的朋友可以參考一下2013-05-05