Android O 後臺startService限制淺析

Android O 推出出了Background Execution Limits,減小後臺應用內存使用及耗電,一個很明顯的應用就是不許後臺應用經過startService啓動服務,這裏有兩個問題須要弄清楚,第一:什麼狀態下startService的屬於後臺啓動service;第二:若是想要在後臺startService,如何兼容,所以分以下幾個問題分析下java

  • 後臺startService的場景
  • 後臺startService的Crash原理分析
  • 如何修改達到兼容

對於普通APP而言,咱們不考慮系統的各類白名單,通常後臺startService服務分下面兩種:android

  • 經過其餘應用startService
  • 經過本身應用startService

而每種又能夠分不一樣的小場景,經過其餘應用startService已經不被推薦,因此先看看本身應用startService。app

本文基於Android P源碼less

經過本身應用在後臺startService限制

能夠經過一個簡單的實驗觀察什麼狀況屬於後臺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();
}
複製代碼

其啓動流程調用堆棧以下:

image.png

也就是啓動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要啓動,就不在是後臺進程,調用流程以下:

image.png

可是對於而對於殺死並經過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,不然會有各類異常。
相關文章
相關標籤/搜索