Service核心:探讨8.0版本以上,后台Service保活机制

问题背景

升级应用程序 targetsdkversion 到8.0 后,发现下面这个问题:

在不升级前,APP退出,后台Service可以存活很长一段时间,但是在升级后,8.0以下版本手机还是可以存活一段很长时间,但是在8.0以上版本手机,app 在退出一分钟,后台Service就被杀死了。

杀死后有这么一条日志:

     Stopping service due to app idle: u0a309 -1m19s437ms xxx.xxx.xxx/xxx.xxxx.xxxx

原因分析

1: 拿到这个日志,当时的想法是,service 被stopping , 原因是:app 处于空闲了,结合Android中 Service是通过Ams(ActivityManagerService)管理的,下面我们来分析下AMS

2: 一顿分析下来:在AMS中有个函数 updateOomAdjLocked, 该函数有一个 sendMessage操作,代码如下 :可以看到如果 应用程序处于后台,且不在白名单中,会延迟发送一个消息,这个延时消息与常量 BACKGROUND_SETTLE_TIME有关,它就是Service在后台存活的时间,并且默认情况下是 60s

// 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) {
    uidRec.lastBackgroundTime = nowElapsed;
    if (!mHandler.hasMessages(IDLE_UIDS_MSG)) {
        // Note: the background settle time is in elapsed realtime, while
        // the handler time base is uptime.  All this means is that we may
        // stop background uids later than we had intended, but that only
        // happens because the device was sleeping so we are okay anyway.
        mHandler.sendEmptyMessageDelayed(IDLE_UIDS_MSG,
                mConstants.BACKGROUND_SETTLE_TIME);
    }
}

public long BACKGROUND_SETTLE_TIME = DEFAULT_BACKGROUND_SETTLE_TIME;

// 存活时间常量
private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;


// 更新存活时间常量
private void updateConstants() {
    final String setting = Settings.Global.getString(mResolver,
            Settings.Global.ACTIVITY_MANAGER_CONSTANTS);
    synchronized (mService) {
        try {
            mParser.setString(setting);
        } catch (IllegalArgumentException e) {
            // Failed to parse the settings string, log this and move on
            // with defaults.
            Slog.e("ActivityManagerConstants", "Bad activity manager config settings", e);
        }
        MAX_CACHED_PROCESSES = mParser.getInt(KEY_MAX_CACHED_PROCESSES,
                DEFAULT_MAX_CACHED_PROCESSES);
        BACKGROUND_SETTLE_TIME = mParser.getLong(KEY_BACKGROUND_SETTLE_TIME,
                DEFAULT_BACKGROUND_SETTLE_TIME);
        ...
    }
}

3:这个常量是被 隐藏的,而且由于是全局,我们不能直接修改这个常量值,我们现在重点放在,BACKGROUND_SETTLE_TIME 这条消息的处理机制

         

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;
        if (mLocalPowerManager != null) {
            mLocalPowerManager.startUidChanges();
        }
        for (int i=N-1; i>=0; i--) {
            final UidRecord uidRec = mActiveUids.valueAt(i);
            final long bgTime = uidRec.lastBackgroundTime;
            if (bgTime > 0 && !uidRec.idle) {
                if (bgTime <= maxBgTime) {
                    EventLogTags.writeAmUidIdle(uidRec.uid);
                    uidRec.idle = true;
                    uidRec.setIdle = true;
                    doStopUidLocked(uidRec.uid, uidRec);
                } else {
                    if (nextTime == 0 || nextTime > bgTime) {
                        nextTime = bgTime;
                    }
                }
            }
        }
        if (mLocalPowerManager != null) {
            mLocalPowerManager.finishUidChanges();
        }
        if (nextTime > 0) {
            mHandler.removeMessages(IDLE_UIDS_MSG);
            mHandler.sendEmptyMessageDelayed(IDLE_UIDS_MSG,
                    nextTime + mConstants.BACKGROUND_SETTLE_TIME - nowElapsed);
        }
    }
}

   化繁就简,抛去一些判断逻辑,我们具体看看停止服务实际是执行了 doStopUidLocked函数

final void doStopUidLocked(int uid, final UidRecord uidRec) {
    mServices.stopInBackgroundLocked(uid);
    enqueueUidChangeLocked(uidRec, uid, UidRecord.CHANGE_IDLE);
}
void stopInBackgroundLocked(int uid) {
    // Stop all services associated with this uid due to it going to the background
    // stopped state.
    ServiceMap services = mServiceMap.get(UserHandle.getUserId(uid));
    ArrayList<ServiceRecord> stopping = null;
    if (services != null) {
        for (int i=services.mServicesByName.size()-1; i>=0; i--) {
            ServiceRecord service = services.mServicesByName.valueAt(i);
            if (service.appInfo.uid == uid && service.startRequested) {
                if (mAm.getAppStartModeLocked(service.appInfo.uid, service.packageName,
                        service.appInfo.targetSdkVersion, -1, false, false)
                        != ActivityManager.APP_START_MODE_NORMAL) {
                    if (stopping == null) {
                        stopping = new ArrayList<>();
                    }
                    String compName = service.name.flattenToShortString();
                    EventLogTags.writeAmStopIdleService(service.appInfo.uid, compName);
                    StringBuilder sb = new StringBuilder(64);
                    sb.append("Stopping service due to app idle: ");
                    UserHandle.formatUid(sb, service.appInfo.uid);
                    sb.append(" ");
                    TimeUtils.formatDuration(service.createTime
                            - SystemClock.elapsedRealtime(), sb);
                    sb.append(" ");
                    sb.append(compName);
                    Slog.w(TAG, sb.toString());
                    stopping.add(service);
                }
            }
        }
        if (stopping != null) {
            for (int i=stopping.size()-1; i>=0; i--) {
                ServiceRecord service = stopping.get(i);
                service.delayed = false;
                services.ensureNotStartingBackgroundLocked(service);
                stopServiceLocked(service);
            }
        }
    }
}

 这里源码可以看到,首先遍历Service ,经过一些条件判断,将满足条件的 service放入到 stopping列表中,然后遍历这个列表,停止service .  

   这里我们得到了一个小结论:8.0及以上版本手机中有一个机制,app退出后一分钟后会清理后台service(满足条件的),但是foreground service不会。所以在8.0上可以通过foreground service的形式提高存活。

从AMS分析,为什么targetsdkversion 8.0以上会杀死后台服务

     我们来看下 AMS中的 getAppStartModeLocked函数

int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
        int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
    UidRecord uidRec = mActiveUids.get(uid);
    if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg="
            + packageName + " rec=" + uidRec + " always=" + alwaysRestrict + " idle="
            + (uidRec != null ? uidRec.idle : false));
    if (uidRec == null || alwaysRestrict || uidRec.idle) {
        boolean ephemeral;
        if (uidRec == null) {
            ephemeral = getPackageManagerInternalLocked().isPackageEphemeral(
                    UserHandle.getUserId(uid), packageName);
        } else {
            ephemeral = uidRec.ephemeral;
        }
 
        if (ephemeral) {
            // We are hard-core about ephemeral apps not running in the background.
            return ActivityManager.APP_START_MODE_DISABLED;
        } else {
            if (disabledOnly) {
                // The caller is only interested in whether app starts are completely
                // disabled for the given package (that is, it is an instant app).  So
                // we don't need to go further, which is all just seeing if we should
                // apply a "delayed" mode for a regular app.
                return ActivityManager.APP_START_MODE_NORMAL;
            }
            final int startMode = (alwaysRestrict)
                    ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
                    : appServicesRestrictedInBackgroundLocked(uid, packageName,
                            packageTargetSdk);
            if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid
                    + " pkg=" + packageName + " startMode=" + startMode
                    + " onwhitelist=" + isOnDeviceIdleWhitelistLocked(uid));
            if (startMode == ActivityManager.APP_START_MODE_DELAYED) {
                // This is an old app that has been forced into a "compatible as possible"
                // mode of background check.  To increase compatibility, we will allow other
                // foreground apps to cause its services to start.
                if (callingPid >= 0) {
                    ProcessRecord proc;
                    synchronized (mPidsSelfLocked) {
                        proc = mPidsSelfLocked.get(callingPid);
                    }
                    if (proc != null &&
                            !ActivityManager.isProcStateBackground(proc.curProcState)) {
                        // Whoever is instigating this is in the foreground, so we will allow it
                        // to go through.
                        return ActivityManager.APP_START_MODE_NORMAL;
                    }
                }
            }
            return startMode;
        }
    }
    return ActivityManager.APP_START_MODE_NORMAL;
}

 这里也有很多判断,我们看首先判断是不是ephemeral apps,短暂应用?这个我没有找到更多的文档,只有一篇说chrome团队开发一款无需下载直接使用的,也不确定就是这里这个。不过我们看注释可以看到,ephemeral apps是完全不允许后台运行的,所以我们的app一定不是ephemeral apps。(这里以后有机会我们再仔细调查一下)继续,disabledOnly在前面的调用可以看到这个参数是false;继续,alwaysRestrict同样参数是false,所以这样startMode就是函数appServicesRestrictedInBackgroundLocked的返回值,这个函数如下:
 

int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
    // Persistent app?
    if (mPackageManagerInt.isPackagePersistent(packageName)) {
        if (DEBUG_BACKGROUND_CHECK) {
            Slog.i(TAG, "App " + uid + "/" + packageName
                    + " is persistent; not restricted in background");
        }
        return ActivityManager.APP_START_MODE_NORMAL;
    }
 
    // Non-persistent but background whitelisted?
    if (uidOnBackgroundWhitelist(uid)) {
        if (DEBUG_BACKGROUND_CHECK) {
            Slog.i(TAG, "App " + uid + "/" + packageName
                    + " on background whitelist; not restricted in background");
        }
        return ActivityManager.APP_START_MODE_NORMAL;
    }
 
    // Is this app on the battery whitelist?
    if (isOnDeviceIdleWhitelistLocked(uid)) {
        if (DEBUG_BACKGROUND_CHECK) {
            Slog.i(TAG, "App " + uid + "/" + packageName
                    + " on idle whitelist; not restricted in background");
        }
        return ActivityManager.APP_START_MODE_NORMAL;
    }
 
    // None of the service-policy criteria apply, so we apply the common criteria
    return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
}

1:这里有三个判断,是否Persistent app;是否在允许后台运行白名单;是否在省电(耗电)白名单。我们看都是返回ActivityManager.APP_START_MODE_NORMAL

2:回到开始的判断我们知道当不等于ActivityManager.APP_START_MODE_NORMAL时,才会将service放入stopping列表,所以这三种情况都不会停掉service

看到这里我们就知道了,如果上面三个条件都不满足的化,该应用程序就会加入到 stopping列表

int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
    // Apps that target O+ are always subject to background check
    if (packageTargetSdk >= Build.VERSION_CODES.O) {
        if (DEBUG_BACKGROUND_CHECK) {
            Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
        }
        return ActivityManager.APP_START_MODE_DELAYED_RIGID;
    }
    // ...and legacy apps get an AppOp check
    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:
            return ActivityManager.APP_START_MODE_NORMAL;
        case AppOpsManager.MODE_IGNORED:
            return ActivityManager.APP_START_MODE_DELAYED;
        default:
            return ActivityManager.APP_START_MODE_DELAYED_RIGID;
    }
}

1:终于看到我们寻找的了,第一段代码就可以看到当targetsdkversion大于等于Build.VERSION_CODES.O = 26,即8.0时,返回

ActivityManager.APP_START_MODE_DELAYED_RIGID

2:当targetsdkversion小于26,会检查app是否有后台运行的权限AppOpsManager.OP_RUN_IN_BACKGROUND,如果有权限则返回ActivityManager.APP_START_MODE_NORMAL,则根据前面的判断不会停止服务。

如何采取措施避免在 targetsdkversion 8.0系统以上杀死后台服务

 那么我们从上面三个判断着手来规避这种情况

1:Persistent  :实际上在Manifest中,我们可以为application设置android:persistent=”true”,但是前提是系统应用,也就是说我们第三方应用设置这个也没效果。关于Persistent app我们以后另开一篇文章细说。

2:是否允许后台允许:我们来看看 uidOnBackgroundWhitelist代码

private boolean uidOnBackgroundWhitelist(final int uid) {
    final int appId = UserHandle.getAppId(uid);
    final int[] whitelist = mBackgroundAppIdWhitelist;
    final int N = whitelist.length;
    for (int i = 0; i < N; i++) {
        if (appId == whitelist[i]) {
            return true;
        }
    }
    return false;
}

再来看看 mBackgroundAppIdWhitelist赋值,在backgroundWhitelistUid函数中:

第一行代码就明确表明了,只能系统应用使用这个方法,所以我们知道第三方应用无法使用这个白名单。

@Override
public void backgroundWhitelistUid(final int uid) {
    if (Binder.getCallingUid() != Process.SYSTEM_UID) {
        throw new SecurityException("Only the OS may call backgroundWhitelistUid()");
    }
 
    if (DEBUG_BACKGROUND_CHECK) {
        Slog.i(TAG, "Adding uid " + uid + " to bg uid whitelist");
    }
    synchronized (this) {
        final int N = mBackgroundAppIdWhitelist.length;
        int[] newList = new int[N+1];
        System.arraycopy(mBackgroundAppIdWhitelist, 0, newList, 0, N);
        newList[N] = UserHandle.getAppId(uid);
        mBackgroundAppIdWhitelist = newList;
    }
}

总结

这样我们基本上弄清楚8.0上service的存活机制了,按顺序经历下面几个判断

1、是否后台service,如果是foreground service则不停,否则继续

2、是否在临时白名单中,如果是则不停,否则继续

3、是否是ephemeral apps,如果是则停,否则继续

4、是否是Persistent app,如果是则不停,否则继续

5、是否在允许后台运行白名单,在则不停,否则继续

6、是否在省电白名单,在则不停,否则继续

7、是否targetsdkversion大于等于26,是则停,否则继续

8、如果targetsdkversion小于26,是否有OP_RUN_IN_BACKGROUND权限,有则不停,否则停

而在8.0上,service的停止则有一分钟的延迟。

那么如果升级了targetsdkversion,怎么才能让后台service存活?

这里有几个条件不需要考虑了,如:

临时白名单用于调试;Persistent app需要系统app;后台运行白名单也需要系统app

那么剩下就是:开启前台服务,省电白名单了

修改方案

方案一:开启前台服务

开启前台Service, 会在通知栏显示 Notification

if (!CallingStateListener.isCallingStateListener()) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    startForegroundService(Intent(this, CallingStateListener::class.java))
                } else {
                    startService(Intent(this, CallingStateListener::class.java))
                }
            }

//关闭监听电话状态服务
        if (CallingStateListener.isCallingStateListener()) {
            stopService(Intent(this, CallingStateListener::class.java))
        }
		
class CallingStateListener : Service() {
    private val TAG = "CallingStateListener"
    private var phoneStateListener: PhoneStateListener? = null
    private var telephonyManager: TelephonyManager? = null
    private val notificationId = "callingChannelId"
    private val notificationName = "callingChannelName"

    override fun onCreate() {
        super.onCreate()

        initCallingStateListener()

        val notificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //创建NotificationChannel
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                notificationId,
                notificationName,
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }
        startForeground(1, getNotification())

    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    
    private fun getNotification(): Notification {
        val builder = Notification.Builder(this)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle("通话服务")
            .setContentText("服务正在运行")
        //设置Notification的ChannelID,否则不能正常显示
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId(notificationId)
        }
        return builder.build()
    }


}
	

方案二:APP加入省电白名单

1: 首先使用:PowerManager.isIgnoringBatteryOptimizations判断是否有已经加入了

private boolean isIgnoringBatteryOptimizations(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String packageName = getPackageName();
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        return pm.isIgnoringBatteryOptimizations(packageName);
    }
    return false;
}

2:如果未加入,则需要通 Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS拉起请求弹窗

private void gotoSettingIgnoringBatteryOptimizations() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        try {
            Intent intent = new Intent();
            String packageName = getPackageName();
            intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse("package:" + packageName));
            startActivityForResult(intent, REQUEST_IGNORE_BATTERY_CODE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3:添加权限

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

4:然后再 onActivityResult中处理结果

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(resultCode == RESULT_OK){
        if (requestCode == REQUEST_IGNORE_BATTERY_CODE) {
            Log.d("Hello World!","开启省电模式成功");
        }
    }else if (resultCode == RESULT_CANCELED) {
        if (requestCode == REQUEST_IGNORE_BATTERY_CODE) {
            Toast.makeText(this, "请用户开启忽略电池优化~", Toast.LENGTH_LONG).show();
        }
    }
}

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 8.0 及以上版本,为了增强应用程序的安全性,Android 引入了后台限制,禁止未在前台运行的应用程序启动服务。如果您想在后台启动服务,需要使用 `startForegroundService()` 方法。这个方法会启动一个前台服务,然后你可以在服务启动后在通知栏显示一个通知,以此来告知用户服务正在运行。 以下是一个使用 `startForegroundService()` 的示例代码: ``` if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 创建一个 NotificationChannel NotificationChannel channel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT); // 向系统注册 NotificationChannel NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel); } // 创建一个 Intent,启动你的服务 Intent serviceIntent = new Intent(this, YourService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 在 Android 8.0 及以上版本上,需要调用 startForegroundService() 方法启动服务。 startForegroundService(serviceIntent); } else { // 在 Android 8.0 以下版本上,可以直接调用 startService() 方法启动服务。 startService(serviceIntent); } ``` 注意:如果你使用的是 `startForeground()` 方法,会在 Android 8.0 及以上版本上抛出 `IllegalStateException` 异常,因为 Android 8.0 及以上版本禁止在后台启动服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值