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提醒用户。

Android O 通知栏的"running in the background"的更多相关文章

  1. 解决Android Studio Gradle Build Running慢的问题

    Android Studio方便好用,但是Android Studio Gradle Build Running很慢 解决方法: C:\Users\你的用户名\.gradle 目录下新建一个文件名为 ...

  2. android 沉浸通知栏

    IOS的沉浸式通知栏很高大上,通知栏和app统一颜色或样式,很美观.android上面也早就人实现这种效果了. 我在这边也写一个实现通知栏沉浸式的方法,目前只实现了相同颜色. 先要改布局文件xml & ...

  3. Android:通知栏的使用

    非常久没有使用Android的通知功能了,今天把两年前的代码搬出来一看.发现非常多方法都废弃了,代码中各种删除线看的十分不爽.于是乎,打开Google,查看官方文档.学习最新的发送通知栏消息的方法. ...

  4. android 自定义通知栏

    package com.example.mvp; import cn.ljuns.temperature.view.TemperatureView;import presenter.ILoginPre ...

  5. Android Studio Gradle Build Running 特别慢的问题探讨

    本文的本本win7 64bit 6G android studio2.1 在运行程序的时候Gradle Build Running 特别慢,一个helloworld都快2min了 1.开启gradle ...

  6. Android编译报Errors running builder 'Android Pre Compiler' on project 'XXX' java.lang.NullPointerException

    编译android时,遇到报错:Errors occurred during the build.Errors running builder 'Android Pre Compiler' on pr ...

  7. Android notifications通知栏的使用

    app发送通知消息到通知栏中的关键代码和点击事件: package com.example.notifications; import android.os.Bundle; import androi ...

  8. Android——状态栏通知栏Notification

    1.AndroidManifest.xml注意要同时注册Notification02Activity <!-- 状态通知栏 Notification -->        <acti ...

  9. android显示通知栏Notification以及自定义Notification的View

    遇到的最大的问题是监听不到用户清除通知栏的广播.所以是不能监听到的. 自定义通知栏的View,然后service运行时更改notification的信息. /** * Show a notificat ...

随机推荐

  1. SQLServer2008 快捷键集合

    CTRL       +       SHIFT       +       B                 生成解决方案           CTRL       +       F7     ...

  2. [Jexus系列] 一、安装并运行 Jexus

    注意,本教程使用的jexus版本为5.8.3专业版,操作系统为 Ubunutu 16.04 64位 一.创建默认站点 不熟悉vim的可以看这个: vim超简单入门教程 sudo mkdir -p /v ...

  3. CentOS 7加强安全性:

    CentOS 7加强安全性:1. 更改 root 密码************************************************************************* ...

  4. 手动加入SSH支持、使用c3p0

    之前做的笔记,如今整理一下.大家有耐心的跟着做就能成功: SSH(struts2.spring.hibernate) *  struts2      *  充当mvc的角色 *  hibernate ...

  5. CBO之Full Table Scan - FTS算法

    转载请注明出处:http://blog.csdn.net/guoyjoe/article/details/44261859 ************************************** ...

  6. [办公自动化]excel工作簿内的表无法删除,单击右键无删除键

    今天同事问,我自己的工作簿,没有设置保护,但是就是无法删除其中的工作表. 后来发现,她的excel工作簿打开的文件名后面显示[共享]. 原因找到了. 取消共享就可以了.

  7. Delphi各销售版本之间的区别

    初步的区别: http://www.embarcadero.com/products/delphi/product-editions http://www.embarcadero.com/fr/pro ...

  8. APDU命令的结构和处理【转】

    本文转载自:http://blog.csdn.net/yonghenzhita/article/details/36402525 版权声明:本文为博主原创文章,未经博主允许不得转载. 简单说,IFD( ...

  9. Mysql中分页查询两个方法比较

    mysql中分页查询有两种方式, 一种是使用COUNT(*)的方式,具体代码如下 1 2 3 SELECT COUNT(*) FROM foo WHERE b = 1;   SELECT a FROM ...

  10. Rails5入门

    更新: 2017/05/29 更新: 2017/09/07 补充对ruby自身的扩张的放置位置  配置文件位置  /config/routes.rb  最简单的定义方法  get ('hello/in ...