Android O_GO後臺啓動服務改動

Android O_GO後臺啓動服務改動

1. 問題現象

應用在適配Android O/GO的系統時,會發現後臺啓動不了服務,會報出如下異常,並強退:

java.lang.RuntimeException: 
Caused by: java.lang.IllegalStateException: Not allowed to start service Intent 
{ cmp=com.android.test/com.android.test.TestService (has extras) }: 
app is in background uid UidRecord{255693 u0a26 RCVR idle procs:2 seq(0,0,0)}
         at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1506)
         at android.app.ContextImpl.startService(ContextImpl.java:1462)
         at android.content.ContextWrapper.startService(ContextWrapper.java:648)
         at android.content.ContextWrapper.startService(ContextWrapper.java:648)

異常狀態是:”IllegalStateException”非法狀態,
內容是:”Not allowed to start service”不允許啓動服務,
原因是:”app is in background”應用在後臺運行

爲什麼google會搞出這東西呢:
=> google對於app的權限釋放得太多了,所以android手機卡頓、耗電快的問題一直都困擾着用戶,
特別是國內很多流氓的apk,不僅自己佔資源,還拉別的應用一起來。
google無奈,只能收回權限,目前收回了很多後臺運行進程的權限(啓動服務、接收廣播等),如此處限制了後臺啓動服務。
這樣做對於android系統性能、功耗確實會有提升,但是道高一尺魔高一丈,我們總是有辦法的。

2. 問題原因分析

需要先找出問題的原因再討論修復方案。

2.1 出錯代碼定位

在ContextImpl的startServiceCommon函數中爆出異常,
//frameworks/base/core/java/android/app/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) {
                //...
                //此處就是曝出異常的地方,非法狀態,不允許啓動服務
                } else if (cn.getPackageName().equals("?")) {
                    throw new IllegalStateException(
                            "Not allowed to start service " + service + ": " + cn.getClassName());
                }
            }
            return cn;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

startServiceCommon這個函數做的操作是AMS的startService,用於啓動服務.

2.2 AMS的startService

接下去看AMS的startService,稍微注意一下傳遞的參數,裏面有一個前臺後臺相關的requireForeground,可能跟問題有關係。
AMS代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    public ComponentName startService(IApplicationThread caller, Intent service,
            String resolvedType, boolean requireForeground, String callingPackage, int userId)
            throws TransactionTooLargeException {
        //...
        //調用ActiveServices的startServiceLocked
        res = mServices.startServiceLocked(caller, service,
                resolvedType, callingPid, callingUid,
                requireForeground, callingPackage, userId);
        //...
    }

其最終會調用ActiveServices的startServiceLocked

2.3 ActiveServices的startServiceLocked

ActiveServices這裏的代碼很多都是很關鍵的,如果對Android組件Service感興趣的最好把這個文件研究一下。
ActiveServices代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
            int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
            throws TransactionTooLargeException {
        //...
        // 啓動服務之前有2個判斷一個是startRequested,一個是fgRequired。
        // startRequested代表的是:是否已經啓動過服務,一般出現問題都是啓動一個沒有運行的服務,
        // 那麼這個就是false。
        // fgRequired這個就是啓動服務傳遞的requireForeground,
        // 可以查看2.1章節的startServiceCommon函數。
        if (!r.startRequested && !fgRequired) {
            // 這裏面有個關鍵函數getAppStartModeLocked,判斷是否運行啓動服務
            // 注意此處傳遞的最後2個參數:alwaysRestrict和disabledOnly都是false
            final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
                    r.appInfo.targetSdkVersion, callingPid, false, false);
            // 如果不允許啓動服務則會運行到裏面
            if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
                //...
                UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
                // 此處就是不允許運行服務返回的原因"app is in background"
                // 和章節1中的問題現象的原因是一致的
                return new ComponentName("?", "app is in background uid " + uidRec);
            }
        }
        //...
    }

這裏面由於出現錯誤(問題現象具體可以查看:章節1),那麼startRequested==false而且fgRequired==false,說明這個服務是第一次啓動,而且是後臺請求啓動服務。
至於爲什麼不允許啓動服務,我們還需要查看AMS的getAppStartModeLocked函數。

2.4 AMS判斷並返回服務啓動模式

AMS代碼位置
//frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

1、getAppStartModeLocked返回值就是啓動模式,其中此處傳遞的alwaysRestrict==false

    int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
            int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
        UidRecord uidRec = mActiveUids.get(uid);
        //...
        // 此處alwaysRestrict==false,於是調用的是appServicesRestrictedInBackgroundLocked
        final int startMode = (alwaysRestrict)
                ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
                : appServicesRestrictedInBackgroundLocked(uid, packageName,
                        packageTargetSdk);
        //...
        return startMode;
        //...
    }

根據alwaysRestrict的值會調用appRestrictedInBackgroundLocked或者appServicesRestrictedInBackgroundLocked;
其中appRestrictedInBackgroundLocked是直接根據應用sdk進行判斷,
appServicesRestrictedInBackgroundLocked會進行條件過濾,直接運行部分應用啓動服務,其它的進行應用sdk的判斷。

2、appServicesRestrictedInBackgroundLocked進行條件過濾,允許部分啓動服務

    int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
        // 如果是常駐內存的,可以直接啓動服務
        if (mPackageManagerInt.isPackagePersistent(packageName)) {
            //...
            return ActivityManager.APP_START_MODE_NORMAL;
        }

        // 如果是非常駐內存的話,但是在白名單列表裏面的uid也是允許的
        // 目前這個白名單裏面就只有一個:藍牙BLUETOOTH_UID = 1002
        if (uidOnBackgroundWhitelist(uid)) {
            //...
            return ActivityManager.APP_START_MODE_NORMAL;
        }

        // 如果是在電源相關的白名單裏面,也是允許啓動服務的
        if (isOnDeviceIdleWhitelistLocked(uid)) {
            //...
            return ActivityManager.APP_START_MODE_NORMAL;
        }

        // 默認的策略是appRestrictedInBackgroundLocked
        return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
    }

分別對於:
1) 是否常駐內存應用,常駐內存允許啓動服務
2) 如果是藍牙也是允許啓動服務
3) 是在電源相關的DeviceIdle白名單裏面,允許啓動服務的
4) 如果都不是則執行默認策略appRestrictedInBackgroundLocked

3、appRestrictedInBackgroundLocked默認限制策略

    int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
        // 如果apk的sdk版本大於AndroidO的話,那麼默認是不允許啓動服務的
        if (packageTargetSdk >= Build.VERSION_CODES.O) {
            //...
            return ActivityManager.APP_START_MODE_DELAYED_RIGID;
        }
        // 如果是之前版本的apk,會查看AppOps是否允許後臺運行權限,
        // 由於我們sdk版本肯定會升級的,這個就暫時不考慮了
        int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND,
                uid, packageName);
        //...
        switch (appop) {
            case AppOpsManager.MODE_ALLOWED:
                return ActivityManager.APP_START_MODE_NORMAL;
            case AppOpsManager.MODE_IGNORED:
                return ActivityManager.APP_START_MODE_DELAYED;
            default:
                return ActivityManager.APP_START_MODE_DELAYED_RIGID;
        }
    }

如果apk的sdk版本大於AndroidO的話,那麼默認是不允許啓動服務的,那麼要適配Android O/GO以後的版本,此處是繞不過去的坎,建議儘早處理。

3. 修改方案

有上面可知,問題原因主要是後臺啓動了服務,在這部分Android O/GO做了限制,
根據章節2.4的過濾條件可以提供如下修改方案:
1) 提升應用優先級到常駐內存級別 (不建議應用採納這種方式,會導致手機出現很多性能問題)
=> 在AndroidManifest.xml添加android:persistent=”true”
並且簽上系統簽名
2) 類似與藍牙BLUETOOTH_UID一樣放在白名單裏面(需要擁有源碼修改權限,而且修改了源碼,不利於apk的版本兼容,不建議採納)
3) 添加在電源相關的DeviceIdle白名單(不建議添加,可能導致功耗增加)

按照上面的都說是不建議採取,是否沒有辦法了呢?

我們繼續往源頭找找看看是否有辦法:
在章節2.1、章節2.3有一個參數requireForeground/fgRequired,是否前臺請求,如果requireForeground/fgRequired爲false纔會進行後臺請求判斷,如果是true的話,是可以直接繞過去的

回到ActiveServices的startServiceLocked=>

    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
            int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
            throws TransactionTooLargeException {
        //...
        // fgRequired是true可以直接繞過
        if (!r.startRequested && !fgRequired) {
            //..
        }
        //...
    }

那麼方案4,我們可以採取如下方式
4) 通過Context(activity、service的this都是包含context的,故不用擔心調用方式),將之前的startService,修改成ContextImpl的startForegroundService或者startForegroundServiceAsUser方法,啓動一個前臺服務。
//frameworks/base/core/java/android/app/ContextImpl.java

    @Override
    public ComponentName startForegroundService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, true, mUser);
    }

    @Override
    public ComponentName startForegroundServiceAsUser(Intent service, UserHandle user) {
        return startServiceCommon(service, true, user);
    }

好了,那麼上述的第4種方法可以很好解決該問題。

ps:注意上面的方法是啓動前臺服務,你的服務需要是前臺的,這個怎麼做呢,下面提供2種方法:
1) 在service中調用startForeground (最常見方法)
2) 設置service爲前臺,可以使用AMS的setProcessImportant設置優先級別 (優點是:不會在通知欄中出現通知圖標。缺點是:需要相應的權限)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章