应用在后台运行时,会消耗一部分有限的设备资源,例如 RAM。这可能会影响用户体验,如果用户正在使用占用大量资源的应用(例如玩游戏或观看视频),影响会尤为明显。为了提升用户体验,Android 8.0(API 级别 26)对应用在后台运行时可以执行的操作施加了限制。

限制了什么?

1. 后台应用对后台服务的访问受到限制

在不与用户直接交互的后台应用中,运行 Service 会消耗系统资源,这可能会影响前台应用的正常运行。Android 8.0 及更高版本「不允许后台应用运行后台服务」,需要通过 startForegroundService() 指定为前台服务运行,或者使用 JobScheduler 替代。

2. 注册隐式广播接收器受到限制

对于一些系统隐式广播(非全部),系统不允许应用在 AndroidManifest 中注册对应的广播接收器,从而避免系统广播导致诸多应用快速、连续消耗系统资源,从而影响用户体验,需要通过 Context.registerReceiver() 动态注册或 JobScheduler 代替。

3. 降低了后台应用接收位置更新的频率

为节约电池电量、保持良好的用户体验和确保系统健康运行,在运行 Android 8.0 的设备上使用后台应用时,降低了后台应用接收位置更新的频率。

什么是前台应用?

系统可以区分前台和后台应用。如果满足以下任意条件,应用将被视为处于前台:

  1. 具有可见 Activity
  2. 具有前台 Service
  3. 另一个前台应用已关联到该应用(绑定 Service 或使用 content providers)

如果以上条件均不满足,应用将被视为处于后台。

正确理解后台服务限制

「不允许后台应用运行后台服务」

官网的这句描述很简单,但你真的明白它的含义吗?顺着这句话推导一下:

后台应用无法启动后台服务

-> 前台应用可以启动后台服务

-> A 为前台应用,则 A 就能启动后台服务

基于这个结论,再结合后台服务的种类,对以下三种场景实践验证,结果如下:

  1. 若后台服务属于 A 应用进程,则能正常启动
  2. 若后台服务属于 B 应用进程,且 B 是前台应用,则能正常启动
  3. 若后台服务属于 B 应用进程,且 B 是后台应用,则无法启动!

通过第三种场景的验证结果,可以知道 不允许后台应用运行后台服务 这个描述是不准确、有歧义的,更精准的描述应该是:

「不允许启动属于后台应用的后台服务」

后台服务限制源码分析

若在 Android 8.0 设备上通过 startService 启动一个属于后台应用的后台服务,会直接崩溃:

Caused by: java.lang.IllegalStateException: Not allowed to start service Intent        { act=intent.action.ServerService pkg=com.server }: app is in background uid null   at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1577)   at android.app.ContextImpl.startService(ContextImpl.java:1532)   at android.content.ContextWrapper.startService(ContextWrapper.java:664)   ...

下面以此异常为线索,一步一步来看源码中是如何限制的。异常在 ContextImpl 中抛出:

private ComponentName startServiceCommon(Intent service, boolean requireForeground,        UserHandle user) {    ...    ComponentName cn = ActivityManager.getService().startService(        mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(                    getContentResolver()), requireForeground,                    getOpPackageName(), user.getIdentifier());    if (cn != null) {        if (cn.getPackageName().equals("!")) {            ...        } else if (cn.getPackageName().equals("?")) {            //这里抛出启动服务限制的异常            throw new IllegalStateException(                    "Not allowed to start service " + service + ": " + cn.getClassName());        }    }    return cn;}

可见关键点是 cn.getPackageName().equals("?") 条件成立, 继续看 AMS startService 方法中是如何返回这个 ComponentName 的:

public ComponentName startService(IApplicationThread caller, Intent service,        String resolvedType, boolean requireForeground, String callingPackage, int userId)throws TransactionTooLargeException {    ...    ComponentName res;    try {        res = mServices.startServiceLocked(caller, service,                resolvedType, callingPid, callingUid,                requireForeground, callingPackage, userId);    } finally {        Binder.restoreCallingIdentity(origId);    }    return res;}

AMS 中转调 ActiveServices 的 startServiceLocked 方法去处理服务的启动,关键代码如下:

ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,int callingPid, int callingUid, boolean fgRequired, String callingPackage,final int userId, boolean allowBackgroundActivityStarts)throws TransactionTooLargeException {    ...    //非前台服务,需要检测是否满足后台服务启动条件,不满足则限制启动    if (forcedStandby || (!r.startRequested && !fgRequired)) {        final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,                r.appInfo.targetSdkVersion, callingPid, false, false, forcedStandby);        if (allowed != ActivityManager.APP_START_MODE_NORMAL) { //不满足启动条件            if (allowed == ActivityManager.APP_START_MODE_DELAYED || forceSilentAbort) {                //这里返回 null 代表此场景下静默的限制启动,不通知应用                return null;            }            ...            //这里代表用户应知道此场景下不允许启动,所以返回 ComponentName,明确的通知应用            //注意返回了 "?",是导致应用崩溃的原因            UidRecord uidRec = mAm.mProcessList.getUidRecordLocked(r.appInfo.uid);            return new ComponentName("?", "app is in background uid " + uidRec);        }    }}

至此可以知道关键在于 mAm.getAppStartModeLocked 方法,如果返回 APP_START_MODE_NORMAL 则代表满足启动条件,不会被限制。

mAm 为 ActivityManagerService,继续看 ActivityManagerService 的 getAppStartModeLocked 方法:

int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,int callingPid, boolean alwaysRestrict, boolean disabledOnly, boolean forcedStandby) {    UidRecord uidRec = mProcessList.getUidRecordLocked(uid);    //注意传入的 alwaysRestrict、forcedStandby 都为 false,暂不关注    if (uidRec == null || alwaysRestrict || forcedStandby || uidRec.idle) {        ...        final int startMode = (alwaysRestrict)                ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)                : appServicesRestrictedInBackgroundLocked(uid, packageName,                        packageTargetSdk);        return startMode;    }    return ActivityManager.APP_START_MODE_NORMAL;}

uidRec 为该服务所在应用,uidRec == null 代表应用还未启动,uidRec.idle 代表应用处于后台。应用未启动可以看作处于后台,当然也是不允许启动后台服务的。

继续看 appServicesRestrictedInBackgroundLocked 方法:

int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {    if (mPackageManagerInt.isPackagePersistent(packageName)) {        //系统永久应用不做限制        return ActivityManager.APP_START_MODE_NORMAL;    }    if (uidOnBackgroundWhitelist(uid)) {        //白名单应用不做限制        return ActivityManager.APP_START_MODE_NORMAL;    }    if (isOnDeviceIdleWhitelistLocked(uid, false)) {        //白名单应用不做限制        return ActivityManager.APP_START_MODE_NORMAL;    }    // 其他应用走通用的限制策略    return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);}

普通应用会走通用的限制策略,继续看 appRestrictedInBackgroundLocked 方法:

int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {    if (packageTargetSdk >= Build.VERSION_CODES.O) {        return ActivityManager.APP_START_MODE_DELAYED_RIGID;    }    //tartget API 小于 8.0 的应用走旧的限制策略,众所周知的不会被限制    ...}

可以看到这里对 tartget API 做的限制,8.0 及以上的应用会被限制启动服务,是上层抛出异常的根本原因。

适配 Android 8.0 startService 限制策略

了解了系统的限制原理后,结合上文对 AMS 启动服务限制的源码分析,列举可能的适配方案:

  1. 使用 startForegroundService 代替
  2. 使用 JobScheduler 代替
  3. 设置应用为 Persisten 系统永久应用类型
  4. 将应用加入到系统白名单
  5. 将应用的 targetSdkVersion 调整为小于 Android 8.0 的版本
  6. 启动服务前,先将服务所在应用从后台切换到前台

方案 1 是工作量较小的兼容旧代码方案,但会显示一条通知,这可能不是我们想要的

方案 2 是官方建议方案,兼容工作量比方案 1 多

方案 3 和方案 4 需要系统侧配合,适用于系统或预装应用,对绝大多数的第三方应用来说不可行

方案 5 可行,但极不推荐这种固步自封的方式

方案 6 可行,但不符合谷歌推进此限制策略的意愿,违背提高用户体验的初衷

如何绕过 Android 8.0 startService 限制?

别忘了标题,最终想要实现的是绕过 Android 8.0 startService 的限制,即不修改为前台服务,调用 startService 方法,仍旧可以启动属于后台应用的后台服务,怎么实现呢?

通过上面的方案 6 :「启动服务前,先将服务所在应用从后台切换到前台」 便可实现,如何将应用从后台切换到前台呢?上文介绍了应用被视为处于前台的条件:

  1. 具有可见 Activity
  2. 具有前台 Service
  3. 另一个前台应用已关联到该应用

依据条件 1 可想到一种实现方案:

如果应用处于后台,就启动一个透明的、用户无感知的 Activity,将应用切换到前台,然后再通过 startService 启动服务,随后 finish 掉透明 Activity。

调用端这样 startService :

Intent serviceIntent = new Intent();serviceIntent.setAction("com.ahab.server.service");serviceIntent.setPackage("com.ahab.server");if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {    try{        context.startService(serviceIntent);    }catch (Exception e){        Intent activityIntent = new Intent();        activityIntent.setAction("com.ahab.server.TranslucentActivity");        activityIntent.setPackage("com.ahab.server");        activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);        context.startActivity(activityIntent);    }}else{    context.startService(serviceIntent);}

启动透明 Activity 后 startService:

public class TranslucentActivity extends Activity {

    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        Intent serviceIntent = new Intent();        serviceIntent.setAction("com.ahab.server.service");        serviceIntent.setPackage("com.ahab.server");        context.startService(serviceIntent);        finish();    }}

可以看到代码实现十分简单。

上面都是围绕 startService 启动方式来讲,没有提及 「bindService」 方式,系统并未限制 bindService 启动后台服务,所以通过 bindService 绕过 Android 8.0 startService 的限制,也是可行的。

推荐阅读

LeakCanary 一只优雅的金丝雀

简洁明了的刘海屏适配方案

Android Canvas 绘制小黄人

关注我

助你升职加薪

Android 面试官

原创不易,在看支持!

前台如何正确接收流信息_如何绕过 Android 8.0 startService 限制?相关推荐

  1. honeycomb开发_完整的Android 3.0 Honeycomb SDK发布

    honeycomb开发 完整的Android Honeycomb SDK发行 现已提供适用于Android 3.0'Honeycomb'的完整SDK . API已完成,现在可以在Android Mar ...

  2. app 如何接收遥控信息_如何选购红外接收头?华新告诉你产品的标准

    关注华新,我们将定时为您推送优质产品信息,为您的业务提供更多的选择! 产品组成 我们知道红外遥控一般分为两部分(发射部分以及接收部分),红外接收头属于接收部分,产品的基础组成为: [内部]PD(光敏器 ...

  3. 谷歌云请更正这张卡片的信息_如何在Android的Google键盘上改进自动更正

    谷歌云请更正这张卡片的信息 Autocorrect can be a blessing until it isn't. Once you have an infamous auto-correct f ...

  4. android分屏模式_浅谈 Android 7.0 多窗口分屏模式的实现

    从 Android 7.0 开始,Google 推出了一个名为"多窗口模式"的新功能,也就是我们常说的"分屏模式".那么,这个功能有什么用呢?作为开发者,我们又 ...

  5. 浏览器登录_谷歌浏览器在Android 7.0及以上版本支持使用指纹进行无密码登录

    此前谷歌已经宣布与 FIDO 联盟达成合作关系并在安卓系统上调用指纹或面部识别等来登录某些支持的网站. 在谷歌浏览器最新发布的帮助文档里谷歌介绍称在部分谷歌服务上允许用户使用安卓设备直接解锁无需密码. ...

  6. tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

    内核版本:3.4.39 前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的. 与发送类似,内核也提供了多个接收数据的系统调用接口 ...

  7. Zabbix 使用微信接收报警信息

    1.Zabbix 使用微信接收报警信息 目录[-] 1.配置Zabbix微信报警媒介 2.配置收件人 3.配置Action 4.准备微信报警脚本 5.微信脚本关联企业微信 6.微信告警脚本配置连接微信 ...

  8. python 串口接收数据错误_PySerial无法正确接收数据

    我在通过pySerial正确接收数据时遇到了一个小问题:它通常不会读取完整的数据或读取太多的数据.有时,有时甚至更常见的情况是,发送的数据中存在附加字符或某些字符/部分丢失.看来,PC机和数据发射器没 ...

  9. asp.net1025-物流信息管理系统#毕业设计

    项目编号:asp.net1025-物流信息管理系统#毕业设计 运行环境:VS+SQL 开发工具:VS2010及以上版本 数据库:SQL2008及以上版本 使用技术:HTML+JS+HTML 开发语言: ...

最新文章

  1. 安装python 的包控制mysql的Python脚本与执行系统命令的Python脚本
  2. ubuntu如何修改php文件夹权限,Linux命令chmod:修改文件或文件夹权限
  3. 北京工业计算机考研科目,2020北京工业大学计算机考研初试科目、参考书目、招生人数汇总...
  4. One order search dynamic sql statement生成位置
  5. KVO 的进一步理解
  6. linux的常用操作——用户的添加、删除和查看
  7. 第二届数据科学家大会日程发布,9月20-21日在桂林等您~
  8. 深入学习java虚拟机第二版学习笔记
  9. [Android]-图片JNI(C++\Java)高斯模糊的实现与比较
  10. 在MAC上安装mysql
  11. sql 左连接数据出现重复
  12. 山东大学舆情分析系统项目结题总结
  13. 小孔子内容管理系统V2.0测试
  14. 办公一般用什么邮箱?哪个邮箱好用邮箱排行榜
  15. 如何判断如何判断RS232线是直连还是交叉连线
  16. Apereo CAS 5.0.X 配置数据库认证方式
  17. android manifest相关属性
  18. 牛客 Celestial Resort 质因数分解求最小公倍数 除法取模
  19. Android源码目录结构分析
  20. matlab滤波器滤除低频直流信号,对低频信号的滤波的方法

热门文章

  1. 企业大数据的主要竞争优势
  2. 使用递归函数求解字符串的逆置问题
  3. 服务器网站打开慢跟什么有关系吗,浏览器访问网站的速度很慢,跟服务器的好差有关系吗?跟域名有关系吗?...
  4. java hashcode相等_关于java:hashCode实现,用于“等于某些字段相等”
  5. Spark内核解析之四:Spark 任务调度机制
  6. IQA+不懂︱图像清洗:图像质量评估(评估指标、传统检测方法)
  7. 利用Caffe训练模型(solver、deploy、train_val)+python使用已训练模型
  8. JS学习(this关键字)
  9. centos7下使用rpm包安装clickhouse
  10. CISCO路由器NTP服务器配置