Android O 推出出了Background Execution Limits,减少后台应用内存使用及耗电,一个很明显的应用就是不准后台应用通过startService启动服务,这里有两个问题需要弄清楚,第一:什么状态下startService的属于后台启动service;第二:如果想要在后台startService,如何兼容,因此分如下几个问题分析下
- 后台startService的场景
- 后台startService的Crash原理分析
- 如何修改达到兼容
对于普通APP而言,我们不考虑系统的各种白名单,一般后台startService服务分下面两种:
- 通过其他应用startService
- 通过自己应用startService
而每种又可以分不同的小场景,通过其他应用startService已经不被推荐,所以先看看自己应用startService。
本文基于Android P源码
通过自己应用在后台startService限制
可以通过一个简单的实验观察什么情况属于后台startService,注意:如果是自己APP启动Service,那么自身应用必定已经起来了。通过延迟执行就复现该场景。比如:通过click事件,延迟执行一个startService操作,延迟时间是65s(要超过一分钟,后面会看到这是个阈值),然后点击Home键,回到桌面,之后等待一分钟就可复现Crash:
@OnClick(R.id.first) void first() { new Handler().postDelayed(new Runnable() { @Override public void run() { Intent intent = new Intent(LabApplication.getContext(), BackGroundService.class); startService(intent); LogUtils.v("延迟执行"); } },1000*65); }
大概一分多钟之后,延迟消息被执行,然后会有如下Crash日志被打印:
--------- beginning of crash 2019-06-17 19:47:43.148 25916-25916/com.snail.labaffinity E/AndroidRuntime: FATAL EXCEPTION: main Process: com.snail.labaffinity, PID: 25916 java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.snail.labaffinity/.service.BackGroundService }: app is in background uid UidRecord{9048c2c u0a73 LAST bg:+1m4s376ms idle change:idle procs:1 seq(0,0,0)} at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1577) at android.app.ContextImpl.startService(ContextImpl.java:1532) at android.content.ContextWrapper.startService(ContextWrapper.java:664) at com.snail.labaffinity.activity.MainActivity$2.run(MainActivity.java:41) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6669) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
就会看到很经典的startService限制信息:
Not allowed to start service Intent XXX : app is in background uid UidRecord
也就是说,当前退到后台的APP已经属于后台应用,不能通过startService启动服务。Why?跟踪源码看下 startService会调用ContextImpl 的startServiceCommon,进而通过Binder调用AMS启动Service,根据返回值选择性抛出IllegalStateException异常:
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) { <!--返回值是?的情况下就是后台启动service的异常--> if (cn.getPackageName().equals("?")) { throw new IllegalStateException( "Not allowed to start service " + service + ": " + cn.getClassName()); } }
什么时候ActivityManager.getService().startService的返回值包名 ?,核心代码在AMS端,AMS进一步调用ActiveServices.java的startServiceLocked:
ActiveServices.java
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType, int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId) throws TransactionTooLargeException { final boolean callerFg; if (caller != null) { final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller); ... callerFg = callerApp.setSchedGroup != ProcessList.SCHED_GROUP_BACKGROUND; } else { callerFg = true; } ServiceLookupResult res = retrieveServiceLocked(service, resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg, false, false); ... ServiceRecord r = res.record; // If we're starting indirectly (e.g. from PendingIntent), figure out whether // we're launching into an app in a background state. This keys off of the same // idleness state tracking as e.g. O+ background service start policy. <!--通过PendingIntent启动的也要检查--> // 是否当前Uid Active 不过不是activity就是后台启动 final boolean bgLaunch = !mAm.isUidActiveLocked(r.appInfo.uid); // If the app has strict background restrictions, we treat any bg service // start analogously to the legacy-app forced-restrictions case, regardless // of its target SDK version. boolean forcedStandby = false; <!--appRestrictedAnyInBackground 一般人不会主动设置,所以这个经常是返回false--> if (bgLaunch && appRestrictedAnyInBackground(r.appInfo.uid, r.packageName)) { ... forcedStandby = true; } <!--forcedStandby可以先无视 这里注意两点,第一点 :r.startRequested标志是通过startService调用启动过,第一次进来的时候是false,第二:对于普通是starServicefgRequired是false--> if (forcedStandby || (!r.startRequested && !fgRequired)) { <!--检测当前app是否允许后台启动--> final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName, r.appInfo.targetSdkVersion, callingPid, false, false, forcedStandby); <!--如果不允许 Background start not allowed--> if (allowed != ActivityManager.APP_START_MODE_NORMAL) { ... <!--返回 ? 告诉客户端现在处于后台启动状态,禁止你--> return new ComponentName("?", "app is in background uid " + uidRec); } }
假设我们是第一次startService,那么(!r.startRequested && !fgRequired)就等于true,进而走进mAm.getAppStartModeLocked,看看当前进程是否处于后台非激活状态,如果是的话 ,就不会允许startService:
ActivityManagerService.java
int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk, int callingPid, boolean alwaysRestrict, boolean disabledOnly, boolean forcedStandby) { UidRecord uidRec = mActiveUids.get(uid); <!--UidRecord是关键 alwaysRestrict || forcedStandby 传入的都是false,忽略 --> if (uidRec == null || alwaysRestrict || forcedStandby || uidRec.idle) { boolean ephemeral; ... final int startMode = (alwaysRestrict) ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk) : appServicesRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); ... return startMode; } return ActivityManager.APP_START_MODE_NORMAL; }
这里UidRecord是关键,UidRecord为null,则说明整个APP没有被启动,那么就一定属于后台启动Service,如果UidRecord非null,则要判断应用是否属于后台应用,而这个关键就是uidRec.idle,如果idle是true,就说明应用处于后台状态,继续调用 appServicesRestrictedInBackgroundLocked看看是否是O以后的,走Crash逻辑:
int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { <!--永久进程 --> // Persistent app? if (mPackageManagerInt.isPackagePersistent(packageName)) { return ActivityManager.APP_START_MODE_NORMAL; } <!--白名单--> // Non-persistent but background whitelisted? if (uidOnBackgroundWhitelist(uid)) { return ActivityManager.APP_START_MODE_NORMAL; } <!--白名单--> // Is this app on the battery whitelist? if (isOnDeviceIdleWhitelistLocked(uid, /*allowExceptIdleToo=*/ false)) { return ActivityManager.APP_START_MODE_NORMAL; } // 普通进程 return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); }
对于普通进程看看O限制
int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) { <!--对于targetSDKVersion>O 的直接 返回ActivityManager.APP_START_MODE_DELAYED_RIGID--> if (packageTargetSdk >= Build.VERSION_CODES.O) { return ActivityManager.APP_START_MODE_DELAYED_RIGID; } // 否则仅仅对老版本做兼容性限制 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: // If force-background-check is enabled, restrict all apps that aren't whitelisted. if (mForceBackgroundCheck && !UserHandle.isCore(uid) && !isOnDeviceIdleWhitelistLocked(uid, /*allowExceptIdleToo=*/ true)) { return ActivityManager.APP_START_MODE_DELAYED; } ... }
appServicesRestrictedInBackgroundLocked仅仅是根据是否是O以后,返回ActivityManager.APP_START_MODE_DELAYED_RIGID,只是兼容,核心还是UidRecord的idle,下面就重点看看UidRecord跟其idle的值,这个值是应用是否位于后台的核心指标,应用未启动的不考虑,未启动肯定也属于”后台“的一种极端。
不是特别老的Android版本都不允许没有LAUNCHER Activity的应用,不然压根没法编译运行,也就说普通场景通过桌面启动应用的时候,都是通过startActivity直接启动APP的,在启动App的时候,UidRecord会被新建(AMS端),UidRecord构造函数中默认 idle = true。
public UidRecord(int _uid) { uid = _uid; idle = true; reset(); }
其启动流程调用堆栈如下:
也就是启动APP时候,刚好一开UidRecord中idle的值是true,被看做后台应用,那么一定有某个地方设置为false,设置为前台应用。
前后台应用切换时机与原理
一个应用可以有一个或者多个进程,当任何一个进程变为被转换成前台可见进程的时候,APP都会被认作前台应用(对于startService应用而言),resumetopActivity是一个非常明确的切换时机,
会调用
final void scheduleIdleLocked() { mHandler.sendEmptyMessage(IDLE_NOW_MSG); }
会通过updateOomAdjLocked修改当前即将可见Activity应用的idle状态,updateOomAdjLocked在期间可能会被调用多次,
@GuardedBy("this") final void updateOomAdjLocked() { ... for (int i=mActiveUids.size()-1; i>=0; i--) { final UidRecord uidRec = mActiveUids.valueAt(i); int uidChange = UidRecord.CHANGE_PROCSTATE; if (uidRec.curProcState != ActivityManager.PROCESS_STATE_NONEXISTENT && (uidRec.setProcState != uidRec.curProcState || uidRec.setWhitelist != uidRec.curWhitelist)) { ... if (ActivityManager.isProcStateBackground(uidRec.curProcState) && !uidRec.curWhitelist) { ... } else { <!--设置为false 标记为前台进程--> if (uidRec.idle) { uidChange = UidRecord.CHANGE_ACTIVE; EventLogTags.writeAmUidActive(uidRec.uid); uidRec.idle = false; } <!--清零后台进程锚点时间--> uidRec.lastBackgroundTime = 0; }
对于即将可见的APP而言 ActivityManager.isProcStateBackground为false,所以走else逻辑设置uidRec.idle = false,uidChange = UidRecord.CHANGE_ACTIVE,之后通过enqueueUidChangeLocked最后设置相对应的idle = false。相对应,上一个被切换走的应用可能会触发设置idle = true的操作,不过设置为true的操作不是即可执行的,而是延迟执行的,延迟时间60s:
final void updateOomAdjLocked() { ... for (int i=mActiveUids.size()-1; i>=0; i--) { final UidRecord uidRec = mActiveUids.valueAt(i); int uidChange = UidRecord.CHANGE_PROCSTATE; if (uidRec.curProcState != ActivityManager.PROCESS_STATE_NONEXISTENT && (uidRec.setProcState != uidRec.curProcState || uidRec.setWhitelist != uidRec.curWhitelist)) { if (ActivityManager.isProcStateBackground(uidRec.curProcState) && !uidRec.curWhitelist) { // UID is now in the background (and not on the temp whitelist). Was it // previously in the foreground (or on the temp whitelist)? if (!ActivityManager.isProcStateBackground(uidRec.setProcState) || uidRec.setWhitelist) { <!--切换后台时候更新lastBackgroundTime--> uidRec.lastBackgroundTime = nowElapsed; if (!mHandler.hasMessages(IDLE_UIDS_MSG)) { <!--60s后更新--> mHandler.sendEmptyMessageDelayed(IDLE_UIDS_MSG, mConstants.BACKGROUND_SETTLE_TIME); } }
延迟60s是为了防止60s之内多次切换APP导致的重复更新,系统只要保证60s内有一次就可以了。
private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;
60s之后调用idleUids更新idle字段
final void idleUids() { synchronized (this) { final int N = mActiveUids.size(); if (N <= 0) { return; } final long nowElapsed = SystemClock.elapsedRealtime(); final long maxBgTime = nowElapsed - mConstants.BACKGROUND_SETTLE_TIME; long nextTime = 0; for (int i=N-1; i>=0; i--) { final UidRecord uidRec = mActiveUids.valueAt(i); <!--刚才切后台的时候已经更新过uidRec.lastBackgroundTime--> final long bgTime = uidRec.lastBackgroundTime; if (bgTime > 0 && !uidRec.idle) { <!--标准:后台存在时间超过mConstants.BACKGROUND_SETTLE_TIME--> if (bgTime <= maxBgTime) { uidRec.idle = true; uidRec.setIdle = true; doStopUidLocked(uidRec.uid, uidRec); } else { 如果被提前执行了,则在下一个60s到达的时候执行 if (nextTime == 0 || nextTime > bgTime) { nextTime = bgTime; } } } } if (nextTime > 0) { mHandler.removeMessages(IDLE_UIDS_MSG); mHandler.sendEmptyMessageDelayed(IDLE_UIDS_MSG, nextTime + mConstants.BACKGROUND_SETTLE_TIME - nowElapsed); } } }
之前是前台,现在变后台,那么uidRec.lastBackgroundTime = nowElapsed赋值,再次切前台,uidRec.lastBackgroundTime清零,简而言之, 应用变为前台,UID状态马上变更为active状态,应用变为后台,即procState大于等于PROCESS_STATE_TRANSIENT_BACKGROUND时,如果持续在后台60s后,UID状态会变更为idle=true状态,不能startService;
通过其他应用startService的情况
跨应用startService已经不被推荐了,不过也容易模拟,在A应用中通过setAction+setPackage就可以startService:
var intent = Intent(); intent.setAction("com.snail.BackGroundService"); intent.setPackage("com.snail.labaffinity"); startService(intent)
当然在B应用中AndroidManifest要暴露出来:
<service android:name=".service.BackGroundService" <!--是否独立进程,无关紧要--> android:process=":service" android:exported="true"> <intent-filter> <action android:name="com.snail.BackGroundService" /> </intent-filter> </service>
这样A中startService同样要遵守不准后台启动的条件。比如如果B没启动过,直接在A中startService,则会Crash,如果B启动了,还没变成后台应用(退到后台没超过60S),则不会Crash。个人觉得通过adb命令startService也属于这种范畴,通过如下命令可以达到相同的效果。
am startservice -n com.snail.labaffinity/com.snail.labaffinity.service.BackGroundService
如果APP没有启动就会看到如下日志:
app is in background uid null
如果启动了,但是属于后台应用,就会看到如下日志,跟自己APP后台启动Service类似:
Not allowed to start service Intent { cmp=com.snail.labaffinity/.service.BackGroundService }: app is in background uid UidRecord{72bb30d u0a238 SVC idle change:idle|uncached procs:1 seq(0,0,0)}
其实,startService不是看调用的APP处于何种状态,而是看Servic所在APP处于何种状态,因为看的是Servic所处的UidRecord的状态,UidRecord仅仅跟APP安装有关系,跟进程pid没关系。
特殊场景:进程通过Service恢复的场景
先看下如下代码,APP在启动的时候,在Application的onCreate中通过startService启动了一个服务,并且没有stop,这种场景下第一次通过Launcher冷启动没问题,如果我们在后台杀死APP,由于存在一个未stop的服务,系统会重新拉起该服务,也就是会重启一个进程,然后启动服务。
public class LabApplication extends Application { @Override public void onCreate() { super.onCreate(); Intent intent = new Intent( this, BackGroundService.class); startService(intent); } } public class BackGroundService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtils.v("onStartCommand"); } }
在这个过程中,应用重启会复现如下Crash(禁止后台启动Service的Crash Log):
java.lang.RuntimeException: Unable to create application com.snail.labaffinity.app.LabApplication: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.snail.labaffinity/.service.BackGroundService }: app is in background uid UidRecord{72bb30d u0a238 SVC idle change:idle|uncached procs:1 seq(0,0,0)} at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5925) at android.app.ActivityThread.access$1100(ActivityThread.java:200) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1656) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6718) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Why?为什么冷启动没问题,后台杀死自启动恢复就有问题,看日志是因为当app is in background,Not allowed to start service,也就是后台进程不能通过startService启动服务,在LabApplication的onCreate中我们确实主动startService(intent),这个就是crash的原因,那为什么第一次没问题?在前文我们知道,通过Laucher启动应用是通过startActivity启动的,也就是存在一个resumeTopActivity的时机,在这个时机,APP的idle会被设置为false,也就是非后台应用,但是对于后台杀死又恢复的场景,他不是通过startActivity启动的,所以APP就算重启了,APP的idle还是true,是非激活的状态,也就是属于后台应用,不准通过startService启动服务(假设单进程)。
因为第一次冷启动时候,走正常启动Activity流程,新建进程,然后去AMS attachApplication,
@GuardedBy("this") private final boolean attachApplicationLocked(IApplicationThread thread, int pid, int callingUid, long startSeq) { ... <!-- 通知APP端创建Application--> thread.bindApplication(processName, appInfo, providers, app.instr.mClass, profilerInfo, app.instr.mArguments,...); ... boolean badApp = false; boolean didSomething = false; // See if the top visible activity is waiting to run in this process... if (normalMode) { try { <!-- 需要启动的Activity 关键点 --> if (mStackSupervisor.attachApplicationLocked(app)) { didSomething = true; } if (!badApp) { try { <!-- 需要恢复的Service--> didSomething |= mServices.attachApplicationLocked(app, processName); checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); }
第一次启动APP的时候,thread.bindApplication首先通知APP端启动Application,并执行onCreate,不过onCreate中的startService要等待AMS端上一个消息执行完毕(Handler保证),这个过程中mStackSupervisor.attachApplicationLocked(app)中会调用realStartActivityLocked启动Activity,先将UidRecord的idle给更新为false,attachApplicationLocked执行之后,才有可能轮到下一个消息startService执行,这个时候APP已经不是后台应用了,所以不会Crash。
boolean attachApplicationLocked(ProcessRecord app) throws RemoteException { final String processName = app.processName; boolean didSomething = false; ... final int size = mTmpActivityList.size(); <!--存在要启动的Activity--> for (int i = 0; i < size; i++) { final ActivityRecord activity = mTmpActivityList.get(i); if (activity.app == null && app.uid == activity.info.applicationInfo.uid && processName.equals(activity.processName)) { try { <!--走realStartActivityLocked--> if (realStartActivityLocked(activity, app, top == activity /* andResume */, true /* checkConfig */))
realStartActivityLocked会更新oom,并设置idle为false,因为有Activity要启动,就不在是后台进程,调用流程如下:
但是对于而对于杀死并通过Service恢复的进程,没有明确的startActivity,所以size = mTmpActivityList.size()这里size是0,不会走realStartActivityLocked,也就在进程恢复阶段,不会将APP归为前台应用,这个时候再AMS执行下一个消息启动Service的时候,就会告诉APP端,不能在后台启动应用。
如何解决这个问题
既然不能再后台偷偷启动,那只能显示启动,Google提供的方案是:
startForegroundService()。并且在系统创建Service后,需要在一定时间内调用startForeground()让Service为用户可见通知,否则则系统将停止此Service,抛出ANR,如果不像让用户可见可以参考JobScheduler。不过本篇只看startForegroundService:
@Override public ComponentName startService(Intent service) { warnIfCallingFromSystemProcess(); return startServiceCommon(service, false, mUser); } @Override public ComponentName startForegroundService(Intent service) { warnIfCallingFromSystemProcess(); return startServiceCommon(service, true, mUser); }
同普通startService的区别那就是startServiceCommon的第二参数boolean requireForeground 是true:
ComponentName startServiceLocked(IApplicationThread caller, Intent service ...} <!--fgRequired为true,不会检测启动后台限制--> 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) { return new ComponentName("?", "app is in background uid " + uidRec); } } ... <!--ServiceRecord赋值r.fgRequired 后面会用到--> r.fgRequired = fgRequired; <!--添加后面回调StartItem--> r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(), service, neededGrants, callingUid));
在AMS端startForegroundService跟普通startService区别, ServiceRecord的fgRequired被设置为true,然后走后续流程bringUpServiceLocked->realStartServiceLocked-> sendServiceArgsLocked,在sendServiceArgsLocked的时候,Service其实已经创建并启动(可以看Service启动流程),
private final void sendServiceArgsLocked(ServiceRecord r, boolean execInFg, boolean oomAdjusted) throws TransactionTooLargeException { ... ArrayList<ServiceStartArgs> args = new ArrayList<>(); while (r.pendingStarts.size() > 0) { ServiceRecord.StartItem si = r.pendingStarts.remove(0); ... if (r.fgRequired && !r.fgWaiting) { if (!r.isForeground) { <!--监听是否5S内startForeground--> scheduleServiceForegroundTransitionTimeoutLocked(r); } ... try { r.app.thread.scheduleServiceArgs(r, slice); }
可以看到对于要求前台启动的Service fgRequired = true,并且第一次r.fgWaiting=false,所以会走scheduleServiceForegroundTransitionTimeoutLocked,
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); }
r.fgWaiting会被设置为true,scheduleServiceForegroundTransitionTimeoutLocked过一次后,就不会再次走。
static final int SERVICE_START_FOREGROUND_TIMEOUT = 10*1000;
看9.0代码,是10s完成调用startForeground,否则在10s后Handler处理这一消息的时候,会停止该服务,并抛出Service的ANR异常。
void serviceForegroundTimeout(ServiceRecord r) { ProcessRecord app; synchronized (mAm) { if (!r.fgRequired || r.destroying) { return; } app = r.app; r.fgWaiting = false; stopServiceLocked(r); } if (app != null) { mAm.mAppErrors.appNotResponding(app, null, null, false, "Context.startForegroundService() did not then call Service.startForeground(): " + r); } }
抛出异常栈如下
--------- beginning of crash E/AndroidRuntime: FATAL EXCEPTION: main Process: com.snail.labaffinity, PID: 21513 android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground() at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1768) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6494) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
解决方案就是及时调用startForeground,对于O以后的还要注意Notification需要一个ChannelID
public class BackGroundService extends Service { @Override public void onCreate() { super.onCreate(); startForeground(); } private void startForeground() { String CHANNEL_ONE_ID = "com.snail.labaffinity"; String CHANNEL_ONE_NAME = "Channel One"; NotificationChannel notificationChannel = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { notificationChannel = new NotificationChannel(CHANNEL_ONE_ID, CHANNEL_ONE_NAME, NotificationManager.IMPORTANCE_DEFAULT); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); assert manager != null; manager.createNotificationChannel(notificationChannel); startForeground(1, new NotificationCompat.Builder(this, CHANNEL_ONE_ID).build()); } } }
startForeground主要就是讲Service至于前台可见,同时取消掉刚才的那个延时Message,这样就不会检测并抛出异常了。
private void setServiceForegroundInnerLocked(final ServiceRecord r, int id, Notification notification, int flags) { <!--id不能为0--> if (id != 0) { ... if (r.fgRequired) { <!--设置fgRequired = false--> r.fgRequired = false; <!--设置 fgWaiting = false--> r.fgWaiting = false; alreadyStartedOp = true; <!--移除ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG消息--> mAm.mHandler.removeMessages( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r); }
不过不过这样的话,状态栏会有一个xxx正在运行的通知,体验不太好,如果是要完成某项任务完成后,最好主动stop掉。还有一个要注意的问题:在调用startForGround前不准调stop,否则也会抛出异常:
private final void bringDownServiceLocked(ServiceRecord r) { ... if (r.fgRequired) { r.fgRequired = false; r.fgWaiting = false; mAm.mAppOpsService.finishOperation(AppOpsManager.getToken(mAm.mAppOpsService), AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName); mAm.mHandler.removeMessages( ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r); if (r.app != null) { Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG); msg.obj = r.app; msg.getData().putCharSequence( ActivityManagerService.SERVICE_RECORD_KEY, r.toString()); mAm.mHandler.sendMessage(msg); } }
如果调用了startForegroundService,但是没有调用startForGround,此时调用stopService时,r.fgRequired = true,那么bringDownServiceLocked就会直接移除ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG消息,并抛出ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG异常,其实只要在onCreate中startForeground就行了。
总结
- startService抛异常不是看调用的APP处于何种状态,而是看Servic所在APP处于何种状态,因为看的是UID的状态,所以这里重要的是APP而不仅仅是进程状态
- 不要通过Handler延迟太久再startService,否则可能会有问题
- 应用进入后台,60s之后就会变成idle状态,无法start其中的Service,但是可以通过startForegroundService来启动
- Application里面不要startService,否则恢复的时候可能有问题
- startForGround 要及时配合startForegroundService,否则会有各种异常。