Android O 推出出了Background Execution Limits,減小後臺應用內存使用及耗電,一個很明顯的應用就是不許後臺應用經過startService啓動服務,這裏有兩個問題須要弄清楚,第一:什麼狀態下startService的屬於後臺啓動service;第二:若是想要在後臺startService,如何兼容,所以分以下幾個問題分析下java
對於普通APP而言,咱們不考慮系統的各類白名單,通常後臺startService服務分下面兩種:android
而每種又能夠分不一樣的小場景,經過其餘應用startService已經不被推薦,因此先看看本身應用startService。app
本文基於Android P源碼less
能夠經過一個簡單的實驗觀察什麼狀況屬於後臺startService,注意:若是是本身APP啓動Service,那麼自身應用一定已經起來了。經過延遲執行就復現該場景。好比:經過click事件,延遲執行一個startService操做,延遲時間是65s(要超過一分鐘,後面會看到這是個閾值),而後點擊Home鍵,回到桌面,以後等待一分鐘就可復現Crash:ide
@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限制信息:oop
Not allowed to start service Intent XXX : app is in background uid UidRecord
複製代碼
也就是說,當前退到後臺的APP已經屬於後臺應用,不能經過startService啓動服務。Why?跟蹤源碼看下 startService會調用ContextImpl 的startServiceCommon,進而經過Binder調用AMS啓動Service,根據返回值選擇性拋出IllegalStateException異常:post
ContextImpl.javaui
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:this
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已經不被推薦了,不過也容易模擬,在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不要緊。
先看下以下代碼,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就好了。