IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 后台Service限制 -> 正文阅读

[移动开发]后台Service限制

每次在后台运行时,应用都会消耗一部分有限的设备资源,例如 RAM。 这可能会影响用户体验,如果用户正在使用占用大量资源的应用(例如玩游戏或观看视频),影响会尤为明显。 为了提升用户体验,Android 8.0(API 级别 26)对应用在后台运行时可以执行的操作施加了限制。
应用在两个方面受到限制:

  • 后台 Service 限制:处于空闲状态时,应用可以使用的后台 Service 存在限制。 这些限制不适用于前台 Service,因为前台 Service 更容易引起用户注意。
  • 广播限制:除了有限的例外情况,应用无法使用清单注册隐式广播。 它们仍然可以在运行时注册这些广播,并且可以使用清单注册专门针对它们的显式广播。

在 Android 8.0 之前,创建前台 Service 的方式通常是先创建一个后台 Service,然后将该 Service 推到前台。 Android 8.0 有一项复杂功能:系统不允许后台应用创建后台 Service。 因此,Android 8.0 引入了一种全新的方法,即 startForegroundService(),以在前台启动新 Service。 在系统创建 Service 后,应用有五秒的时间来调用该 Service 的 startForeground() 方法以显示新 Service 的用户可见通知。 如果应用在此时间限制内_未_调用 startForeground(),则系统将停止此 Service 并声明此应用为 ANR

本文总结:

  • App处于后台且不在临时白名单中1min后会:
    • 限制运行后台Service
    • 限制启动Service(受限app同时限制启动FGS)
  • 临时白名单常见添加场景:
    • 短信/彩信
    • 通知

前台判断

如果满足以下任意条件,应用将被视为处于前台:

  • 具有可见 Activity(不管该 Activity 已启动还是已暂停)。
  • 具有前台 Service。
  • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个 Service,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的 Service,那么该应用处于前台:
    • IME
    • 壁纸 Service
    • 通知侦听器
    • 语音或文本 Service

临时白名单

在这些情况下,后台应用将被置于一个临时白名单中并持续数分钟。 位于白名单中时,应用可以无限制地启动 Service,并且其后台 Service 也可以运行。 处理对用户可见的任务时,应用将被置于白名单中,例如:

出于特定原因在短时间内将应用程序添加到临时允许列表。 临时允许列表与永久允许列表分开保存,应用程序会在预定时间后自动从临时允许列表中删除。

// 最大临时白名单豁免时间为5min
private static final long DEFAULT_MAX_TEMP_APP_ALLOWLIST_DURATION_MS = 5 * 60 * 1000L;
// 彩信60s
private static final long DEFAULT_MMS_TEMP_APP_ALLOWLIST_DURATION_MS = 60 * 1000L;
// 短信 20s
private static final long DEFAULT_SMS_TEMP_APP_ALLOWLIST_DURATION_MS = 20 * 1000L;
// 通知30s
private static final long DEFAULT_NOTIFICATION_ALLOWLIST_DURATION_MS = 30 * 1000L;
// 由于高优先级消息而暂时允许逃避后台检查的一组应用程序 ID,短信/彩信
@CompositeRWLock({"this", "mProcLock"})
int[] mDeviceIdleTempAllowlist = new int[0];
// 暂时绕过省电模式的白名单,通知
@CompositeRWLock({"this", "mProcLock"})
final PendingTempAllowlists mPendingTempAllowlist = new PendingTempAllowlists(this);

短信/彩信

        @Override
        public void updateDeviceIdleTempAllowlist(@Nullable int[] appids, int changingUid,
                boolean adding, long durationMs, @TempAllowListType int type,
                @ReasonCode int reasonCode, @Nullable String reason, int callingUid) {
            synchronized (ActivityManagerService.this) {
                synchronized (mProcLock) {
                    if (appids != null) {
                        // 更新临时白名单
                        mDeviceIdleTempAllowlist = appids;
                    }
                    if (adding) {
                        if (type == TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED) {
                            // Note, the device idle temp-allowlist are by app-ids, but here
                            // mFgsStartTempAllowList contains UIDs.
                            mFgsStartTempAllowList.add(changingUid, durationMs,
                                    new FgsTempAllowListItem(durationMs, reasonCode, reason,
                                    callingUid));
                        }
                    } else {
                        mFgsStartTempAllowList.removeUid(changingUid);
                    }
                    setAppIdTempAllowlistStateLSP(changingUid, adding);
                }
            }
        }
    @GuardedBy({"mService", "mProcLock"})
    void setAppIdTempAllowlistStateLSP(int uid, boolean onAllowlist) {
        boolean changed = false;
        for (int i = mActiveUids.size() - 1; i >= 0; i--) {
            final UidRecord uidRec = mActiveUids.valueAt(i);
            if (uidRec.getUid() == uid && uidRec.isCurAllowListed() != onAllowlist) {
                // 设置当前uid的curAllowListed,如果是加入白名单,这里为true
                // 可用作判断当前uid的状态是否为idle
                uidRec.setCurAllowListed(onAllowlist);
                changed = true;
            }
        }
        if (changed) {
            updateOomAdjLSP(OOM_ADJ_REASON_ALLOWLIST);
        }
    }

主要是调用power相关的接口设置,具体设置流程如下时序图
在这里插入图片描述

通知

   // 这里的reason为NotificationManagerService
    // reasonCode为REASON_NOTIFICATION_SERVICE
    // type为TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED
    // duration为30s
    @GuardedBy("this")
    void tempAllowlistUidLocked(int targetUid, long duration, @ReasonCode int reasonCode,
            String reason, @TempAllowListType int type, int callingUid) {
        synchronized (mProcLock) {
            // The temp allowlist type could change according to the reasonCode.
            if (mLocalDeviceIdleController != null) {
                type = mLocalDeviceIdleController.getTempAllowListType(reasonCode, type);
            }
            if (type == TEMPORARY_ALLOW_LIST_TYPE_NONE) {
                return;
            }
            // 添加到临时白名单中
            mPendingTempAllowlist.put(targetUid,
                    new PendingTempAllowlist(targetUid, duration, reasonCode, reason, type,
                            callingUid));
            setUidTempAllowlistStateLSP(targetUid, true);
            mUiHandler.obtainMessage(PUSH_TEMP_ALLOWLIST_UI_MSG).sendToTarget();

            if (type == TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED) {
                mFgsStartTempAllowList.add(targetUid, duration,
                        new FgsTempAllowListItem(duration, reasonCode, reason, callingUid));
            }
        }
    }
        // temporarily allow apps to perform extra work when their pending intents are launched
        if (notification.allPendingIntents != null) {
            final int intentCount = notification.allPendingIntents.size();
            if (intentCount > 0) {
                // 30s
                final long duration = LocalServices.getService(
                        DeviceIdleInternal.class).getNotificationAllowlistDuration();
                for (int i = 0; i < intentCount; i++) {
                    PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                    if (pendingIntent != null) {
                        // 设置duration
                        mAmi.setPendingIntentAllowlistDuration(pendingIntent.getTarget(),
                                ALLOWLIST_TOKEN, duration,
                                TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                                REASON_NOTIFICATION_SERVICE,
                                "NotificationManagerService");
                    }
                }
            }
        }
    // allowlistToken为target
    // 这里的reason为NotificationManagerService
    // reasonCode为REASON_NOTIFICATION_SERVICE
    // type为TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED
    // duration为30s
	void setAllowlistDurationLocked(IBinder allowlistToken, long duration, int type,
            @ReasonCode int reasonCode, @Nullable String reason) { 
        if (duration > 0) {
            if (mAllowlistDuration == null) {
                mAllowlistDuration = new ArrayMap<>();
            }
            mAllowlistDuration.put(allowlistToken,
                    new TempAllowListDuration(duration, type, reasonCode, reason));
        } else if (mAllowlistDuration != null) {
            mAllowlistDuration.remove(allowlistToken);
            if (mAllowlistDuration.size() <= 0) {
                mAllowlistDuration = null;
            }
        }
        this.stringName = null;
    }

整个设置duration的流程如下时序图
在这里插入图片描述

PendingIntent发送时会通过校验,通知AMS设置临时白名单

    public int sendInner(int code, Intent intent, String resolvedType, IBinder allowlistToken,
            IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo,
            String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) {
        .......
        synchronized (controller.mLock) {
           ......
            // 如果在通知入队时已经设置了duration
            if (mAllowlistDuration != null) {
                duration = mAllowlistDuration.get(allowlistToken);
            }
            .......
        }
       ......
        try {
            // 调用AMS接口tempAllowlistForPendingIntent设置临时白名单
            if (duration != null) {
                StringBuilder tag = new StringBuilder(64);
                tag.append("setPendingIntentAllowlistDuration,reason:");
                tag.append(duration.reason == null ? "" : duration.reason);
                tag.append(",pendingintent:");
                UserHandle.formatUid(tag, callingUid);
                tag.append(":");
                if (finalIntent.getAction() != null) {
                    tag.append(finalIntent.getAction());
                } else if (finalIntent.getComponent() != null) {
                    finalIntent.getComponent().appendShortString(tag);
                } else if (finalIntent.getData() != null) {
                    tag.append(finalIntent.getData().toSafeString());
                }
                controller.mAmInternal.tempAllowlistForPendingIntent(callingPid, callingUid,
                        uid, duration.duration, duration.type, duration.reasonCode, tag.toString());
            } else if (key.type == ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE
                    && options != null) {
                // If this is a getForegroundService() type pending intent, use its BroadcastOptions
                // temp allowlist duration as its pending intent temp allowlist duration.
                BroadcastOptions brOptions = new BroadcastOptions(options);
                if (brOptions.getTemporaryAppAllowlistDuration() > 0) {
                    controller.mAmInternal.tempAllowlistForPendingIntent(callingPid, callingUid,
                            uid, brOptions.getTemporaryAppAllowlistDuration(),
                            brOptions.getTemporaryAppAllowlistType(),
                            brOptions.getTemporaryAppAllowlistReasonCode(),
                            brOptions.getTemporaryAppAllowlistReason());
                }
            }

          ....
    }

设置白名单的流程如下时序图
在这里插入图片描述

Uid空闲状态

如果 UID 现在在后台(不在临时白名单上), 它之前是在前台(或在临时白名单上);那么1min后当前UID将会处于idle状态。

// procState >= PROCESS_STATE_TRANSIENT_BACKGROUND 8
if (ActivityManager.isProcStateBackground(uidRec.getCurProcState())
        && !uidRec.isCurAllowListed()) {
    // UID 现在在后台(不在临时许可名单上)。 它之前是否在前台(或在临时许可名单上)?
    if (!ActivityManager.isProcStateBackground(uidRec.getSetProcState())
        || uidRec.isSetAllowListed()) {
        // 这里传入的elapsedRealtime而不是uptimeMillis,谷歌代码曾出过问题,
        // 具体见:http://gerrit.pt.mioffice.cn/c/platform/frameworks/base/+/1633705
        uidRec.setLastBackgroundTime(nowElapsed);
        if (!mService.mHandler.hasMessages(IDLE_UIDS_MSG)) {
            // post 1min后的消息到handler中去执行idle操作
            mService.mHandler.sendEmptyMessageDelayed(IDLE_UIDS_MSG,
                    mConstants.BACKGROUND_SETTLE_TIME);
        }
    }
    if (uidRec.isIdle() && !uidRec.isSetIdle()) {
        uidChange |= UidRecord.CHANGE_IDLE;
        becameIdle.add(uidRec);
    }

查找最近不活动的应用程序,并在宽限期后将它们标记为空闲。 如果空闲,停止任何后台服务并通知听众。

  • idle处理逻辑是在system server的main线程中进行的
  • idle时间是在处于后台或不在临时白名单的1min后
  • 遍历active uid时,时间到了就执行doStopUidLocked
  • 时间未到计算更新为最近的一次idle时间并再次发送IDLE_UIDS_MSG消息
    @GuardedBy("mService")
    void idleUidsLocked() {
        ......
        final long nowElapsed = SystemClock.elapsedRealtime();
        final long maxBgTime = nowElapsed - mConstants.BACKGROUND_SETTLE_TIME;
        ......
    	// 1min后执行消息
        for (int i = N - 1; i >= 0; i--) {
            final UidRecord uidRec = mActiveUids.valueAt(i);
            final long bgTime = uidRec.getLastBackgroundTime();
            // 距离设置后台时间超过1min且当前uid不是idle则设置idle
            if (bgTime > 0 && !uidRec.isIdle()) {
                if (bgTime <= maxBgTime) {
                    EventLogTags.writeAmUidIdle(uidRec.getUid());
                    synchronized (mProcLock) {
                        uidRec.setIdle(true);
                        uidRec.setSetIdle(true);
                    }
                    // 停止当前uid下的后台Services
                    mService.doStopUidLocked(uidRec.getUid(), uidRec);
                } .......
            }
        }

后台Service限制

处于前台时,应用可以自由创建和运行前台与后台 Service。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用 Service。 在该时间窗结束后,应用将被视为处于_空闲_状态。 此时,系统将停止应用的后台 Service,就像应用已经调用 Service 的 Service.stopSelf() 方法一样。

停止后台运行Service

因为当前uid进入后台1min后处于idle状态,停止与此 uid 关联的所有服务;所以我们会经常在bugreport中看到类似如下log。

08-27 20:08:12.038 1588 5729 I am_uid_active: 10135
08-27 20:09:13.517 1588 2114 I am_uid_idle: 10135
08-27 20:09:13.517 1588 2114 I am_stop_idle_service: [10135,com.android.htmlviewer/com.android.settings.services.MemoryOptimizationService]

void stopInBackgroundLocked(int uid) {
    ServiceMap services = mServiceMap.get(UserHandle.getUserId(uid));
    ArrayList<ServiceRecord> stopping = null;
    if (services != null) {
        for (int i = services.mServicesByInstanceName.size() - 1; i >= 0; i--) {
            ServiceRecord service = services.mServicesByInstanceName.valueAt(i);
            if (service.appInfo.uid == uid && service.startRequested) {
                if (mAm.getAppStartModeLOSP(service.appInfo.uid, service.packageName,
                        service.appInfo.targetSdkVersion, -1, false, false, false)
                        service.mRecentCallingPackage)
                        != ActivityManager.APP_START_MODE_NORMAL) {
                    if (stopping == null) {
                        stopping = new ArrayList<>();
                    }
                    String compName = service.shortInstanceName;
                    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.createRealTime
                            - SystemClock.elapsedRealtime(), sb);
                    sb.append(" ");
                    sb.append(compName);
                    Slog.w(TAG, sb.toString());
                    stopping.add(service);
                	// 如果应用程序受到后台限制,还要确保取消任何通知
                    if (appRestrictedAnyInBackground(
                            service.appInfo.uid, service.packageName)) {
                        cancelForegroundNotificationLocked(service);
                    }
                }
            }
        }
        if (stopping != null) {
            final int size = stopping.size();
            for (int i = size - 1; i >= 0; i--) {
                ServiceRecord service = stopping.get(i);
                service.delayed = false;
                services.ensureNotStartingBackgroundLocked(service);
                stopServiceLocked(service, true);
            }
            if (size > 0) {
                mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE);
            }
        }
    }
}

注意:stopInBackgroundLocked回调后,service不一定会被stop

    private final void bringDownServiceIfNeededLocked(ServiceRecord r, boolean knowConn,
            boolean hasConn) {

        if (isServiceNeededLocked(r, knowConn, hasConn)) {
            return;
        }

        // 如果有新拉起service的需求,本次不会stop该service
        if (mPendingServices.contains(r)) {
            return;
        }

        bringDownServiceLocked(r);
    }

限制后台启动Service

如果Service是间接启动的(例如从 PendingIntent),弄清楚是否正在后台状态下启动一个应用程序。
当Service所在App处于后台(uid为idle)时,会限制启动Service,限制分两种情况:

  1. App处于后台受限模式下,限制启动任何Service(包括FGS);

ActivityManager: Background start not allowed: service Intent { act=geofence_trigered cmp=com.xiaomi.smarthome/.scene.activity.GeoActionService (has extras) } to com.xiaomi.smarthome/.scene.activity.GeoActionService from pid=9233 uid=10270 pkg=com.xiaomi.smarthome startFg?=true

  1. App处于无限制模式下,仅限制启动后台Service;

无限制下android.app.AppOpsManager#OP_RUN_ANY_IN_BACKGROUND 为ALLOWED

09-13 10:30:40.633 1581 2603 W ActivityManager: Background start not allowed: service Intent { cmp=com.android.deskclock/.addition.resource.ResourceLoadService (has extras) } to com.android.deskclock/.addition.resource.ResourceLoadService from pid=14625 uid=10216 pkg=com.android.deskclock startFg?=false

// 当前uid不处于空闲状态
final boolean bgLaunch = !mAm.isUidActiveLOSP(r.appInfo.uid);
boolean forcedStandby = false;
// 如果应用程序有严格的后台限制,我们将任何 bg 服务启动类似于旧版应用程序强制限制情况,
// 无论其目标 SDK 版本如何。
if (bgLaunch && appRestrictedAnyInBackground(r.appInfo.uid, r.packageName)) {
    if (DEBUG_FOREGROUND_SERVICE) {
        Slog.d(TAG, "Forcing bg-only service start only for " + r.shortInstanceName
                + " : bgLaunch=" + bgLaunch + " callerFg=" + callerFg);
    }
    forcedStandby = true;
}
    	.......
if (forcedStandby || (!r.startRequested && !fgRequired)) {
    // 在继续之前——如果这个应用程序不允许在后台启动服务,那么在这一点上我们不会让它运行。    
    final int allowed = mAm.getAppStartModeLOSP(r.appInfo.uid, r.packageName,
            r.appInfo.targetSdkVersion, callingPid, false, false, forcedStandby);
    if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
        Slog.w(TAG, "Background start not allowed: service "
                + service + " to " + r.shortInstanceName
                + " from pid=" + callingPid + " uid=" + callingUid
                + " pkg=" + callingPackage + " startFg?=" + fgRequired);

查看是否允许启动

    @GuardedBy(anyOf = {"this", "mProcLock"})
    int appServicesRestrictedInBackgroundLOSP(int uid, String packageName, int packageTargetSdk) {
        ......
        // Is this app on the battery whitelist?
        if (isOnDeviceIdleAllowlistLOSP(uid, /*allowExceptIdleToo=*/ false)) {
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "App " + uid + "/" + packageName
                        + " on idle allowlist; not restricted in background");
            }
            return ActivityManager.APP_START_MODE_NORMAL;
        }
        ......
    }
    /**
     * @return whether a UID is in the system, user or temp doze allowlist.
     */
    @GuardedBy(anyOf = {"this", "mProcLock"})
    boolean isOnDeviceIdleAllowlistLOSP(int uid, boolean allowExceptIdleToo) {
        final int appId = UserHandle.getAppId(uid);

        final int[] allowlist = allowExceptIdleToo
                ? mDeviceIdleExceptIdleAllowlist
                : mDeviceIdleAllowlist;

        return Arrays.binarySearch(allowlist, appId) >= 0
                // 临时白名单 for 短信/彩信等
                || Arrays.binarySearch(mDeviceIdleTempAllowlist, appId) >= 0
                // 临时白名单 for 通知等
                || mPendingTempAllowlist.get(uid) != null;
    }

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-12-25 11:21:32  更:2022-12-25 11:26:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年6日历 -2024/6/18 13:02:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码