轉自《 安卓尖端技術研究》
上一篇文章從Native角度講解了Android程序管理的相關概念,本文將繼續從上層的Framework中的程序啟動、銷燬場景和優先順序處理、以及它們與四大元件的種種關聯,來逐步解析Android程序管理的其他關鍵要素。
程序的啟動Android Framework層是執行在ART虛擬機器之上的,對於一般的Java程序是不具備主動建立程序的能力的。我們都知道Android中存在四大元件:Activity、Service、BroadcastReceiver和ContentProvider,那麼為什麼這樣劃分呢,一個主要的原因是在四大元件的啟動階段,如果宿主程序沒有正在執行,那麼系統服務程序system_server將會先拉起程序,然後才會繼續啟動元件。簡而言之,正因為AMS將安卓中程序的建立、銷燬及優先順序進行了封裝,應用側開發者才能夠在對程序無感知的情況的同時使用四大元件。
啟動四大元件方法 startActivity, sendBroadcast, startService/bindService 和 getContentProviderImpl 之所以能夠啟動程序,是因為它們都會通過傳送 binder 訊息到 AMS 進行處理,當 AMS 發現對應元件沒有在 xml 宣告的程序啟動時,會先拉起該程序,然後再啟動對應元件。
也就是說,四大元件正是APP程序的入口。它們拉起程序的方式都是一致的,基本流程:
Zygote程序啟動,初始化虛擬機器及基本環境,同時進入一個無限迴圈,提供建立 java 程序服務AMS 通過呼叫 Process.start() 方法,傳送 socket 資訊到 Zygote 程序Zygote 程序處理訊息,並通過fork建立應用程序應用程序反射呼叫 ActivityThread.main 方法,進入自身邏輯 (初始化 Looper/Binder 等主要服務 )AMS 建立程序成功後,會將程序以 ProcessRecord 的形式進行封裝、管理。彩蛋:實用的程序相關adb命令
Java程序的建立與銷燬實時
adb logcat -b events | grep proc**07-16 14:19:23.357 4774 4800 I am_proc_start: [0,30314,10063,com.koushikdutta.vysor,activity,com.koushikdutta.vysor/.StartActivity]07-16 14:19:23.375 4774 6027 I am_proc_bound: [0,30314,com.koushikdutta.vysor]07-16 14:19:31.621 4774 5281 I am_proc_died: [0,30314,com.koushikdutta.vysor,900,17]
獲取應用程序中元件詳細資訊
adb shell dumpsys activity p <packageName>*APP* UID 10115 ProcessRecord{4114d9d 18987:com.ss.android.ugc.aweme/u0a115} user #0 uid=10115 gids={50115, 20115, 9997, 3002, 3003, 3001} requiredAbi=armeabi-v7a instructionSet=arm class=com.ss.android.ugc.aweme.app.host.HostApplication ... // adj, procState優先順序資訊 oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0 curSchedGroup=2 setSchedGroup=2 systemNoUi=false trimMemoryLevel=0 curProcState=2 repProcState=2 pssProcState=5 setProcState=2 ... **Activities**: - ActivityRecord{5e38d19 u0 com.ss.android.ugc.aweme/.main.MainActivity t948} **Services**: - ServiceRecord{13f23bc u0 com.ss.android.ugc.aweme/com.tt.miniapphost.process.base.HostCrossProcessCallService} ... **Connections**: - ConnectionRecord{77ae579 u0 CR com.ss.android.ugc.aweme/com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService:@30b4240} ... **Published Providers**: - com.umeng.message.provider.MessageProvider -> ContentProviderRecord{76d000a u0 com.ss.android.ugc.aweme/com.umeng.message.provider.MessageProvider} ... **Connected Providers**: - c887614/com.android.providers.settings/.SettingsProvider->18987:com.ss.android.ugc.aweme/u0a115 s1/1 u0/0 +19h23m3s147ms - c4d7fba/com.android.providers.media/.MediaProvider->18987:com.ss.android.ugc.aweme/u0a115 s0/1 u1/2 +1m10s935ms **Receivers**: - ReceiverList{4afbc5 18987 com.ss.android.ugc.aweme/10115/u0 remote:687d23c} ...
獲取應用優先順序資訊
adb shell dumpsys activity oProc 0: fore T/A/TOP trm: 0 18987:com.ss.android.ugc.aweme/u0a115 (top-activity) oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0 state: cur=TOP set=TOP lastPss=268MB lastSwapPss=0.00 lastCachedPss=0.00 cached=false empty=false hasAboveClient=false
程序的優先順序
Android Framework中程序優先順序的衡量單位有兩種,除了 adj (對應 lmk 的 oom_score_adj)之外,新添了 procState 變數。這兩個優先順序相輔相成,adj 更多的用在給 lmk 判斷該程序是否應該被殺死,而procState 更多的給 Framework 層的服務給程序狀態"定級",例如,AMS可以簡單粗暴的通過判斷 procState 是否小於等於 PROCESS_STATE_IMPORTANT_FOREGROUND 來判斷該程序是否為"前臺程序":
[ActivityManagerService.java]
@Overridepublic boolean isAppForeground(int uid) { synchronized (this) { UidRecord uidRec = mActiveUids.get(uid); if (uidRec == null || uidRec.idle) { return false; } return uidRec.curProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; }}
這裡的 UidRecord 是 Android 常用的許可權管理方案,如果沒有做修改的話,普通應用建立的程序都是在同一個 uid 裡的。在系統側,Android 常常通過 uid 來劃分對應的程序是否具有相應的優先順序。
關於Android中adj與procState具體值的含義,可以檢視文章最後的附錄表格,具體場景僅供參考。如果需要細緻判定當前程序優先順序的狀態,通過原始碼分析最為直觀。
adj、schedGroup、procState和adjType的核心計算步驟是在AMS中的computeOomAdjLocked方法完成的,如圖:
這個方法的核心執行無外乎以下幾個功能:
首先檢查該程序是否處於高優場景中,如前臺Activity、正在執行RemoteAnimation,正在處理廣播和Service等該程序是否是某種特殊程序,如Home,height weight和backup是否存在程序因為service和content provider互相繫結提升優先順序的情況如果都不是上述情況,處於的優先順序都比較低,最低的是cache程序,AMS對系統整體cache程序個數有閾值限制,超過上限就會觸發清理操作彩蛋:使用framework中的"自相矛盾"進行保活
程序保活無外乎也是一種普通應用對作業系統的攻擊手段,對於一般的攻擊手段,我把它理解成使用開發者約定俗成的規則漏洞來突破普通使用者原有許可權的行為。
比如說 buffer overflow,是利用了舊版本C編譯器在切換函式堆疊時,沒有有效檢查堆疊資訊作用範圍的漏洞。使得普通應用使用者可以利用超長的棧內資訊覆蓋調原有的堆疊跳轉資訊,使得應用執行其它意想不到的程式。通過這種方式可以獲取到root許可權;又比如說 TCP 劫持,是利用了傳送方與接收方沒有很好的 SEQ和ACK 序列的校驗手段,使得中間者可以通過控制 SEQ和ACK的數值來控制正常通訊方的流程。
方式一:bindService 互相繫結以提升優先順序這種優先順序方式分為兩種
第一種,依賴高優先順序 client:
if (mayBeTop && procState > ActivityManager.PROCESS_STATE_TOP) { switch (procState) { case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE: case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE: // Something else is keeping it at this level, just leave it. break; case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND: case ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND: case ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND: case ActivityManager.PROCESS_STATE_SERVICE: procState = ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; app.adjType = mayBeTopType; app.adjSource = mayBeTopSource; app.adjTarget = mayBeTopTarget; ... }}
maybeTop 為 true 需要 ContentProvider 或者 Service 的 client 正在前臺顯示 Activity
同理,通過讓使用者選擇該client來提供服務,例如WallPaperService,可以讓system_server成為client,以此來提高程序優先順序。
第二種,兩個程序互相繫結:
if ((cr.flags&Context.BIND_FOREGROUND_SERVICE) != 0) { clientProcState = ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;} else if (mWakefulness == PowerManagerInternal.WAKEFULNESS_AWAKE && (cr.flags&Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE) != 0) { clientProcState = ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;} else { clientProcState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;}...if (procState > clientProcState) { procState = clientProcState; if (adjType == null) { adjType = "service"; }}
通過設定 flag,兩個普通的程序可以通過互相"攀升"來提升優先順序
方式二、監聽鎖屏廣播拉起Activity,監聽解鎖廣播銷燬Activity:具體步驟:
啟動一個Service,通過動態廣播receiver接收鎖屏、開屏廣播滅屏時,啟動Activity到前臺開屏時,將Activity進行 finish,確保使用者無感知adb shell ps | grep <pacakgeName>ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes) All known processes: *APP* UID 10064 ProcessRecord{f59eb2f 30917:com.bytedance.gcsuppression.demo/u0a64} ... oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100 curSchedGroup=1 setSchedGroup=1 systemNoUi=false trimMemoryLevel=0 curProcState=5 repProcState=5 pssProcState=5 setProcState=5 lastStateTime=-23s716ms
(該方法在 Android 8.0 上測試過仍有效,更高版本尚未測試;因為是利用了 service 與動態廣播的特性,所以 framework 很難對這個邏輯做出應對,除非是在系統側維護一個 service 黑名單)
雖然殺程序有這麼多的花樣,但常見的方式就三種:
清理名稱觸發場景功能kill粒度最細,用於程序自殺、AMS 查殺單個快取程序等殺死單個 java 程序killBackground普通應用也可以通過 AM 服務呼叫殺死系統中 background 程序force-stop以 package 為單位,強力殺死和該包有關聯的程序並禁止大部分自啟殺死該包下所有程序,關閉並銷燬相關元件
普通的查殺方式如kill和killBackground都只是殺死單個或部分程序,當程序死亡後,會因為 binder fd 的釋放通過死亡回撥通知 AMS 程序已死,這期間 AMS 也有可能因為 Service 等元件重新將程序拉起。考慮到拉活方案花樣多,應用中的其它 java 程序或 c 程序仍然可以通過啟動元件的方式重新將程序拉起來。
在一般的原生機器上,force-stop雖然很少使用,一般只在設定裡的"強制停止"按鈕觸發。除了殺死程序之外也會銷燬元件等資訊,同時也會更改 PMS 中對應包名的狀態,避免再次拉活。其主要功能如下:
systemui 程序中的 com.android.systemui/.recents.RecentsActivity 接收到卡片滑動事件systemui 獲取到 AMS 服務,再呼叫 am.removeTask 方法來移除 Task 和殺死顯示該介面的程序;注意這個方法普通應用是沒有呼叫許可權的。最後通過 AMS 的 cleanUpRemovedTaskLocked 方法來殺死程序,這個方法的具體實現如下:彩蛋:當我們上滑應用卡片時Android在做什麼
void cleanUpRemovedTaskLocked(TaskRecord tr, boolean killProcess, boolean removeFromRecents) { // 1. 在最近任務中移除 Task if (removeFromRecents) { mRecentTasks.remove(tr); } ... // 2. 選擇需要查殺的程序 final String pkg = component.getPackageName(); ArrayList<ProcessRecord> procsToKill = new ArrayList<>(); ArrayMap<String, SparseArray<ProcessRecord>> pmap = mService.mProcessNames.getMap(); for (int i = 0; i < pmap.size(); i++) { SparseArray<ProcessRecord> uids = pmap.valueAt(i); for (int j = 0; j < uids.size(); j++) { ProcessRecord proc = uids.valueAt(j); ... // 2.1 如果 Task 所在程序還有別的 Task 在最近任務中顯示,那麼該程序不會被殺 for (int k = 0; k < proc.activities.size(); k++) { TaskRecord otherTask = proc.activities.get(k).getTask(); if (tr.taskId != otherTask.taskId && otherTask.inRecents) { return; } } // 2.2 程序有前臺service的不會被殺,典型的有音樂播放軟體 if (proc.foregroundServices) { return; } // Add process to kill list. procsToKill.add(proc); } } // 3. 正式查殺 for (int i = 0; i < procsToKill.size(); i++) { ProcessRecord pr = procsToKill.get(i); // 如果滿足這兩個條件才會立即被殺 if (pr.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND && pr.curReceivers.isEmpty()) { pr.kill("remove task", true); } else { pr.waitingToKill = "remove task"; } }}
彩蛋:反保活的終結者
Android 中的程序查殺最強武器是force-stop,本質上它會通過遍歷並殺死所有和應用有關的Java程序:
private final boolean killPackageProcessesLocked(String packageName, int appId, int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart, boolean doit, boolean evenPersistent, String reason) { ArrayList<ProcessRecord> procs = new ArrayList<>(); final int NP = mProcessNames.getMap().size(); for (int ip=0; ip<NP; ip++) { SparseArray<ProcessRecord> apps = mProcessNames.getMap().valueAt(ip); final int NA = apps.size(); for (int ia=0; ia<NA; ia++) { ProcessRecord app = apps.valueAt(ia); ... // continue if the process do not need to kill app.removed = true; procs.add(app); } } int N = procs.size(); // force-stop 的本質是通過遍歷所有與應用有關的程序,依次查殺來實現殺死所有程序的 for (int i=0; i<N; i++) { removeProcessLocked(procs.get(i), callerWillRestart, allowRestart, reason); } updateOomAdjLocked(); return N > 0;}
想要做反保活的話,我們只需要在 removeProcessLocked 的遍歷之前,將 java 程序和在其同一cgroup的c程序一起通過傳送訊號hang住,再通過迴圈依次殺死所有程序,這種方法基本能杜絕所有拉活方案。
總結程序管理與大家日常開發息息相關,也是在Android系統中是個舉足輕重的模組。相信通過本系列的兩篇文章,大家已經對程序啟動期間涉及到fork、對程序的優先順序管理涉及到adj、對程序的排程這些概念已經有了更加深入的理解了。