Android8.0的后台Service优化源码解析

今天在用户的错误列表上看到这么个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后出现的,解释下就是,app在后台uid的进程下面不允许启动Service.

重现情景:

由于我们的桌面小控件在onUpdate()方法里用Context.startService()启动了Service.当app的进程没有启动时,把桌面部件拉到Launcher桌面上就会报这个错误.

先来看看Android官网在8.0时的后台服务启动优化的一些措施:

后台服务限制:处于空闲状态时,应用可以使用的后台服务存在限制。 这些限制不适用于前台服务,因为前台服务更容易引起用户注意。
在 Android 8.0 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台。
Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务。 因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。
在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground()方法以显示新服务的用户可见通知。
如果应用在此时间限制内调用 startForeground(),则系统将停止服务并声明此应用为 ANR

我总结一下就是8.0后,如果一个处于后台的应用想要启动Service就必须调用Context.startForegroundService()并且5秒内在该Service内调用startForeground()

下面看看源码的变动情况

源码解析:

首先是后台应用调用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会调用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标记处是不是又看到相关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是传入的参数false,这里的uidRec由于应用进程都未启动,于是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,白名单,电量白名单应用,很显然普通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);然后就出现了开头的异常.

下面看下Context.startForegroundService启动Service的逻辑

入口依旧为ContextImpl.startForegroundService()

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

这里与startService的区别就在于传入的fgRequired为true.于是一路
ContextImpl.startServiceCommon()-->ActivityManagerService.startService()-->ActiveServices.startServiceLocked(),由于fgRequired为true,就跳过刚才那段逻辑下面就是正常的Service启动流程了.
那么还有一个问题,为什么还需要在5秒内调用Service.startForeground()呢?
在启动Service的过程中会调用到ActiveServices.bringUpServiceLocked()方法,然后会调用ActiveServices.sendServiceArgsLocked()

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

在3处会调用scheduleServiceForegroundTransitionTimeoutLocked()作用就是发送一个延时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);
        }
    }

这里就是调用stopServiceLocked(r)把service关掉了.那么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就是调用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了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容