背景

  我们在使用手机时,难免需要对某些应用做后台保活,比如社交软件、音乐播放器、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 验证一下:

1
2
3
4
5
findMethod("com.android.systemui.shared.recents.system.ProcessManagerWrapper") {
name == "doOneKeyClean"
}!! before {
Log.i("called doOneKeyClean!")
}

  发现确实调用了它,接下来看看它是怎么写的:

1
2
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()

1
2
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() 是如何工作的:

1
2
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

1
2
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 从何而来:

1
2
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:

1
2
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() 方法:

  • doOneKeyClean
1
2
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);
}
}
}
}
  • doSwapUPClean
1
2
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() 方法:

1
2
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 代表的含义:

1
2
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 /* 104 */:
return "force-stop";
default:
return "";
}
}

  其中有一个 level 是 ProcessConfig.KILL_LEVEL_TRIM_MEMORY(101),它不会杀死应用进程,只会对其进行一些内存清理,而这正是我们所需要的功能

1
2
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() { // from class: com.android.server.am.ProcessManagerService.1
@Override // java.lang.Runnable
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 进行通信:

  • Server 端
1
2
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
}
}
}

// ............

}
  • Client 端
1
2
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() 方法,再仔细看一看:

1
2
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 功能。