• Android O 通知栏的"running in the background"


    Android O新增的一个特性,系统会在通知栏显示当前在后台运行的应用,其实际是显示启动了前台服务的应用,并且当前应用的Activity不在前台。具体我们看下源码是怎么实现的。

    1 APP调用startServicestartForegroundService启动一个service.

    startServicestartForegroundService在Android O上主要有两个区别:
    一个是后台应用无法通过startService启动一个服务,而无论前台应用还是后台应用,都可以通过startForegroundService启动一个服务。
    此外Android规定,在调用startForegroundService启动一个服务后,需要在服务被启动后5秒内调用startForeground方法,
    否则会结束掉该service并且抛出一个ANR异常。关于前台应用和后台应用的规范见官网

    2 Service被启动后,需要调用startForeground方法,将service置为前台服务。其中有一个地方要注意,第一个参数id不能等于0。

        public final void startForeground(int id, Notification notification) {
            try {
                mActivityManager.setServiceForeground(
                        new ComponentName(this, mClassName), mToken, id,
                        notification, 0);
            } catch (RemoteException ex) {
            }
        }
    

    接着调用了AMS的setServiceForeground方法,该方法会调用ActiveServicessetServiceForegroundLocked方法。
    ActiveServices是用来辅助AMS管理应用service的一个类。

        public void setServiceForegroundLocked(ComponentName className, IBinder token,
                int id, Notification notification, int flags) {
            final int userId = UserHandle.getCallingUserId();
            final long origId = Binder.clearCallingIdentity();
            try {
                ServiceRecord r = findServiceLocked(className, token, userId);
                if (r != null) {
                    setServiceForegroundInnerLocked(r, id, notification, flags);
                }
            } finally {
                Binder.restoreCallingIdentity(origId);
            }
        }
    

    3 调用了setServiceForegroundInnerLocked方法

       private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
                Notification notification, int flags) {
            if (id != 0) {
                if (notification == null) {
                    throw new IllegalArgumentException("null notification");
                }
                // Instant apps need permission to create foreground services.
                // ...A lot of code is omitted
                notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
                r.foregroundNoti = notification;
                if (!r.isForeground) {
                    final ServiceMap smap = getServiceMapLocked(r.userId);
                    if (smap != null) {
                        ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
                        if (active == null) {
                            active = new ActiveForegroundApp();
                            active.mPackageName = r.packageName;
                            active.mUid = r.appInfo.uid;
                            active.mShownWhileScreenOn = mScreenOn;
                            if (r.app != null) {
                                active.mAppOnTop = active.mShownWhileTop =
                                        r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;
                            }
                            active.mStartTime = active.mStartVisibleTime
                                    = SystemClock.elapsedRealtime();
                            smap.mActiveForegroundApps.put(r.packageName, active);
                            requestUpdateActiveForegroundAppsLocked(smap, 0);
                        }
                        active.mNumActive++;
                    }
                    r.isForeground = true;
                }
                r.postNotification();
                if (r.app != null) {
                    updateServiceForegroundLocked(r.app, true);
                }
                getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
                mAm.notifyPackageUse(r.serviceInfo.packageName,
                                     PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
            } 
        }
    

    该方法主要做的事情,创建一个ActiveForegroundApp实例,并把实例加入到smap.mActiveForegroundApps
    调用requestUpdateActiveForegroundAppsLocked,设置ServiceRecord的isForeground = true.
    由此可见,所有的前台服务都会在smap.mActiveForegroundApps列表中对应一个实例。
    requestUpdateActiveForegroundAppsLocked方法又调用了updateForegroundApps方法,见下面代码。
    这里有个关键代码是

    active.mAppOnTop = active.mShownWhileTop =
                                        r.app.uidRecord.curProcState <= ActivityManager.PROCESS_STATE_TOP;
    

    下面会再次提到这段代码。

    4 updateForegroundApps方法。通知栏上面的“running in the background”就是在这个方法里面去更新的。

        void updateForegroundApps(ServiceMap smap) {
            // This is called from the handler without the lock held.
            ArrayList<ActiveForegroundApp> active = null;
            synchronized (mAm) {
                final long now = SystemClock.elapsedRealtime();
                long nextUpdateTime = Long.MAX_VALUE;
                if (smap != null) {
                    for (int i = smap.mActiveForegroundApps.size()-1; i >= 0; i--) {
                        ActiveForegroundApp aa = smap.mActiveForegroundApps.valueAt(i);
                        // ...A lot of code is omitted
                        if (!aa.mAppOnTop) {
                            if (active == null) {
                                active = new ArrayList<>();
                            }
                            active.add(aa);
                        }
                    }
                }
            }
    
            final NotificationManager nm = (NotificationManager) mAm.mContext.getSystemService(
                    Context.NOTIFICATION_SERVICE);
            final Context context = mAm.mContext;
    
            if (active != null) {
                // ...A lot of code is omitted
                //这里是更新通知的地方,具体代码太长,省略掉了。
            } else {
                //如果active为空,取消掉通知。
                nm.cancelAsUser(null, SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
                        new UserHandle(smap.mUserId));
            }
        }
    
    • 遍历smap.mActiveForegroundApps列表,判断列表中的元素,如果其mAppOnTop成员属性为false,则加入active列表中。

    • 根据active列表,更新notification。

    可见,只有在smap.mActiveForegroundApps列表中,并且mAppOnTop为false的前台服务才会显示在通知栏中的“running in the background”中。

    以上是一个应用启动一个前台服务到被显示在通知栏中的“running in the background”中的代码上的流程。此外我们再了解些相关的逻辑。

    mAppOnTop的状态

    从上面的分析看出,mAppOnTop的值决定了一个前台服务是否会被显示在通知栏的“running in the background”中。mAppOnTop的状态除了会在创建的时候赋值,还会在另一个方法中被更新。
    每次更新后,如果值有变化,就会调用requestUpdateActiveForegroundAppsLocked,该方法上面分析过了。

        void foregroundServiceProcStateChangedLocked(UidRecord uidRec) {
            ServiceMap smap = mServiceMap.get(UserHandle.getUserId(uidRec.uid));
            if (smap != null) {
                boolean changed = false;
                for (int j = smap.mActiveForegroundApps.size()-1; j >= 0; j--) {
                    ActiveForegroundApp active = smap.mActiveForegroundApps.valueAt(j);
                    if (active.mUid == uidRec.uid) {
                        if (uidRec.curProcState <= ActivityManager.PROCESS_STATE_TOP) {
                            if (!active.mAppOnTop) {
                                active.mAppOnTop = true;
                                changed = true;
                            }
                            active.mShownWhileTop = true;
                        } else if (active.mAppOnTop) {
                            active.mAppOnTop = false;
                            changed = true;
                        }
                    }
                }
                if (changed) {
                    requestUpdateActiveForegroundAppsLocked(smap, 0);
                }
            }
        }
    

    该方法传入一个uidrecord参数,指定具体的uid及相关状态。判断逻辑跟前面setServiceForegroundInnerLocked方法的逻辑一致。
    其中ActivityManager.PROCESS_STATE_TOP的官方解释是

    Process is hosting the current top activities. Note that this covers all activities that are visible to the user.

    意思就是,当前进程的一个activity在栈顶,覆盖了所有其它activity,用户可以真正看到的。
    换句话,如果用户不能直接看到该应用的activity,并且该应用启动了一个前台服务,那么就会被显示在“running in the background”中。
    foregroundServiceProcStateChangedLocked方法只有一处调用,AMS的updateOomAdjLocked,该方法的调用地方太多,无法一一分析。

    "running in the background"通知的更新。

    除了以上两种情况(应用在service中调用startForegroundmAppOnTop的状态变更)会触发该通知的更新外,还有一些其它情况会触发更新。
    从上面代码的分析中,我们知道,触发更新的地方必须要调用requestUpdateActiveForegroundAppsLocked方法。
    该方法会在ActiveSercices类中的如下几个方法中调用。

    setServiceForegroundInnerLocked (这个我们前面分析过)
    decActiveForegroundAppLocked (减小前台应用)
    updateScreenStateLocked (屏幕状态变化)
    foregroundServiceProcStateChangedLocked (进程状态变化)
    forceStopPackageLocked (强制停止应用)

    我们看下其中的decActiveForegroundAppLocked方法

    decActiveForegroundAppLocked

        private void decActiveForegroundAppLocked(ServiceMap smap, ServiceRecord r) {
            ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
            if (active != null) {
                active.mNumActive--;
                if (active.mNumActive <= 0) {
                    active.mEndTime = SystemClock.elapsedRealtime();
                    if (foregroundAppShownEnoughLocked(active, active.mEndTime)) {
                        // Have been active for long enough that we will remove it immediately.
                        smap.mActiveForegroundApps.remove(r.packageName);
                        smap.mActiveForegroundAppsChanged = true;
                        requestUpdateActiveForegroundAppsLocked(smap, 0);
                    } else if (active.mHideTime < Long.MAX_VALUE){
                        requestUpdateActiveForegroundAppsLocked(smap, active.mHideTime);
                    }
                }
            }
        }
    

    该方法主要是移除前台service,根据foregroundAppShownEnoughLocked判断,是否马上移除还是过一段时间移除。
    该方法主要在两个地方调用。一个是在setServiceForegroundInnerLocked中调用,当应用调用startForeground,第一个参数设为0时,会走到这个路径。
    另一个bringDownServiceLocked,也就是当绑定到该service的数量减小时,会调用该方法。

    前台服务是否一定会在通知栏显示应用自己的通知

    如果是一定的话,我想系统也没必要再额外显示一条“running in the background”的通知,列出所有后台运行的应用了。

    所以答案是不一定,虽然在调用startForeground方法时,必须要传一个notification作为参数,但依然会有两种情况会导致不会在通知栏显示应用发的通知。

    • 用户主动屏蔽应用通知,可以通过长按通知,点击“ALL CATEGORIES”进入通知管理,关闭通知。关闭后前台服务依然生效。
    • NotificationManager在显示应用通知的时候,因为某些原因显示失败,失败原因可能是应用创建了不规范的通知,比如Android O新增了NotificationChannel,应用在创建通知的时候,必须指定一个NotificationChannel,但是如果应用创建通知的时候,指定的NotificationChannel是无效的,或者直接传null作为参数值,那么在NotificationManager就没办法显示该通知。这种情况下,前台服务还是会生效,但是却不会在通知栏显示应用的通知,不过NotificationManager发现不规范的通知时,一般会弹出一个toast提醒用户。
  • 相关阅读:
    P4146 序列终结者(Splay树)
    P2617 Dynamic Rankings(树套树)
    P4168 [Violet]蒲公英(分块魔术)
    P3649[APIO2014]回文串(回文自动机)
    [IOI2011]Race(树上启发式合并)
    CentOS 7安装 .net core 环境 官网说明地址
    宝塔 Linux 面板php.ini文件在哪个目录
    KPPW部署一直提示No input file specified的Apache伪静态设置
    【分享】 MPSoC的VCU超频
    Versal AIE 上手尝鲜 2 -- Linux例程
  • 原文地址:https://www.cnblogs.com/xiaji5572/p/7729987.html
Copyright © 2020-2023  润新知