欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

黑科技!提升进程优先级的一种新姿势

程序员文章站 2022-06-09 23:10:03
...
黑科技!提升进程优先级的一种新姿势

大家好我是张拭心,今天这篇文章是我认识的一个优秀少年 easoll 的分享,他在阅读 framework 源码后发现了一种新的提升进程优先级的方法,绝对是你之前没有用过的方法,值得学习!

作者:easoll

原文地址:https://easoll.github.io/

想了解更多的朋友欢迎复制链接或者点击“阅读原文”去作者博客



大家都知道在安卓中调用  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 的实例

  2. 拿到 ActivityThread$H 的实例 mH

  3. 向 mH 设置一个 Handler.Callback

  4. 在 Handler.Callback 中检测到 SCHEDULE_CRASH 消息时则消费该消息

具体实现代码如下所示:

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

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

作者写了一个工具类方便大家使用:

https://github.com/easoll/RaisePriorityHack,觉得有帮助就给个 star 吧!




推荐阅读

我的安卓开发半年工作经验总结

安卓开发必备知识体系:Java篇

跳槽需要内推?这里有一大波机会



黑科技!提升进程优先级的一种新姿势

点击“阅读原文”去作者博客阅读更多精彩文章