Android8.0的后臺Service優(yōu)化源碼解析

今天在用戶的錯誤列表上看到這么個bug

java.lang.RuntimeException: Unable to start receiver com.anysoft.tyyd.appwidget.PlayAppWidgetProvider: 
java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.anysoft.tyyd/.play.PlayerService }: 
app is in background uid UidRecord{607ef50 u0a127 RCVR idle change:idle|uncached procs:1 seq(0,0,0)}

這個bug是在適配Android8.0后出現(xiàn)的,解釋下就是,app在后臺uid的進程下面不允許啟動Service.

重現(xiàn)情景:

由于我們的桌面小控件在onUpdate()方法里用Context.startService()啟動了Service.當(dāng)app的進程沒有啟動時,把桌面部件拉到Launcher桌面上就會報這個錯誤.

先來看看Android官網(wǎng)在8.0時的后臺服務(wù)啟動優(yōu)化的一些措施:

后臺服務(wù)限制:處于空閑狀態(tài)時,應(yīng)用可以使用的后臺服務(wù)存在限制。 這些限制不適用于前臺服務(wù),因為前臺服務(wù)更容易引起用戶注意。
在 Android 8.0 之前,創(chuàng)建前臺服務(wù)的方式通常是先創(chuàng)建一個后臺服務(wù),然后將該服務(wù)推到前臺。
Android 8.0 有一項復(fù)雜功能;系統(tǒng)不允許后臺應(yīng)用創(chuàng)建后臺服務(wù)。 因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前臺啟動新服務(wù)。
在系統(tǒng)創(chuàng)建服務(wù)后,應(yīng)用有五秒的時間來調(diào)用該服務(wù)的 startForeground()方法以顯示新服務(wù)的用戶可見通知。
如果應(yīng)用在此時間限制內(nèi)調(diào)用 startForeground(),則系統(tǒng)將停止服務(wù)并聲明此應(yīng)用為 ANR。

我總結(jié)一下就是8.0后,如果一個處于后臺的應(yīng)用想要啟動Service就必須調(diào)用Context.startForegroundService()并且5秒內(nèi)在該Service內(nèi)調(diào)用startForeground()

下面看看源碼的變動情況

源碼解析:

首先是后臺應(yīng)用調(diào)用Context.startService()啟動Service為什么會報錯

啟動Service的入口ContextImpl.startService()

ContextImpl:

    @Override
    public ComponentName startService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, false, mUser);
    }
    //進入startServiceCommon()
    private ComponentName startServiceCommon(Intent service, boolean requireForeground,
            UserHandle user) {
        try {
            validateServiceIntent(service);
            service.prepareToLeaveProcess(this);
            ComponentName cn = ActivityManager.getService().startService(
                mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                            getContentResolver()), requireForeground,
                            getOpPackageName(), user.getIdentifier());
            if (cn != null) {
                if (cn.getPackageName().equals("!")) {
                    throw new SecurityException(
                            "Not allowed to start service " + service
                            + " without permission " + cn.getClassName());
                } else if (cn.getPackageName().equals("!!")) {
                    throw new SecurityException(
                            "Unable to start service " + service
                            + ": " + cn.getClassName());
                } else if (cn.getPackageName().equals("?")) {//1
                    throw new IllegalStateException(
                            "Not allowed to start service " + service + ": " + cn.getClassName());
                }
            }
            return cn;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

1處就是我們bug拋出異常的地方Not allowed to start service Intent...
我們先看看ActivityManager.getService().startService()的返回邏輯

ActivityManagerService:

    @Override
    public ComponentName startService(
        ...
        try {
              res = mServices.startServiceLocked(caller, service resolvedType, callingPid, callingUid, requireForeground, callingPackage, userId);
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
        return res;
    }

啟動Service會調(diào)用ActiveServices.startServiceLocked()

ActiveServices:

  ComponentName startServiceLocked(...){
        ...
        // If this isn't a direct-to-foreground start, check our ability to kick off an
        // arbitrary service
        if (!r.startRequested && !fgRequired) {
            // Before going further -- if this app is not allowed to start services in the
            // background, then at this point we aren't going to let it period.
            final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
                    r.appInfo.targetSdkVersion, callingPid, false, false);
            if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
                Slog.w(TAG, "Background start not allowed: service "
                        + service + " to " + r.name.flattenToShortString()
                        + " from pid=" + callingPid + " uid=" + callingUid
                        + " pkg=" + callingPackage);
                if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
                    return null;
                }
                UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
                //2.
                return new ComponentName("?", "app is in background uid " + uidRec);
            }
        }
  }    

這里的fgRequired是從ContextImpl.startServiceCommon(fgRequired:false)傳進來的,為false.
2標(biāo)記處是不是又看到相關(guān)bug信息了 "app is in background uid...",于是我們看看allowed返回值mAm.getAppStartModeLocked()

ActivityManagerService:

  int getAppStartModeLocked(){
      UidRecord uidRec = mActiveUids.get(uid);
      ...
      if (uidRec == null || alwaysRestrict || uidRec.idle) {
          final int startMode = (alwaysRestrict) ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk) : 
          appServicesRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
      }
      return startMode;
      ...
  }
  

allowed的返回值就是startMode.這里alwaysRestrict是傳入的參數(shù)false,這里的uidRec由于應(yīng)用進程都未啟動,于是uidRec.idle為true表示空閑進程,所以我們直接看appServicesRestrictedInBackgroundLocked()

ActivityManagerService:

  int appServicesRestrictedInBackgroundLocked(){
      ...
      // Persistent app?
      if (mPackageManagerInt.isPackagePersistent(packageName)) {
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "App " + uid + "/" + packageName
                        + " is persistent; not restricted in background");
            }
            return ActivityManager.APP_START_MODE_NORMAL;
      }

      // Non-persistent but background whitelisted?
      if (uidOnBackgroundWhitelist(uid)) {
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "App " + uid + "/" + packageName
                        + " on background whitelist; not restricted in background");
            }
            return ActivityManager.APP_START_MODE_NORMAL;
      }

      // Is this app on the battery whitelist?
      if (isOnDeviceIdleWhitelistLocked(uid)) {
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "App " + uid + "/" + packageName
                        + " on idle whitelist; not restricted in background");
            }
            return ActivityManager.APP_START_MODE_NORMAL;
      }
      return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
  }

這個方法會判斷是否是Persistent app,白名單,電量白名單應(yīng)用,很顯然普通app都不是,于是進入appRestrictedInBackgroundLocked()看看

ActivityManagerService:

        // Apps that target O+ are always subject to background check
        if (packageTargetSdk >= Build.VERSION_CODES.O) {
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
            }
            return ActivityManager.APP_START_MODE_DELAYED_RIGID;
        }
        // ...and legacy apps get an AppOp check
        int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND,
                uid, packageName);
        if (DEBUG_BACKGROUND_CHECK) {
            Slog.i(TAG, "Legacy app " + uid + "/" + packageName + " bg appop " + appop);
        }
        switch (appop) {
            case AppOpsManager.MODE_ALLOWED:
                return ActivityManager.APP_START_MODE_NORMAL;
            case AppOpsManager.MODE_IGNORED:
                return ActivityManager.APP_START_MODE_DELAYED;
            default:
                return ActivityManager.APP_START_MODE_DELAYED_RIGID;
        }

這里的packageTargetSdk剛好是O,所以返回ActivityManager.APP_START_MODE_DELAYED_RIGID了.由于返回值不是ActivityManager.APP_START_MODE_NORMAL.于是就return new ComponentName("?", "app is in background uid " + uidRec);然后就出現(xiàn)了開頭的異常.

下面看下Context.startForegroundService啟動Service的邏輯

入口依舊為ContextImpl.startForegroundService()

    @Override
    public ComponentName startForegroundService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, true, mUser);
    }

這里與startService的區(qū)別就在于傳入的fgRequired為true.于是一路
ContextImpl.startServiceCommon()-->ActivityManagerService.startService()-->ActiveServices.startServiceLocked(),由于fgRequired為true,就跳過剛才那段邏輯下面就是正常的Service啟動流程了.
那么還有一個問題,為什么還需要在5秒內(nèi)調(diào)用Service.startForeground()呢?
在啟動Service的過程中會調(diào)用到ActiveServices.bringUpServiceLocked()方法,然后會調(diào)用ActiveServices.sendServiceArgsLocked()

ActiveServices:
    ...
    while (r.pendingStarts.size() > 0) {
    ...
    if (r.fgRequired && !r.fgWaiting) {
        if (!r.isForeground) {
            //3
            scheduleServiceForegroundTransitionTimeoutLocked(r);
        } else {
            r.fgRequired = false;
        }
    }
    ...
    }

在3處會調(diào)用scheduleServiceForegroundTransitionTimeoutLocked()作用就是發(fā)送一個延時5秒的message

ActiveServices:

    void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) {
        if (r.app.executingServices.size() == 0 || r.app.thread == null) {
            return;
        }
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG);
        msg.obj = r;
        r.fgWaiting = true;
        mAm.mHandler.sendMessageDelayed(msg, SERVICE_START_FOREGROUND_TIMEOUT);//這個值是5*1000
    }

看下這個消息的處理

ActivityManagerService:
  
  class MainHandler extends Handler{
      @Override
      public void handleMessage(Message msg) {
            switch (msg.what) {
                ...
                case SERVICE_FOREGROUND_TIMEOUT_MSG: {
                mServices.serviceForegroundTimeout((ServiceRecord)msg.obj);
            }
      }
  }

又來到ActiveServices

ActiveServices:
  
    void serviceForegroundTimeout(ServiceRecord r) {
        ProcessRecord app;
        synchronized (mAm) {
            if (!r.fgRequired || r.destroying) {
                return;
            }
            app = r.app;
            r.fgWaiting = false;
            stopServiceLocked(r);
        }
    }

這里就是調(diào)用stopServiceLocked(r)把service關(guān)掉了.那么Service.startForeground()一定會有代碼取消這個消息,來看:

Service:
  
  public final void startForeground(int id, Notification notification) {
        try {
            mActivityManager.setServiceForeground(
                    new ComponentName(this, mClassName), mToken, id,
                    notification, 0);
        } catch (RemoteException ex) {
        }
  }

mActivityManager就是調(diào)用AMS

ActivityManagerService:

  @Override
  public void setServiceForeground(ComponentName className, IBinder token,
            int id, Notification notification, int flags) {
        synchronized(this) {
            mServices.setServiceForegroundLocked(className, token, id, notification, flags);
        }
  }

ActiveServices:
  public void setServiceForegroundLocked(ComponentName className, IBinder token,
            int id, Notification notification, int flags) {
        final int userId = UserHandle.getCallingUserId();
        final long origId = Binder.clearCallingIdentity();
        try {
            ServiceRecord r = findServiceLocked(className, token, userId);
            if (r != null) {
                setServiceForegroundInnerLocked(r, id, notification, flags);
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
  }

來看setServiceForegroundInnerLocked()

ActiveServices:
  private void setServiceForegroundInnerLocked(){
    ...
    if (r.fgRequired) {
        if (DEBUG_SERVICE || DEBUG_BACKGROUND_CHECK) { Slog.i(TAG, "Service called startForeground() as required: " + r);}
                r.fgRequired = false;
                r.fgWaiting = false;
                mAm.mHandler.removeMessages(
                        ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
    }
    ...
  }

這里就removeMessages(SERVICE_FOREGROUND_TIMEOUT_MSG)取消這個message了.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容