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

今天在用戶的錯(cuò)誤列表上看到這么個(gè)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)}

這個(gè)bug是在適配Android8.0后出現(xiàn)的,解釋下就是,app在后臺(tái)uid的進(jìn)程下面不允許啟動(dòng)Service.

重現(xiàn)情景:

由于我們的桌面小控件在onUpdate()方法里用Context.startService()啟動(dòng)了Service.當(dāng)app的進(jìn)程沒有啟動(dòng)時(shí),把桌面部件拉到Launcher桌面上就會(huì)報(bào)這個(gè)錯(cuò)誤.

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

后臺(tái)服務(wù)限制:處于空閑狀態(tài)時(shí)郁稍,應(yīng)用可以使用的后臺(tái)服務(wù)存在限制。 這些限制不適用于前臺(tái)服務(wù)猖凛,因?yàn)榍芭_(tái)服務(wù)更容易引起用戶注意删掀。
在 Android 8.0 之前,創(chuàng)建前臺(tái)服務(wù)的方式通常是先創(chuàng)建一個(gè)后臺(tái)服務(wù)躬翁,然后將該服務(wù)推到前臺(tái)怠堪。
Android 8.0 有一項(xiàng)復(fù)雜功能;系統(tǒng)不允許后臺(tái)應(yīng)用創(chuàng)建后臺(tái)服務(wù)隙疚。 因此壤追,Android 8.0 引入了一種全新的方法磕道,即 Context.startForegroundService(),以在前臺(tái)啟動(dòng)新服務(wù)行冰。
在系統(tǒng)創(chuàng)建服務(wù)后溺蕉,應(yīng)用有五秒的時(shí)間來調(diào)用該服務(wù)的 startForeground()方法以顯示新服務(wù)的用戶可見通知。
如果應(yīng)用在此時(shí)間限制內(nèi)調(diào)用 startForeground()悼做,則系統(tǒng)將停止服務(wù)并聲明此應(yīng)用為 ANR疯特。

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

下面看看源碼的變動(dòng)情況

源碼解析:

首先是后臺(tái)應(yīng)用調(diào)用Context.startService()啟動(dòng)Service為什么會(huì)報(bào)錯(cuò)

啟動(dòng)Service的入口ContextImpl.startService()

ContextImpl:

    @Override
    public ComponentName startService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, false, mUser);
    }
    //進(jìn)入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;
    }

啟動(dòng)Service會(huì)調(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)傳進(jìn)來的,為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)用進(jìn)程都未啟動(dòng),于是uidRec.idle為true表示空閑進(jìn)程,所以我們直接看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);
  }

這個(gè)方法會(huì)判斷是否是Persistent app,白名單,電量白名單應(yīng)用,很顯然普通app都不是,于是進(jìn)入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啟動(dòng)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啟動(dòng)流程了.
那么還有一個(gè)問題,為什么還需要在5秒內(nèi)調(diào)用Service.startForeground()呢?
在啟動(dòng)Service的過程中會(huì)調(diào)用到ActiveServices.bringUpServiceLocked()方法,然后會(huì)調(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處會(huì)調(diào)用scheduleServiceForegroundTransitionTimeoutLocked()作用就是發(fā)送一個(gè)延時(shí)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);//這個(gè)值是5*1000
    }

看下這個(gè)消息的處理

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()一定會(huì)有代碼取消這個(gè)消息,來看:

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)取消這個(gè)message了.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肛走,隨后出現(xiàn)的幾起案子漓雅,更是在濱河造成了極大的恐慌,老刑警劉巖朽色,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邻吞,死亡現(xiàn)場離奇詭異,居然都是意外死亡葫男,警方通過查閱死者的電腦和手機(jī)抱冷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梢褐,“玉大人旺遮,你說我怎么就攤上這事∮龋” “怎么了耿眉?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鱼响。 經(jīng)常有香客問我鸣剪,道長,這世上最難降的妖魔是什么热押? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任西傀,我火速辦了婚禮,結(jié)果婚禮上桶癣,老公的妹妹穿的比我還像新娘拥褂。我一直安慰自己,他們只是感情好牙寞,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布饺鹃。 她就那樣靜靜地躺著莫秆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悔详。 梳的紋絲不亂的頭發(fā)上镊屎,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音茄螃,去河邊找鬼缝驳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛归苍,可吹牛的內(nèi)容都是我干的用狱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼拼弃,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼夏伊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吻氧,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤溺忧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后盯孙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鲁森,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年镀梭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刀森。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡报账,死狀恐怖研底,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情透罢,我是刑警寧澤榜晦,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站羽圃,受9級(jí)特大地震影響乾胶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朽寞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一识窿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脑融,春花似錦喻频、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锻煌。三九已至,卻和暖如春姻蚓,著一層夾襖步出監(jiān)牢的瞬間宋梧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工狰挡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捂龄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓圆兵,卻偏偏與公主長得像跺讯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子殉农,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容