作者:easoll




大家都知道在安卓中调用  Service 的 startForeground()  方法可以将Service  所在进程的优先级提高,减小进程被回收的概率。


调用 startForeground() 方法的时候系统会在通知栏显示一个通知,这对于传统的多媒体应用来说是没有问题的。但是对于那些只想提升优先级而不想让用户感知的应用来说,强行显示个通知栏看起来太怪异了。查看 startForeground() 的源码之后,发现有很重要的两步,如下所示:

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

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() 方法的代码:

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);
    }
}
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

  1.  的实例
  2. 拿到 

ActivityThread$H

  1.  的实例 

mH

  1. 向 

mH

  1.  设置一个 

Handler.Callback

  1. 在 

Handler.Callback

  1.  中检测到 

SCHEDULE_CRASH

  1.  消息时则消费该消息

具体实现代码如下所示:

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();
        }
    }

这样就可以实现优先级提升而又不弹出通知栏了。