安卓-关于使用startForegroundService启动服务于服务提前终止的思考

news/2025/3/6 5:08:27/

在安卓官方说明中对前台服务的说明是这样的:

从应用启动前台服务分为两步。首先,您必须通过调用 context.startForegroundService() 来启动服务。然后,让该服务调用 ServiceCompat.startForeground() 将自身提升为前台服务。


启动前台服务  |  Background work  |  Android Developers

其中,说明到了要调用ServiceCompat.startForeground() 将自身提升为前台服务

但是在这里并没有对其进行细致说明,如果你直接用,并且没有调用这个函数,大多在几秒钟后就会出现这么一句话:

AndroidRuntime          com.xxx            E  FATAL EXCEPTION: mainProcess: com.xxx, PID: 10040android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{1717d14 u0 com.rizuiyou.tracollctor/.Main.Location.RecordLocationService}at android.app.ActivityThread.generateForegroundServiceDidNotStartInTimeException(ActivityThread.java:2005)at android.app.ActivityThread.throwRemoteServiceException(ActivityThread.java:1979)at android.app.ActivityThread.-$$Nest$mthrowRemoteServiceException(Unknown Source:0)at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2241)at android.os.Handler.dispatchMessage(Handler.java:106)at android.os.Looper.loopOnce(Looper.java:201)at android.os.Looper.loop(Looper.java:288)at android.app.ActivityThread.main(ActivityThread.java:7924)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)Caused by: android.app.StackTrace: Last startServiceCommon() call for this service was made hereat android.app.ContextImpl.startServiceCommon(ContextImpl.java:1915)at android.app.ContextImpl.startForegroundService(ContextImpl.java:1870)at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:822)at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:822)

这东西一出来,它不管三七二十一,就直接把你的整个application给闪退了
我们可以在这个文档https://developer.android.com/about/versions/oreo/background?hl=zh-cn中找到:

在 Android 8.0 之前,创建前台服务的常用方法是创建后台服务,然后将该服务提升到前台。在 Android 8.0 中,存在一个复杂问题:系统不允许后台应用创建后台服务。因此,Android 8.0 引入了新方法 startForegroundService(),用于在前台启动新服务。系统创建服务后,应用有 5 秒钟的时间来调用服务的 [startForeground()](/reference/android/app/Service#startForeground(int, android.app.Notification) 方法,以显示新服务的用户可见通知。如果应用未在时限内调用 startForeground(),系统会停止服务并声明应用存在 ANR。

这里很明确的说明了,如果启动前台服务,就必须在5s内调用那个方法。

到了这里,问题其实很明确了:在安卓8之后,启动前台服务之后,service必须要在5s内调用自身的startForeground方法,否则系统就会强行把程序终止

但是一个新的想法就出现了:我的应用是在后台获取定位的。对于我需要的前台服务,如果说我没有定位相关的服务,我不应该进入到定位的获取逻辑中。

所以一个很想当然的做法就出现了:在init的时候先对应用做权限检查,如果权限检查不通过,我直接调用自身的stopself方法自我终结,那我自身这个服务也就不存在了。也无需调用startForeground了

吗?

实际上,这样操作之后,我很快乐地又闪退了,报错一模一样

所以这里,实验证明了:无论你的service是不是在5s内停止。在是被前台服务调起的情况下都必须要调用startForeground!

那为什么会这么反直觉呢,为此,我又特地去调了安卓的原码:在frameworks/base/services/core/java/com/android/server/am/ActiveServices.java中,有一个serviceForegroundTimeout方法,其中:有这么一个豁免情况引起了我的注意,在这个情况下,不会触发未调用startForeground的强制退出

if (!r.fgRequired || !r.fgWaiting || r.destroying) {return;
}

在这里,只要满足一个条件就不会触发强制退出;我把目光放在了r.destroying这个参数上
按理来说,我stopself之后,r.destroying就应该为true了,为true就不会被抛出错误;

为此,我还特地去怀疑了一下是不是我代码到onDestroy直接是不是大于了5s,特地在init的开始换和destroy的结束搞了一个测试,打印了一下时间,结果一算这时差,才0.2s不到;我真的是低于了计算机的速度了(微笑)

为此 我又特地去找了下r的对象frameworks/base/services/core/java/com/android/server/am/ServiceRecord.java中对destroying定义的定义所在:源码是这样写的:

boolean destroying;     // set when we have started destroying the service

我对destroy进行了查询,发现对它的值进行写入的只有在我们前面提及到过的

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

里面,一个叫做

private void bringDownServiceLocked(ServiceRecord r, boolean enqueueOomAdj) 

的方法;

由于我没有看到源码中对这个函数的说明,只能凭感觉用名字猜测,这是用于停止服务的函数;在这之下无数个嵌套条件的一个地方看到了本calss中唯一的一个对它的赋值。

r.destroying = true;

从结果论的角度,既然我们的serviceForegroundTimeout没有按照预期执行了那个return,那肯定是r.destroying没有被在这个有且只出现了一次的地方赋值

好吧,那只好开始痛苦地看这一长串的判断条件;

对于对我们所期望被执行的赋值,它是包在了一个else语句里面,往上面的if语句爬,爬到了一个叫做if (r.mIsFgsDelegate)的判断条件,这玩意不知道是什么东西,回到ServiceRecord.java里面看定义,是这样写的:

// This is a service record of a FGS delegate (not a service record of a real service)
boolean mIsFgsDelegate;

这里面FGS是我们到此为止闹腾了半天的ForeGroundService的缩写,到此大抵上是破案了。如果我们的服务是前台服务类型,它不会把我们亲爱的r.destroying参数赋值,而是单独定义了段逻辑。

保险起见再往上层的if语句看一下:if (r.app != null)和if (r.app.isThreadReady(),从这名字上就不像是卡在了这些地方;

所以:至此,终于破案了;

我虽然还没有想明白谷歌是为什么不在前台服务的退出逻辑中运行赋值r.destroying,我感觉是它有意为之,特地要求前台服务哪怕是5s内关闭也必须要先建立前台服务,这可能是为了更规范地管理这么一个比较敏感的服务类型吧


http://www.ppmy.cn/news/1576984.html

相关文章

【每日八股】计算机网络篇(二):TCP 和 UDP

目录 TCP 的头部结构?TCP 如何保证可靠传输?1. 确认应答机制2. 超时重传3. 数据排序与去重4. 流量控制5. 拥塞控制6. 校验和 TCP 的三次握手?第一次握手第二次握手第三次握手 TCP 为什么要三次握手?问题一:防止历史连接…

大型语言模型中微调和提炼的详细技术比较

目录 概要 介绍 技术背景 微调和参数高效策略 模型提炼 理念的冲突 QLoRA:将量化与低秩自适应相结合 高级量化:不破坏的缩小艺术 4 位量化为何有效 低阶适配器集成:效率的艺术 低秩适应为何有效 QLoRA 为何如此重要:宏观视角 提炼:机制与训练动态 学生永远无…

【C++设计模式】第四篇:建造者模式(Builder)

注意:复现代码时,确保 VS2022 使用 C17/20 标准以支持现代特性。 分步骤构造复杂对象,实现灵活装配 1. 模式定义与用途 核心目标:将复杂对象的构建过程分离,使得同样的构建步骤可以创建不同的表示形式。 常见场景&am…

Rust Async 并发编程:任务、消息传递与 `join`

1. 创建异步任务 在传统的多线程模型中,我们使用 std::thread::spawn 来创建新的线程。而在 async 模型中,使用 spawn_task 代替 thread::spawn 来创建异步任务,并结合 await 关键字来处理异步操作。 示例:使用 spawn_task 进行…

Python在NFT市场中的应用:从创建到交易的完整指南

Python在NFT市场中的应用:从创建到交易的完整指南 大家好,我是Echo_Wish。今天我们来聊聊一个近年来备受关注的话题——NFT(非同质化代币)。NFT的出现不仅为数字艺术家和收藏家带来了全新的机会,也为开发者提供了一个…

MySQL之 NoneType object has no attribute cursor

查下MySQL报错日志 首先,看下日志文件所在位置 SHOW GLOBAL VARIABLES LIKE log_error;然后查看日志文件中当时的报错信息 发现这样的日志: Aborted connection … to db … Got timeout reading communication packets初步猜测是,数据库…

从零搭建Tomcat:深入理解Java Web服务器的工作原理

Tomcat是Java生态中最常用的Web服务器之一,广泛应用于Java Web应用的部署和运行。本文将带你从零开始搭建一个简易的Tomcat服务器,深入理解其工作原理,并通过代码实现一个基本的Servlet容器。 1. Tomcat的基本概念 Tomcat是一个开源的Servl…

美丽的2024【算法赛】

1.美丽的2024【算法赛】 - 蓝桥云课 问题描述 小蓝刚学习完二进制知识,所以现在他对任何数字的二进制都特别感兴趣。恰好即将迎来2024年,他想知道2024的二进制中有几个1?请你帮忙解决这个问题。 输入格式 本题为填空题,无输入…