在安卓官方说明中对前台服务的说明是这样的:
从应用启动前台服务分为两步。首先,您必须通过调用 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内关闭也必须要先建立前台服务,这可能是为了更规范地管理这么一个比较敏感的服务类型吧