Android O新增的一个特性,系统会在通知栏显示当前在后台运行的应用,其实际是显示启动了前台服务的应用,并且当前应用的Activity不在前台。具体我们看下源码是怎么实现的。
1 APP调用startService
或startForegroundService
启动一个service.
startService
和startForegroundService
在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
方法,该方法会调用ActiveServices
的setServiceForegroundLocked
方法。
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中调用startForeground
和mAppOnTop
的状态变更)会触发该通知的更新外,还有一些其它情况会触发更新。
从上面代码的分析中,我们知道,触发更新的地方必须要调用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提醒用户。