背景
  我们在使用手机时,难免需要对某些应用做后台保活,比如社交软件、音乐播放器、VPN 代理、广告跳过工具等等,但许多国内 ROM 厂商却将 Android 的「最近任务」做成了「任务列表」,在其中侧滑或点击一键清理按钮将导致应用被杀死,使用起来很不方便。
  就我这台 MIUI 而言,虽然诸如 QQ、微信 等受众较广的应用官方已经在内置的规则列表中默认为其放行,但到了 Temegram、Clash 一类的小众应用,不小心划掉之后就会比较痛苦。好在我们已经 root 了,那自然不能任由它这样下去,遂决定开发一款专为 MIUI 打造的进程锁,为我们想要的进程提供保护,使其不被清理掉
实现
逆向分析
- 以下测试均基于 MIUI V12.5.4.0.RJVCNXM
  我们知道,在较新版本的 MIUI 系统中,「最近任务」属于「系统桌面」这个应用,那么首先把它提取出来,通过方法名和包名的简单搜索,很容易就可以定位到下面这个方法:
| 1
 | com.android.systemui.shared.recents.system.ProcessManagerWrapper#doOneKeyClean(ArrayList<Task>, ArrayList<String>)
 | 
  写个小 Hook 验证一下:
| 12
 3
 4
 5
 
 | findMethod("com.android.systemui.shared.recents.system.ProcessManagerWrapper") {name == "doOneKeyClean"
 }!! before {
 Log.i("called doOneKeyClean!")
 }
 
 | 
  发现确实调用了它,接下来看看它是怎么写的:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | public static void doOneKeyClean(ArrayList<Task> arrayList, ArrayList<String> arrayList2) {try {
 ArrayList arrayList3 = new ArrayList();
 Iterator<Task> it = arrayList.iterator();
 while (it.hasNext()) {
 arrayList3.add(Integer.valueOf(it.next().key.id));
 }
 ProcessConfig processConfig = new ProcessConfig(1);
 processConfig.setRemoveTaskNeeded(true);
 processConfig.setRemovingTaskIdList(arrayList3);
 processConfig.setWhiteList(arrayList2);
 ProcessManager.kill(processConfig);
 } catch (Exception e) {
 Log.e(TAG, "doOneKeyClean", e);
 }
 }
 
 | 
  注意到 12 行这里调用了一个 ProcessManager.kill() 方法,正准备跟进去看一下,然后发现……并没有这个类?那它一定就在 framework.jar!到 /system/framework/ 下把这个 jar 扒出来丢进 jadx,发现内部继续调用了 ProcessManagerNative.getDefault().kill():
| 12
 3
 4
 5
 6
 7
 8
 
 | public static boolean kill(ProcessConfig config) {try {
 return ProcessManagerNative.getDefault().kill(config);
 } catch (RemoteException e) {
 e.printStackTrace();
 return false;
 }
 }
 
 | 
  看看这个 getDefault() 是如何工作的:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | public static IProcessManager getDefault() {if (pm == null) {
 synchronized (ProcessManagerNative.class) {
 if (pm == null) {
 pm = asInterface(ServiceManager.getService("ProcessManager"));
 }
 }
 }
 return pm;
 }
 
 | 
  到这里已经很明显了,下一站:services.jar!
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 
 | public boolean kill(ProcessConfig config) throws RemoteException {if (checkPermission()) {
 int callingPid = Binder.getCallingPid();
 int policy = config.getPolicy();
 Slog.i("ProcessManager", "Kill reason " + getKillReason(config) + " from pid=" + callingPid);
 addConfigToHistory(config);
 this.mProcessPolicy.resetWhiteList(this.mContext, UserHandle.getCallingUserId());
 boolean success = false;
 switch (policy) {
 case 1:
 case 2:
 case 4:
 case 5:
 case 14:
 case 15:
 case 16:
 success = killAll(config);
 break;
 case 3:
 case 6:
 case 10:
 success = killAny(config);
 break;
 case 7:
 success = swipeToKillApp(config);
 break;
 case 8:
 case 9:
 default:
 Slog.w("ProcessManager", "unKnown policy");
 break;
 case 11:
 case 12:
 case 13:
 success = autoKillApp(config);
 break;
 case 17:
 success = killByPriority(config);
 break;
 }
 ProcessRecordInjector.reportAppPss();
 return success;
 }
 String msg = "Permission Denial: ProcessManager.kill() from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid();
 Slog.w("ProcessManager", msg);
 throw new SecurityException(msg);
 }
 
 | 
  定位到 com.android.server.am.ProcessManagerService,其中 kill 方法很据我们传入的 config 得到一个 policy,然后再根据 policy 决定要使用哪种 kill 手段。于是开始回溯,看看这个 ProcessConfig 从何而来:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | public static void doOneKeyClean(ArrayList<Task> arrayList, ArrayList<String> arrayList2) {try {
 ArrayList arrayList3 = new ArrayList();
 Iterator<Task> it = arrayList.iterator();
 while (it.hasNext()) {
 arrayList3.add(Integer.valueOf(it.next().key.id));
 }
 ProcessConfig processConfig = new ProcessConfig(1);
 processConfig.setRemoveTaskNeeded(true);
 processConfig.setRemovingTaskIdList(arrayList3);
 processConfig.setWhiteList(arrayList2);
 ProcessManager.kill(processConfig);
 } catch (Exception e) {
 Log.e(TAG, "doOneKeyClean", e);
 }
 }
 
 | 
  注意这个 new ProcessConfig(1); 中的 1 就是 policy,相应地我们可以发现下面 doSwapUPClean(最近任务侧滑)对应的 policy 是 7:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | public static void doSwapUPClean(Task task) {try {
 String packageName = task.key.getComponent().getPackageName();
 int i = task.key.userId;
 ProcessConfig processConfig = new ProcessConfig(7, packageName, i, task.key.id);
 processConfig.setRemoveTaskNeeded(true);
 ProcessManager.kill(processConfig);
 if (isHasRelatedPkg(packageName)) {
 Iterator<String> it = getRelatedPkg(packageName).iterator();
 while (it.hasNext()) {
 ProcessManager.kill(new ProcessConfig(7, it.next(), i, task.key.id));
 }
 }
 } catch (Exception e) {
 Log.e(TAG, "doSwapUPClean", e);
 }
 }
 
 | 
  继续在 service.jar 中跟踪这两种 policy 的后续,发现它们最终都调用了 killOnceByPolicy() 方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 
 | protected boolean killAll(ProcessConfig config) {int policy = config.getPolicy();
 String reason = getKillReason(config);
 Map<Integer, String> fgTaskPackageMap = null;
 List<String> whiteList = null;
 if (policy != 2) {
 whiteList = config.getWhiteList();
 if (policy == 1 || policy == 15 || policy == 4 || policy == 14 || policy == 16) {
 if (whiteList == null) {
 whiteList = new ArrayList();
 }
 if (policy == 15 || policy == 14 || policy == 16) {
 whiteList.add("com.android.deskclock");
 }
 fgTaskPackageMap = WindowProcessUtils.getPerceptibleRecentAppList(this.mActivityManagerService.mActivityTaskManager);
 }
 }
 if (config.isRemoveTaskNeeded() && config.getRemovingTaskIdList() != null) {
 removeTasksIfNeeded(config.getRemovingTaskIdList(), fgTaskPackageMap != null ? fgTaskPackageMap.keySet() : null, whiteList);
 } else if (policy == 2) {
 removeAllTasks(UserHandle.getCallingUserId(), policy);
 }
 if (whiteList != null) {
 if (fgTaskPackageMap != null) {
 whiteList.addAll(fgTaskPackageMap.values());
 }
 this.mProcessPolicy.addWhiteList(8, whiteList, false);
 }
 killAll(policy, reason);
 return true;
 }
 
 private void killAll(int policy, String reason) {
 ArrayList<ProcessRecord> processList;
 if (this.mLruProcesses != null) {
 synchronized (this.mActivityManagerService) {
 try {
 ActivityManagerService.boostPriorityForLockedSection();
 processList = (ArrayList) this.mLruProcesses.clone();
 } catch (Throwable th) {
 ActivityManagerService.resetPriorityAfterLockedSection();
 throw th;
 }
 }
 ActivityManagerService.resetPriorityAfterLockedSection();
 if (!(processList == null || processList.isEmpty())) {
 Slog.i("ProcessManager", "mLruProcesses size=" + this.mLruProcesses.size());
 filterCurrentProcess(processList, policy);
 for (int i = processList.size() + (-1); i >= 0; i--) {
 killOnceByPolicy(processList.get(i), reason, policy);
 }
 }
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 
 | protected boolean swipeToKillApp(ProcessConfig config) throws RemoteException {boolean processHasOtherTask;
 if (config.isUserIdInvalid() || config.isTaskIdInvalid()) {
 String msg = "userId:" + config.getUserId() + " or taskId:" + config.getTaskId() + " is invalid";
 Slog.w("ProcessManager", msg);
 throw new IllegalArgumentException(msg);
 }
 String packageName = config.getKillingPackage();
 if (TextUtils.isEmpty(packageName)) {
 return false;
 }
 int taskId = config.getTaskId();
 List<ProcessRecord> appList = getProcessRecordList(packageName, config.getUserId());
 boolean appHasOtherTask = isAppHasActivityInOtherTask(appList, taskId);
 ProcessRecord taskTopApp = null;
 if (appHasOtherTask) {
 taskTopApp = ProcessUtils.getTaskTopApp(taskId, this.mActivityManagerService);
 }
 if (config.isRemoveTaskNeeded()) {
 if (appList.isEmpty()) {
 removeTaskIfNeeded(taskId, packageName, -1);
 } else {
 removeTaskIfNeeded(taskId, packageName, appList.get(0).info.uid);
 }
 }
 String killReason = getKillReason(config);
 if (!appHasOtherTask) {
 for (ProcessRecord app : appList) {
 if (!Build.IS_INTERNATIONAL_BUILD || !isAppHasForegroundServices(app)) {
 killOnceByPolicy(app, killReason, config.getPolicy());
 }
 }
 return true;
 } else if (taskTopApp == null) {
 return true;
 } else {
 synchronized (this.mActivityManagerService) {
 try {
 ActivityManagerService.boostPriorityForLockedSection();
 processHasOtherTask = WindowProcessUtils.isProcessHasActivityInOtherTaskLocked(taskTopApp.getWindowProcessController(), taskId);
 } catch (Throwable th) {
 ActivityManagerService.resetPriorityAfterLockedSection();
 throw th;
 }
 }
 ActivityManagerService.resetPriorityAfterLockedSection();
 if (processHasOtherTask) {
 return true;
 }
 if (Build.IS_INTERNATIONAL_BUILD && isAppHasForegroundServices(taskTopApp)) {
 return true;
 }
 killOnceByPolicy(taskTopApp, killReason, config.getPolicy(), false);
 return true;
 }
 }
 
 | 
  killOnceByPolicy() 有两个重载,它们最终都会调用到 killOnce() 方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | private void killOnceByPolicy(ProcessRecord app, String reason, int policy) {killOnceByPolicy(app, reason, policy, true);
 }
 
 private void killOnceByPolicy(ProcessRecord app, String reason, int policy, boolean canForceStop) {
 if (app != null && app.thread != null && !app.killed && !skipCurrentProcessInBackup(app, app.info.packageName, UserHandle.getCallingUserId())) {
 int killLevel = 100;
 if (app.isPersistent() || app.setAdj < 0 || isInWhiteList(app, app.userId, policy)) {
 ProcessRecordInjector.addAppPssIfNeeded(this, app);
 if (!app.hasOverlayUi() && !app.hasTopUi() && isTrimMemoryEnable(app.info.packageName)) {
 killLevel = 101;
 }
 } else {
 killLevel = (!isForceStopEnable(app, policy) || !canForceStop) ? policy == 3 ? 102 : 103 : HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION;
 }
 killOnce(app, reason, killLevel);
 }
 }
 
 | 
  继续深入分析,这里 killOnce() 方法引入了一个参数 killLevel,下方还有一个 killLevelToString(),在这里我们可以了解到各个 killLevel 代表的含义:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | private String killLevelToString(int level) {switch (level) {
 case 100:
 return "none";
 case 101:
 return "trim-memory";
 case 102:
 return "kill-background";
 case 103:
 return "kill";
 case HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION :
 return "force-stop";
 default:
 return "";
 }
 }
 
 | 
  其中有一个 level 是 ProcessConfig.KILL_LEVEL_TRIM_MEMORY(101),它不会杀死应用进程,只会对其进行一些内存清理,而这正是我们所需要的功能
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | private void killOnce(ProcessRecord app, String reason, int killLevel) {killOnce(app, reason, killLevel, false);
 }
 
 private void killOnce(final ProcessRecord app, String reason, int killLevel, boolean evenForeground) {
 if (app != null && app.thread != null && !app.killed) {
 if (app.processName.equals("com.miui.fmservice:remote")) {
 final Intent intent = new Intent("miui.intent.action.TURN_OFF");
 intent.addFlags(AudioFormat.EVRC);
 this.mHandler.post(new Runnable() {
 @Override
 public void run() {
 ProcessManagerService.this.mContext.sendBroadcastAsUser(intent, new UserHandle(app.userId));
 }
 });
 Slog.i("ProcessManager", "don't kill fmservice just send turn off intent");
 return;
 }
 if (killLevel >= 102) {
 Slog.i("ProcessManager", reason + ": " + killLevelToString(killLevel) + " " + app.processName + " Adj=" + app.curAdj + " State=" + app.getCurProcState());
 }
 if (killLevel == 101) {
 this.mProcessKiller.trimMemory(app, evenForeground);
 } else if (killLevel == 102) {
 this.mProcessKiller.killBackgroundApplication(app, reason);
 } else if (killLevel == 103) {
 this.mProcessKiller.killApplication(app, reason, evenForeground);
 } else if (killLevel == 104) {
 this.mProcessKiller.forceStopPackage(app, reason, evenForeground);
 }
 }
 }
 
 | 
  同时我们也得到了一个非常优秀的 hook 点,只要 hook 这个 killOnce() 方法,并在 beforeHookedMethod 中把 level 修改为 trim-memory,就可以实现应用免杀。
模块开发
  光有这些还不够,要想开发一个成熟的模块,必须能让用户很方便地进行各项配置,这就要求我们的模块进程具有与 system_server 通信的能力(不可能让用户每次修改 AppLock 的作用域都手动修改配置文件,然后重启系统),这里我选择劫持 ProcessManagerNative 类的 onTransact 方法,向 client 发送 Binder 进行通信:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | object AppLockHelper {
 val server by lazy { AppLockManagerService() }
 
 
 
 private object TransactionHook : XC_MethodHook() {
 override fun beforeHookedMethod(param: MethodHookParam) {
 catch {
 if (param.args[0] != TRANSACTION_CODE) return
 if (packageManager.getNameForUid(Binder.getCallingUid()) != BuildConfig.APPLICATION_ID) return
 (param.args[2] as Parcel).writeStrongBinder(server)
 param.result = true
 }
 }
 }
 
 
 
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | object AppLockHelper {
 
 
 val client by lazy {
 MyApplication.processManager?.let {
 val data = Parcel.obtain()
 val reply = Parcel.obtain()
 try {
 if (it.transact(TRANSACTION_CODE, data, reply, 0)) {
 return@lazy IAppLockManager.Stub.asInterface(reply.readStrongBinder())
 }
 } finally {
 data.recycle()
 reply.recycle()
 }
 return@lazy null
 }
 }
 
 
 
 }
 
 | 
  完整代码已经开源到 GitHub,欢迎各位小伙伴来给我点 star(手动狗头
展望
  等等!好像刚刚错过了什么东西,让我们回到 killOnceByPolicy() 方法,再仔细看一看:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | private void killOnceByPolicy(ProcessRecord app, String reason, int policy, boolean canForceStop) {if (app != null && app.thread != null && !app.killed && !skipCurrentProcessInBackup(app, app.info.packageName, UserHandle.getCallingUserId())) {
 int killLevel = 100;
 if (app.isPersistent() || app.setAdj < 0 || isInWhiteList(app, app.userId, policy)) {
 ProcessRecordInjector.addAppPssIfNeeded(this, app);
 if (!app.hasOverlayUi() && !app.hasTopUi() && isTrimMemoryEnable(app.info.packageName)) {
 killLevel = 101;
 }
 } else {
 killLevel = (!isForceStopEnable(app, policy) || !canForceStop) ? policy == 3 ? 102 : 103 : HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION;
 }
 killOnce(app, reason, killLevel);
 }
 }
 
 | 
  我们重点关注第四行这个 if,只要让代码进入这个 if 分支内执行,那么 killLevel 最后不是 100(none)就是 101(trim-memory),应用进程并不会被杀死。在 if 的三个判断条件中,isInWhiteList() 似乎是我们可以控制的东西
  而 MIUI 这样的系统显然不会硬编码规则列表,这自然而然就引出了第二个方案:通过某种手段修改规则列表,将我们需要的应用直接添加到白名单中,实现免注入、即时生效的 AppLock 功能。