Android 10 (API 级别 29) 及更高版本对后台应用可启动 Activity
的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。 注意:为启动
Activity,系统仍会将运行前台服务的应用视为“后台”应用。
为了改进应用性能和用户体验,以 Android 12 或更高版本为目标平台的应用无法从用作通知 trampoline
的服务或广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用
startActivity()。
本文总结
- 豁免while-in-use和后台启动Activity
豁免场景 | 后台启动Activity | 后台FGS的while-in-use | 时限 |
---|---|---|---|
系统的 PendingIntent 通知 | trampoline白名单中允 | 允许 | 10s |
可见应用发送的 PendingIntent | 允许 | 允许 | 10s |
绑定服务时传入BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS | 允许 | 允许 | Connection存活期间 |
- 系统的 PendingIntent 通知 同时也会豁免后台启动后台Service,时限为30s。
当前有助于此过程的一组令牌被临时允许启动活动,即使它不在前台也是如此。 此映射的值是可选的(可以为空),用于追溯对通知令牌机制的授权。
@GuardedBy("this")
private @Nullable ArrayMap<Binder, IBinder> mBackgroundActivityStartTokens;
BackgroundLaunchProcessController 介绍
进程控制器决定进程是否可以后台启动activity或后台启动前台Service,这个类是android13从WindowProcessController中开始抽取出来,为了是不持有wm Lock。
- 代码路径:frameworks/base/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
- 类结构:
添加或更新Token
后台启动的例外情况包含如下:
● 应用收到系统的 PendingIntent 通知。对于服务和广播接收器的PedingIntent,应用可在该PendingIntent 发送几秒钟后启动 Activity。
● 应用收到另一个可见应用发送的 PendingIntent。
添加或更新当前进程的mBackgroundActivityStartTokens,token的作用主要是校验后台启动FGS或后台启动Activity;且一般由广播分发或Service启动时去添加或更新,主要添加路径有如下几个:
- 通知的PendingIntent触发添加;
- 发送PendingIntent的app处于前台;
- bind Service时带有BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS的flag(这个需要申请START_ACTIVITIES_FROM_BACKGROUND)
允许后台activity使用令牌 {@param entity} 启动。 或者,您可以提供 {@param
originatingToken} 如果您有一个这样的原始令牌,这对于在通知令牌的情况下追溯授权很有用。
// entity:广播或service的实例
// originatingToken: 如果是通知触发,则为NMS的ALLOWLIST_TOKEN
void addOrUpdateAllowBackgroundActivityStartsToken(Binder entity,
@Nullable IBinder originatingToken) {
synchronized (this) {
if (mBackgroundActivityStartTokens == null) {
mBackgroundActivityStartTokens = new ArrayMap<>();
}
mBackgroundActivityStartTokens.put(entity, originatingToken);
}
}
主要调用的地方如下:
添加的时序图如下,由BroadcastQueue或ServiceRecord触发
通知触发
几乎在所有情况下,后台应用都应显示有时效性的通知,以便向用户提供紧急信息,而非直接启动
Activity。此类通知的适用情形包括处理来电或正在响铃的闹钟。
通知入队时会检测是否有PendingIntent并遍历调用setPendingIntentAllowBgActivityStarts来设置PendingIntentRecord中的对应的mAllowBgActivityStarts;在sendInner发送到对应的Broadcast或Service中去更新上面说到的token。
// 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) {
.....
for (int i = 0; i < intentCount; i++) {
PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
if (pendingIntent != null) {
......
// 设置后台允许的token,这里包括activity&broadcast&service的flag
mAmi.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
ALLOWLIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER
| FLAG_SERVICE_SENDER));
}
}
}
}
设置的流程如下时序图
由前面的flag可知,会加入如下三个列表,至于具体的添加过程下面会讲到。
void setAllowBgActivityStarts(IBinder token, int flags) {
if (token == null) return;
if ((flags & FLAG_ACTIVITY_SENDER) != 0) {
mAllowBgActivityStartsForActivitySender.add(token);
}
if ((flags & FLAG_BROADCAST_SENDER) != 0) {
mAllowBgActivityStartsForBroadcastSender.add(token);
}
if ((flags & FLAG_SERVICE_SENDER) != 0) {
mAllowBgActivityStartsForServiceSender.add(token);
}
}
移除Token
因通知而添加的Start Service或广播相关Token是有时间限制的,10s后均会被移除;即10s内这些trampoline可能会豁免相关的后台启动activity或FGS。
void removeAllowBackgroundActivityStartsToken(Binder entity) {
synchronized (this) {
if (mBackgroundActivityStartTokens != null) {
mBackgroundActivityStartTokens.remove(entity);
}
}
}
广播相关Token移除
- 无序广播执行performReceive 10s后移除Token;
- 有序广播则需等第一个广播分发完成才开始post 10s移除;
Service相关Token移除
- Start Service token移除:service相关的进程重启;Service启动10s后移除
- Bind Service token移除:相关的Connection被移除
时序图如下,也是由BroadcastQueue或ServiceRecord触发移除。
使用Token
android 12+通知 trampoline 限制
当用户与通知互动时,某些应用会启动一个应用组件来响应通知点按操作,该应用组件最终会启动用户最终看到并与之互动的 activity。此应用组件被称为通知 trampoline。
为了改进应用性能和用户体验,以 Android 12 或更高版本为目标平台的应用无法从用作通知 trampoline 的服务或广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用 startActivity()。
当您的应用尝试从充当通知 trampoline 的服务或广播接收器启动 activity 时,系统会阻止该 activity 启动,并在 Logcat 中显示以下消息:
Indirect notification activity start (trampoline) from PACKAGE_NAME, \ this should be avoided for performance reasons.
对于以 Android 13(API 级别
33)及更高版本为目标平台的应用,出于用户体验和性能考虑,之前具有豁免角色的权限持有者(浏览器)将被禁止从广播接收器或服务启动
Activity 来响应通知和通知点击操作。
具体限制代码如下:
private class NotificationTrampolineCallback implements BackgroundActivityStartCallback {
@Override
public boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid,
String packageName) {
checkArgument(!tokens.isEmpty());
for (IBinder token : tokens) {
// 如果当前进程有不是通知传入的token,则直接允许
if (token != ALLOWLIST_TOKEN) {
// We only block or warn if the start is exclusively due to notification
return true;
}
}
String logcatMessage =
"Indirect notification activity start (trampoline) from " + packageName;
if (blockTrampoline(uid)) {
Slog.e(TAG, logcatMessage + " blocked");
return false;
} else {
Slog.w(TAG, logcatMessage + ", this should be avoided for performance reasons");
return true;
}
}
private boolean blockTrampoline(int uid) {
if (mRoleObserver != null && mRoleObserver.isUidExemptFromTrampolineRestrictions(uid)) {
return CompatChanges.isChangeEnabled(NOTIFICATION_TRAMPOLINE_BLOCK_FOR_EXEMPT_ROLES,
uid);
}
return CompatChanges.isChangeEnabled(NOTIFICATION_TRAMPOLINE_BLOCK, uid);
}
Token豁免后台启动限制
- 后台启动FGS使用中权限校验:进程有tokens即允许;
- 后台启动activity:在进程有tokens基础上,进程有非通知传入的token;或在豁免trampoline白名单中;
private boolean isBackgroundStartAllowedByToken(int uid, String packageName,
boolean isCheckingForFgsStart) {
synchronized (this) {
// 该进程没有tokens,直接返回false
if (mBackgroundActivityStartTokens == null
|| mBackgroundActivityStartTokens.isEmpty()) {
return false;
}
// 进程有tokens,如果是background FGS start则直接允许
if (isCheckingForFgsStart) {
// BG-FGS-start only checks if there is a token.
return true;
}
// 这个一般不会为null,为NotificationTrampolineCallback
if (mBackgroundActivityStartCallback == null) {
// We have tokens but no callback to decide => allow.
return true;
}
// The callback will decide.
// 通知服务中NotificationTrampolineCallback的isActivityStartAllowed方法来决定
return mBackgroundActivityStartCallback.isActivityStartAllowed(
mBackgroundActivityStartTokens.values(), uid, packageName);
}
}
后台启动FGS
- 后台启动FGS使用中权限校验:进程有tokens即允许;
当前进程10s内有相关通知触发的trampoline或者进程下的Service有带BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS flag的Connection即可允许。
@HotPath(caller = HotPath.START_SERVICE)
public boolean areBackgroundFgsStartsAllowed() {
return areBackgroundActivityStartsAllowed(mAtm.getBalAppSwitchesState(),
true /* isCheckingForFgsStart */);
}
后台启动activity
- 后台启动activity:在进程有tokens基础上,进程有非通知传入的token;或在豁免trampoline白名单中;
boolean areBackgroundActivityStartsAllowed(int appSwitchState) {
return areBackgroundActivityStartsAllowed(appSwitchState,
false /* isCheckingForFgsStart */);
}
广播Token的添加和移除
添加
allowTrampoline介绍
应用收到另一个可见应用发送的 PendingIntent。
如果PendingIntent的caller处于可见状态,则跟通知添加的mAllowBgActivityStarts token的效果类似,也会最终添加上面说到的token。
// temporarily allow receivers and services to open activities from background if the
// PendingIntent.send() caller was foreground at the time of sendInner() call
final boolean allowTrampoline = uid != callingUid
&& controller.mAtmInternal.isUidForeground(callingUid)
&& isPendingIntentBalAllowedByCaller(options);
PendingIntentRecord发送时会校验传入的allowBackgroundActivityStart的值,满足如下其一为true
- mAllowBgActivityStartsForBroadcastSender中存在传入的allowListToken
- 满足上面的allowTrampoline
case ActivityManager.INTENT_SENDER_BROADCAST:
try {
final boolean allowedByToken =
mAllowBgActivityStartsForBroadcastSender.contains(allowlistToken);
final IBinder bgStartsToken = (allowedByToken) ? allowlistToken : null;
// If a completion callback has been requested, require
// that the broadcast be delivered synchronously
int sent = controller.mAmInternal.broadcastIntentInPackage(key.packageName,
key.featureId, uid, callingUid, callingPid, finalIntent,
resolvedType, finishedReceiver, code, null, null,
requiredPermission, options, (finishedReceiver != null), false,
userId, allowedByToken || allowTrampoline, bgStartsToken,
null /* broadcastAllowList */);
广播分发给app之前触发更新或添加
private void maybeAddAllowBackgroundActivityStartsToken(ProcessRecord proc, BroadcastRecord r) {
if (r == null || proc == null || !r.allowBackgroundActivityStarts) {
return;
}
String msgToken = (proc.toShortString() + r.toString()).intern();
// first, if there exists a past scheduled request to remove this token, drop
// that request - we don't want the token to be swept from under our feet...
mHandler.removeCallbacksAndMessages(msgToken);
// ...then add the token
proc.addOrUpdateAllowBackgroundActivityStartsToken(r, r.mBackgroundActivityStartsToken);
}
移除
一般是post到handler中延时10s去执行移除Token的操作,即可能的后台启动能力在10s内才有效。
private void postActivityStartTokenRemoval(ProcessRecord app, BroadcastRecord r) {
// the receiver had run for less than allowed bg activity start timeout,
// so allow the process to still start activities from bg for some more time
String msgToken = (app.toShortString() + r.toString()).intern();
// first, if there exists a past scheduled request to remove this token, drop
// that request - we don't want the token to be swept from under our feet...
mHandler.removeCallbacksAndMessages(msgToken);
// ...then schedule the removal of the token after the extended timeout
mHandler.postAtTime(() -> {
synchronized (mService) {
app.removeAllowBackgroundActivityStartsToken(r);
}
}, msgToken, (r.receiverTime + mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT));
}
无序广播在执行performReceive后即可postActivityStartTokenRemoval
maybeAddAllowBackgroundActivityStartsToken(filter.receiverList.app, r);
maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);
maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
new Intent(r.intent), r.resultCode, r.resultData,
r.resultExtras, r.ordered, r.initialSticky, r.userId,
filter.receiverList.uid, r.callingUid);
// parallel broadcasts are fire-and-forget, not bookended by a call to
// finishReceiverLocked(), so we manage their activity-start token here
if (filter.receiverList.app != null
&& r.allowBackgroundActivityStarts && !r.ordered) {
postActivityStartTokenRemoval(filter.receiverList.app, r);
}
有序广播则需等第一个广播分发完成才开始post
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
.......
if (r.allowBackgroundActivityStarts && r.curApp != null) {
if (elapsed > mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT) {
// if the receiver has run for more than allowed bg activity start timeout,
// just remove the token for this process now and we're done
r.curApp.removeAllowBackgroundActivityStartsToken(r);
} else {
// It gets more time; post the removal to happen at the appropriate moment
postActivityStartTokenRemoval(r.curApp, r);
}
}
Service Token的添加和移除
Start Service Token的添加和移除
跟广播类似,通过mAllowBgActivityStartsForServiceSender传入allowBackgroundActivityStarts参数去start service。
case ActivityManager.INTENT_SENDER_SERVICE:
case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE:
try {
final boolean allowedByToken =
mAllowBgActivityStartsForServiceSender.contains(allowlistToken);
final IBinder bgStartsToken = (allowedByToken) ? allowlistToken : null;
controller.mAmInternal.startServiceInPackage(uid, finalIntent, resolvedType,
key.type == ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE,
key.packageName, key.featureId, userId,
allowedByToken || allowTrampoline, bgStartsToken);
} catch (RuntimeException e) {
Slog.w(TAG, "Unable to send startService intent", e);
} catch (TransactionTooLargeException e) {
res = ActivityManager.START_CANCELED;
}
break;
}
启动Service过程中因为传入的allowBackgroundActivityStarts为true,会去执行ServiceRecord的allowBgActivityStartsOnServiceStart相关流程,最终设置将当前的token添加到BackgroundLaunchProcessController的mBackgroundActivityStartTokens列表中。
if (allowBackgroundActivityStarts) {
r.allowBgActivityStartsOnServiceStart(backgroundActivityStartsToken);
}
void allowBgActivityStartsOnServiceStart(@Nullable IBinder originatingToken) {
mBgActivityStartsByStartOriginatingTokens.add(originatingToken);
// 添加Token
setAllowedBgActivityStartsByStart(true);
if (app != null) {
mAppForAllowingBgActivityStartsByStart = app;
}
// 创建Runnable,并在10s后post
// This callback is stateless, so we create it once when we first need it.
if (mCleanUpAllowBgActivityStartsByStartCallback == null) {
// 创建
mCleanUpAllowBgActivityStartsByStartCallback = () -> {
synchronized (ams) {
// 如果这不是当前Service的最后一个Token
mBgActivityStartsByStartOriginatingTokens.remove(0);
if (!mBgActivityStartsByStartOriginatingTokens.isEmpty()) {
// There are other callbacks in the queue, let's just update the originating
// token
// 更新Token
if (mIsAllowedBgActivityStartsByStart) {
// mAppForAllowingBgActivityStartsByStart can be null here for example
// if get 2 calls to allowBgActivityStartsOnServiceStart() without a
// process attached to this ServiceRecord, so we need to perform a null
// check here.
if (mAppForAllowingBgActivityStartsByStart != null) {
mAppForAllowingBgActivityStartsByStart
.addOrUpdateAllowBackgroundActivityStartsToken(
this, getExclusiveOriginatingToken());
}
} else {
Slog.wtf(TAG,
"Service callback to revoke bg activity starts by service "
+ "start triggered but "
+ "mIsAllowedBgActivityStartsByStart = false. This "
+ "should never happen.");
}
} else {
// 最后一个Token则移除当前进程Token
// Last callback on the queue
if (app == mAppForAllowingBgActivityStartsByStart) {
// The process we allowed is still running the service. We remove
// the ability by start, but it may still be allowed via bound
// connections.
setAllowedBgActivityStartsByStart(false);
} else if (mAppForAllowingBgActivityStartsByStart != null) {
// The process we allowed is not running the service. It therefore can't
// be bound so we can unconditionally remove the ability.
mAppForAllowingBgActivityStartsByStart
.removeAllowBackgroundActivityStartsToken(ServiceRecord.this);
}
mAppForAllowingBgActivityStartsByStart = null;
}
}
};
}
// 10s后post执行
// Existing callbacks will only update the originating token, only when the last callback is
// executed is the grant revoked.
ams.mHandler.postDelayed(mCleanUpAllowBgActivityStartsByStartCallback,
ams.mConstants.SERVICE_BG_ACTIVITY_START_TIMEOUT);
}
Bind Service Token的添加和移除
添加或移除Token都是调用如下两个方法:
void setAllowedBgActivityStartsByBinding(boolean newValue) {
mIsAllowedBgActivityStartsByBinding = newValue;
updateParentProcessBgActivityStartsToken();
}
private void updateParentProcessBgActivityStartsToken() {
if (app == null) {
return;
}
if (mIsAllowedBgActivityStartsByStart || mIsAllowedBgActivityStartsByBinding) {
// if the token is already there it's safe to "re-add it" - we're dealing with
// a set of Binder objects
app.addOrUpdateAllowBackgroundActivityStartsToken(this, getExclusiveOriginatingToken());
} else {
app.removeAllowBackgroundActivityStartsToken(this);
}
}
添加
bindService的时候如果bind flag中添加了BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,会去添加或更新对应的Token。
if ((flags & Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS) != 0) {
s.setAllowedBgActivityStartsByBinding(true);
}
移除
unbindService或Service被杀调用removeConnectionLocked时,会去check是否有BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,查看是否需要移除相关Token
// And do the same for bg activity starts ability.
if ((c.flags & Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS) != 0) {
s.updateIsAllowedBgActivityStartsByBinding();
}
void updateIsAllowedBgActivityStartsByBinding() {
boolean isAllowedByBinding = false;
for (int conni = connections.size() - 1; conni >= 0; conni--) {
ArrayList<ConnectionRecord> cr = connections.valueAt(conni);
for (int i = 0; i < cr.size(); i++) {
if ((cr.get(i).flags & Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS) != 0) {
isAllowedByBinding = true;
break;
}
}
if (isAllowedByBinding) {
break;
}
}
setAllowedBgActivityStartsByBinding(isAllowedByBinding);
}
Token使用
Bg start FGS的while in use权限
if (ret == REASON_DENIED) {
final Integer allowedType = mAm.mProcessList.searchEachLruProcessesLOSP(false, pr -> {
if (pr.uid == callingUid) {
if (pr.getWindowProcessController().areBackgroundFgsStartsAllowed()) {
return REASON_ACTIVITY_STARTER;
}
}
return null;
});
if (allowedType != null) {
ret = allowedType;
}
}
Bg Start Activity
检查callerApp或者同一uid下的其他进程是否允许后台启动。
if (callerApp != null && useCallingUidState) {
// first check the original calling process
if (callerApp.areBackgroundActivityStartsAllowed(appSwitchState)) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Background activity start allowed: callerApp process (pid = "
+ callerApp.getPid() + ", uid = " + callerAppUid + ") is allowed");
}
return false;
}
// only if that one wasn't allowed, check the other ones
final ArraySet<WindowProcessController> uidProcesses =
mService.mProcessMap.getProcesses(callerAppUid);
if (uidProcesses != null) {
for (int i = uidProcesses.size() - 1; i >= 0; i--) {
final WindowProcessController proc = uidProcesses.valueAt(i);
if (proc != callerApp
&& proc.areBackgroundActivityStartsAllowed(appSwitchState)) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG,
"Background activity start allowed: process " + proc.getPid()
+ " from uid " + callerAppUid + " is allowed");
}
return false;
}
}
}
}