app is in background uid UidRecord问题原因分析

应用在适配Android 8.0以上系统时,会发现后台启动不了服务,会报出如下异常,并强退:

Fatal Exception: java.lang.IllegalStateException
Not allowed to start service Intent { act=com.xxx.xxx.xxx pkg=com.xxx.xxx (has extras) }: app is in background uid UidRecord{1cbd9ed u0a1967 CEM idle procs:1 seq(0,0,0)}

问题原因分析

Android 8.0 行为变更

https://developer.android.com/about/versions/oreo/android-8.0-changes.html#back-all

Android 8.0 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍您应该了解并在开发应用时加以考虑的一些主要变更。

其中大部分变更会影响所有应用,而不论应用针对的是何种版本的 Android。不过,有几项变更仅影响针对 Android 8.0 的应用。为清楚起见,本页面分为两个部分:针对所有 API 级别的应用针对 Android 8.0 的应用

 

针对所有 API 级别的应用

这些行为变更适用于 在 Android 8.0 平台上运行的 所有应用,无论这些应用是针对哪个 API 级别构建。所有开发者都应查看这些变更,并修改其应用以正确支持这些变更(如果适用)。

后台执行限制

Android 8.0 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。

此外,为提高设备性能,系统会限制未在前台运行的应用的某些行为。具体而言:

  • 现在,在后台运行的应用对后台服务的访问受到限制。
  • 应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)。

默认情况下,这些限制仅适用于针对 O 的应用。不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台。

Android 8.0 还对特定函数做出了以下变更:

  • 如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException
  • 新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。

如需了解详细信息,请参阅后台执行限制

出错代码定位

在ContextImpl的startServiceCommon函数中爆出异常, 
http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/ContextImpl.java

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("?")) {
                    throw new IllegalStateException(
                            "Not allowed to start service " + service + ": " + cn.getClassName());
                }
            }
            return cn;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

startServiceCommon这个函数做的操作是AMS的startService,用于启动服务

AMS的startService

接下去看AMS的startService,稍微注意一下传递的参数,里面有一个前台后台相关的requireForeground,可能跟问题有关系。 
AMS代码位置 

http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

@Override
public ComponentName startService(IApplicationThread caller, Intent service,
            String resolvedType, boolean requireForeground, String callingPackage, int userId)
           throws TransactionTooLargeException {
        enforceNotIsolatedCaller("startService");
        // Refuse possible leaked file descriptors
       if (service != null && service.hasFileDescriptors() == true) {
            throw new IllegalArgumentException("File descriptors passed in Intent");
       }

        if (callingPackage == null) {
            throw new IllegalArgumentException("callingPackage cannot be null");
       }

        if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
                "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground);
       synchronized(this) {
            final int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            final long origId = Binder.clearCallingIdentity();
            ComponentName res;
            try {
            //调用ActiveServices的startServiceLocked
                res = mServices.startServiceLocked(caller, service,
                        resolvedType, callingPid, callingUid,
                        requireForeground, callingPackage, userId);
            } finally {
                Binder.restoreCallingIdentity(origId);
            }
            return res;
        }
    }

会调用ActiveServices的startServiceLocked

ActiveServices的startServiceLocked

http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
            int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
            throws TransactionTooLargeException {
        //...
        // 启动服务之前有2个判断一个是startRequested,一个是fgRequired。
        // startRequested代表的是:是否已经启动过服务,一般出现问题都是启动一个没有运行的服务,
        // 那么这个就是false。
        // fgRequired这个就是启动服务传递的requireForeground,
        if (!r.startRequested && !fgRequired) {
            // 这里面有个关键函数getAppStartModeLocked,判断是否运行启动服务
            // 注意此处传递的最后2个参数:alwaysRestrict和disabledOnly都是false
            final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
                    r.appInfo.targetSdkVersion, callingPid, false, false);
            // 如果不允许启动服务则会运行到里面
            if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
                //...
                UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
                // 此处就是不允许运行服务返回的原因"app is in background"
                // 找到了原因就在这里
                return new ComponentName("?", "app is in background uid " + uidRec);
            }
        }
        //...
    }

这里面由于出现错误,那么startRequested==false而且fgRequired==false,说明这个服务是第一次启动,而且是后台请求启动服务。
至于为什么不允许启动服务,我们还需要查看AMS的getAppStartModeLocked函数。

AMS判断并返回服务启动模式

http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
            int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
        UidRecord uidRec = mActiveUids.get(uid);
        if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg="
                + packageName + " rec=" + uidRec + " always=" + alwaysRestrict + " idle="
                + (uidRec != null ? uidRec.idle : false));
        if (uidRec == null || alwaysRestrict || uidRec.idle) {
            boolean ephemeral;
            if (uidRec == null) {
                ephemeral = getPackageManagerInternalLocked().isPackageEphemeral(
                        UserHandle.getUserId(uid), packageName);
            } else {
                ephemeral = uidRec.ephemeral;
            }

            if (ephemeral) {
                // We are hard-core about ephemeral apps not running in the background.
                return ActivityManager.APP_START_MODE_DISABLED;
            } else {
                if (disabledOnly) {
                    // The caller is only interested in whether app starts are completely
                    // disabled for the given package (that is, it is an instant app).  So
                    // we don‘t need to go further, which is all just seeing if we should
                    // apply a "delayed" mode for a regular app.
                    return ActivityManager.APP_START_MODE_NORMAL;
                }
          // 此处alwaysRestrict==false,于是调用的是appServicesRestrictedInBackgroundLocked
final int startMode = (alwaysRestrict) ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk) : appServicesRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg=" + packageName + " startMode=" + startMode + " onwhitelist=" + isOnDeviceIdleWhitelistLocked(uid)); if (startMode == ActivityManager.APP_START_MODE_DELAYED) { // This is an old app that has been forced into a "compatible as possible" // mode of background check. To increase compatibility, we will allow other // foreground apps to cause its services to start. if (callingPid >= 0) { ProcessRecord proc; synchronized (mPidsSelfLocked) { proc = mPidsSelfLocked.get(callingPid); } if (proc != null && !ActivityManager.isProcStateBackground(proc.curProcState)) { // Whoever is instigating this is in the foreground, so we will allow it // to go through. return ActivityManager.APP_START_MODE_NORMAL; } } } return startMode; } } return ActivityManager.APP_START_MODE_NORMAL; }

根据alwaysRestrict的值会调用appRestrictedInBackgroundLocked或者appServicesRestrictedInBackgroundLocked;
其中appRestrictedInBackgroundLocked是直接根据应用sdk进行判断,
appServicesRestrictedInBackgroundLocked会进行条件过滤,直接运行部分应用启动服务,其它的进行应用sdk的判断。

接下来看appServicesRestrictedInBackgroundLocked进行条件过滤,允许部分启动服务

int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
        // 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?
     // 如果是非常驻内存的话,但是在白名单列表里面的uid也是允许的 
     // 目前这个白名单里面就只有一个:蓝牙BLUETOOTH_UID = 1002

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

        // None of the service-policy criteria apply, so we apply the common criteria
        return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
    }

分别对于: 
1. 是否常驻内存应用,常驻内存允许启动服务 
2.如果是蓝牙也是允许启动服务 
3.是在电源相关的DeviceIdle白名单里面,允许启动服务的 
4.如果都不是则执行默认策略appRestrictedInBackgroundLocked

再看appServicesRestrictedInBackgroundLocked进行条件过滤,允许部分启动服务

 int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
        // Apps that target O+ are always subject to background check
        // 如果apk的sdk版本大于AndroidO的话,那么默认是不允许启动服务的

        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
       // 如果是之前版本的apk,会查看AppOps是否允许后台运行权限, 
       // 由于我们sdk版本肯定会升级的,这个就暂时不考虑了

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

如果apk的sdk版本大于AndroidO的话,那么默认是不允许启动服务的,那么要适配Android O/GO以后的版本,此处是绕不过去的坎,建议尽早处理。

修改方案

有上面可知,问题原因主要是后台启动了服务,在这部分Android O/GO做了限制,
根据章节2.4的过滤条件可以提供如下修改方案:
1) 提升应用优先级到常驻内存级别 (不建议应用采纳这种方式,会导致手机出现很多性能问题)
=> 在AndroidManifest.xml添加android:persistent=”true”
并且签上系统签名
2) 类似与蓝牙BLUETOOTH_UID一样放在白名单里面(需要拥有源码修改权限,而且修改了源码,不利于apk的版本兼容,不建议采纳)
3) 添加在电源相关的DeviceIdle白名单(不建议添加,可能导致功耗增加)

按照上面的都说是不建议采取,是否没有办法了呢?

我们继续往源头找找看看是否有办法:
在ContextImpl的startServiceCommon、ActiveServices的startServiceLocked有一个参数requireForeground/fgRequired,是否前台请求,如果requireForeground/fgRequired为false才会进行后台请求判断,如果是true的话,是可以直接绕过去的

回到ActiveServices的startServiceLocked=>

  ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
            int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
            throws TransactionTooLargeException {
        //...
        // fgRequired是true可以直接绕过
        if (!r.startRequested && !fgRequired) {
            //..
        }
        //...
    }

那么方案4,我们可以采取如下方式
4) 通过Context(activity、service的this都是包含context的,故不用担心调用方式),将之前的startService,修改成ContextImpl的startForegroundService或者startForegroundServiceAsUser方法,启动一个前台服务。
//frameworks/base/core/java/android/app/ContextImpl.java

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

    @Override
    public ComponentName startForegroundServiceAsUser(Intent service, UserHandle user) {
        return startServiceCommon(service, true, user);
    }

ps:注意上面的方法是启动前台服务,你的服务需要是前台的,这个怎么做呢,下面提供2种方法:
1) 在service中调用startForeground (最常见方法)
2) 设置service为前台,可以使用AMS的setProcessImportant设置优先级别 (优点是:不会在通知栏中出现通知图标。缺点是:需要相应的权限)

 

解决办法

https://www.cnblogs.com/mingfeng002/p/9647720.html

app is in background uid UidRecord问题原因分析

上一篇:scheme :The components of a URL


下一篇:keyup在移动端失效解决方法