提升进程优先级的一种新姿势

在安卓中调用Service的startForeground方法可以将service所在进程的优先级提高,减小进程被回收的概率。
调用startForeground方法的时候系统会在通知栏显示一个通知,这对于传统的多媒体应用来说是没有问题的。
但是对于那些只想提升优先级而不想让用户感知的应用来说强行显示个通知栏看起来太怪异了。

查看startForeground的源码之后,发现有很重要的两步,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ActiveServices.java
private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
Notification notification, int flags) {
if (id != 0) {
......
r.postNotification(); //step1: 在通知栏显示通知
if (r.app != null) {
updateServiceForegroundLocked(r.app, true); //step2: 更新进程优先级
}
getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
mAm.notifyPackageUse(r.serviceInfo.packageName,
PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
} else {
......
}
}

那么我们有没有办法能让updateServiceForegroundLocked执行成功,而让postNotification()执行失败呢?
进一步查看postNotification()方法:

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
ServiceRecord.java
public void postNotification() {
if (foregroundId != 0 && foregroundNoti != null) {
// Do asynchronous communication with notification manager to
// avoid deadlocks.
final String localPackageName = packageName;
final int localForegroundId = foregroundId;
final Notification _foregroundNoti = foregroundNoti;
ams.mHandler.post(new Runnable() {
public void run() {
NotificationManagerInternal nm = LocalServices.getService(
NotificationManagerInternal.class);
if (nm == null) {
return;
}
Notification localForegroundNoti = _foregroundNoti;
try {
......
//step1: 向NotificationManagerServervice发送通知
nm.enqueueNotification(localPackageName, localPackageName,
appUid, appPid, null, localForegroundId, localForegroundNoti,
userId);
} catch (RuntimeException e) {
Slog.w(TAG, "Error showing notification for service", e);
// If it gave us a garbage notification, it doesn't
// get to be foreground.
ams.setServiceForeground(name, ServiceRecord.this,
0, null, 0);
//step2:如果出现异常,则将应用进程crash掉
ams.crashApplication(appUid, appPid, localPackageName, -1,
"Bad notification for startForeground: " + e);
}
}
});
}
}

由上可知,只要enqueueNotification执行出现异常,通知栏则不会显示通知了,但是此时却会导致应用进程crash调用。
那么如何使得crashApplication这个方法失效呢,我们进一步查看crashApplication方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ActivityManagerService.java
public void crashApplication(int uid, int initialPid, String packageName, int userId,
String message) {
if (checkCallingPermission(android.Manifest.permission.FORCE_STOP_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
String msg = "Permission Denial: crashApplication() from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid()
+ " requires " + android.Manifest.permission.FORCE_STOP_PACKAGES;
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
synchronized(this) {
//最终会通过binder,调用应用进程中的scheduleCrash方法
mAppErrors.scheduleAppCrashLocked(uid, initialPid, packageName, userId, message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ActivityThread.java
private class ApplicationThread extends IApplicationThread.Stub {
public void scheduleCrash(String msg) {
//通过handler发送一个类型为H.SCHEDULE_CRASH的消息
sendMessage(H.SCHEDULE_CRASH, msg);
}
}
private class H extends Handler {
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
......
case SCHEDULE_CRASH:
//当收到类型为SCHEDULE_CRASH的消息的时候则抛出一个异常,导致进程crash
throw new RemoteServiceException((String)msg.obj);
......
}
}

我们现在已经知道了ams是如何让我们的进程crash的了,基本就是ams跟我们应用进程说,你准备准备该去死了,然后应用进程就去死了。
但是做为一个有个性的进程,能不能在ams让他去死的时候假装没听见呢?显然是可以的,基本的流程就是:

  1. 先拿到ActivityThread的实例
  2. 拿到ActivityThread$H 的实例mH
  3. 向mH设置一个Handler.Callback
  4. 在Handler.Callback 中检测到 SCHEDULE_CRASH消息时则消费该消息

具体实现代码如下所示:

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
private static void hookH(){
if(mHasHookH){
return;
}
mHasHookH = true;
try {
try {
Class hClass = Class.forName("android.app.ActivityThread$H");
Field scheduleCrashField = hClass.getDeclaredField("SCHEDULE_CRASH");
mScheduleCrashMsgWhat = (int)scheduleCrashField.get(null);
Log.i(TAG, "get mScheduleCrashMsgWhat success");
}catch (Exception e){
Log.i(TAG, "get mScheduleCrashMsgWhat failed");
e.printStackTrace();
}
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Log.i(TAG, msg.toString());
if(msg.what == mScheduleCrashMsgWhat){
return true;
}
return false;
}
};
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Field mH = activityThreadClass.getDeclaredField("mH");
mH.setAccessible(true);
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThreadInstance = currentActivityThread.invoke(null);
Handler hInstance = (Handler) mH.get(activityThreadInstance);
Class handlerClass = Handler.class;
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(hInstance, callback);
}catch (Exception e){
e.printStackTrace();
}
}

这里写了一个工具类方便大家使用:RaisePriorityHack