请点赞,你的点赞对我意义重大,满足下我的虚荣心。
🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 跟我一起成长。
PendingIntent 的应用场景关键在于间接的 Intent 跳转需求, 即先通过一级 Intent 跳转到某个组件,在该组件完成任务后再间接地跳转到二级的 Intent。PendingIntent 中的单词 “pending” 指延迟或挂起,就是指它是延迟的或挂起的。例如,你在以下场景中就可以使用 PendingIntent:
可以看到,在这些场景中,我们真正感兴趣的操作是挂起的,并且该操作并不是由当前应用执行,而是由某个外部应用来 “间接” 执行的。例如,我们在发送系统通知消息时,会通过 PendingIntent 构造一个系统通知 Notification
,并调用 NotificationManagerCompat.notify(…)
发送通知,此时并不会直接执行 PendingIntent。而是当系统显示通知,并且用户点击通知时,才会由系统通知这个系统应用间接执行 PendingIntent#send()
,而不是通过当前应用执行。
当然,在低版本系统中,你还可以使用嵌套 Intent(Intent#extra 中嵌套另一个 Intent)来实现以上需求。但是从 Android 12 开始,嵌套 Intent 将被严格禁止,原因下文会说。
从结构上来说,PendingIntent 是 Intent 的包装类,其内部持有一个代表最终意图操作的 Intent(事实上,内部是通过 IIntentSender
间接持有)。它们的区别我认为可以概括为 3 个维度:
提示: 当然了,如果你创建 PendingIntent 后又马上同步地在当前进程消费这个 PendingIntent,那么时间维度上就没区别了。但是这样做其实不符合 PendingIntent 的应用场景。
上文提到,在低版本系统中,你可以使用嵌套 Intent 实现类似于 PendingIntent 的需求。但这一方案从 Android 12 开始被严格禁止,为什么呢 —— 存在安全隐患。
举个例子,我们将启动 ClientCallbackActivity 的 Intent 嵌套到启动 ApiService 的 Intent 里,实现一个 场景 4 - 第三方应用回调操作 的效果:
该过程用示意图表示如下:
乍看起来没有问题,但其实存在 2 个隐蔽的安全隐患:
该攻击过程用示意图表示如下:
解决方法是使用 PendingIntent 代替嵌套 Intent,此时这两个风险都不存在。为什么呢?—— 因为 PendingIntent 将以 Client App(PendingIntent 的创建进程)的身份执行,而不是 Provider App (PendingIntent 的消费进程)的身份执行。
现在,我们再回顾下还有没有安全隐患:
该过程用示意图表示如下:
提示: 担心有的同学钻牛角这里再补充一下:如果我的二级 Intent 就是想要回调到 Provider App 中的 ApiSensitiveActivity 那怎么办?很简单,说明 Client 并不关心回调,那么就直接使用 Intent 即可,Provider App 内部的回调行为交给其内部处理。
PendingIntent 支持在启动 Activity、Service 或 BroadcastReceiver。不同类型的组件必须使用特定的静态方法:
示例程序
// 启动 Activity PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags) // 启动 Service PendingIntent.Service(Context context, int requestCode, Intent intent, int flags) // 启动 BroadcastReceiver(发送广播) PendingIntent.getBroadcast(Context context, int requestCode, Intent intent, int flags)
创建 PendingIntent 后,就可以将 PendingIntent 发送给其他应用,例如发送到系统通知消息:
示例程序
// 通知构造器 NotificationManagerCompat compat = NotificationManagerCompat.from(context); NotificationCompat.Builder builder = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { builder = new NotificationCompat.Builder(context, CHANNEL_ID); } else { builder = new NotificationCompat.Builder(context); } ... // 设置 PendingIntent builder.setContentIntent(pendingIntent); // 构造通知 Notification notification = builder.build() // 发送通知 compat.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
简单说明下创建 PendingIntent 的 4 个参数:
创建 PendingIntent 时有一个容易犯错的地方需要注意:重复调用 PendingIntent.getActivity() 等创建方法不一定会返回新的对象,系统会基于两个要素判断是否需要返回相同的 PendingIntent:
上面提到 PendingIntent 是 Intent 的嵌套类,那么在消费 PendingIntent 时是否可以从中取出嵌套的 Intent 再执行 startActivity 之类的方法呢?NO!消费 PendingIntent 的方法只能使用 PendingIntent#send() 相关重载方法。例如:
PendingIntent.java
public void send() throws CanceledException { send(null, 0, null, null, null, null, null); } public void send(Context context, int code, @Nullable Intent intent) throws CanceledException { send(context, code, intent, null, null, null, null); }
关于 send() 内部的实现原理,我们在下一节原理分析中再说。
调用 PendingIntent#cancel() 方法可以取消已经创建的 PendingIntent,该方法将从系统中移除已经注册的 PendingIntent(事实上,是移除 IIntentSender
)。如果后续继续消费这个已经被取消的 PendingIntent,将抛出 CanceledException 异常。
PendingIntent.java
private final IIntentSender mTarget; public void cancel() { ActivityManager.getService().cancelIntentSender(mTarget); }
PendingIntent 可变性是一种对外部应用消费行为的约束机制,通过标记位 FLAG_MUTABLE
和 FLAG_IMMUTABLE
控制 PendingIntent 可变或不可变。例如:
示例程序
// 创建可变 PendingIntent val pendingIntent = PendingIntent.getActivity(applicationContext, NOTIFICATION_REQUEST_CODE, intent, PendingIntent.FLAG_MUTABLE) // 创建不可变 PendingIntent val pendingIntent = PendingIntent.getActivity(applicationContext, NOTIFICATION_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE)
那么,可变性意味着什么呢?可变性意味着在消费 PendingIntent 时,可以针对其中包装的 Intent 进行修改,即使用 PendingIntent#send(Context, int, Intent) 进行修改。需要注意的是,这里的 Intent 参数并不会完全替换 PendingIntent 中包装的 Intent,而是将修改的信息填充到原有的 Intent 上。
源码摘要
// send() 内部通过 Intent#fillIn() 修改 Intent,而不是替换 Intent // PendingIntent#send() 最终执行到: int changes = finalIntent.fillIn(intent, key.flags);
例如,以下为修改可变 PendingIntent 示例:
示例程序
val intentWithExtrasToFill = Intent().apply { putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage) } mutablePendingIntent.send(applicationContext, PENDING_INTENT_CODE, intentWithExtrasToFill) // 至此,PendingIntent 内部包装的 Intent 将持有 EXTRA_CUSTOMER_MESSAGE 信息
另外,PendingIntent 可变性的注意事项:
修改示例
// 创建不可变 PendingIntent val pendingIntent = PendingIntent.getActivity(applicationContext, NOTIFICATION_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE) // 在当前应用修改不可变 PendingIntent,需要使用 PendingIntent.FLAG_UPDATE_CURRENT 标记位 val updatedPendingIntent = PendingIntent.getActivity(applicationContext, NOTIFICATION_REQUEST_CODE, anotherIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
现在,我们回过头再总结一下 PendingIntent 的 flags 标记位:
创建 PendingIntent 需要使用特定的静态方法,内部会通过 Binder 通信将 PendingIntent 意图注册到 AMS 系统服务进程中,并获得一个 Binder 对象 IIntentSender
。关键源码摘要如下:
PendingIntent.java
private final IIntentSender mTarget; // 此处运行在应用进程 public static PendingIntent getActivity(Context context, int requestCode, Intent intent, @Flags int flags) { return getActivity(context, requestCode, intent, flags, null); } public static PendingIntent getActivity(Context context, int requestCode, @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded(context.getContentResolver()) : null; intent.migrateExtraStreamToClipData(context); intent.prepareToLeaveProcess(context); // 通过 Binder 通信注册 Intent,得到 IIntentSender IIntentSender target = ActivityManager.getService().getIntentSenderWithFeature( ActivityManager.INTENT_SENDER_ACTIVITY, packageName, context.getAttributionTag(), null, null, requestCode, new Intent[] { intent }, resolvedType != null ? new String[] { resolvedType } : null, // 注意这个参数,使用当前应用的 UserId flags, options, context.getUserId()); return new PendingIntent(target); }
ActivityManagerService.java
// 此处运行在 AMS 系统服务进程 public IIntentSender getIntentSenderWithFeature(int type, String packageName, String featureId, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions, int userId) { ... int callingUid = Binder.getCallingUid(); return mPendingIntentController.getIntentSender(type, packageName, featureId, callingUid /*调用应用进程*/, userId /*原始应用进程*/, token, resultWho, requestCode, intents, resolvedTypes, flags, bOptions); }
PendingIntentController.java
// 存储已注册的 pendingIntent 记录 final HashMap<PendingIntentRecord.Key, WeakReference<PendingIntentRecord>> mIntentSenderRecords = new HashMap<>(); // 此处运行在 AMS 系统服务进程 public PendingIntentRecord getIntentSender(int type, String packageName, @Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) { // 构建 PendingIntent 的 Key PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,token, resultWho, requestCode, intents, resolvedTypes, flags, SafeActivityOptions.fromBundle(bOptions), userId); WeakReference<PendingIntentRecord> ref = mIntentSenderRecords.get(key); // 此处处理以下标记位的逻辑 // FLAG_NO_CREATE // FLAG_CANCEL_CURRENT // FLAG_UPDATE_CURRENT if(ref != null) { return ref; } rec = new PendingIntentRecord(this, key, callingUid); mIntentSenderRecords.put(key, rec.ref); return rec; }
PendingIntentRecord.java
public final class PendingIntentRecord extends IIntentSender.Stub { final static class Key { // 关键参数:创建进程的 UserId final int userId; Key(int _t, String _p, ..., int _userId) { ... userId = _userId; } public boolean equals(Object otherObj) { ... // 要素 1 - requestCode 源码体现 if (requestCode != other.requestCode) { return false; } // 要素 2 - Intent 源码体现 if (requestIntent != other.requestIntent) { if (requestIntent != null) { if (!requestIntent.filterEquals(other.requestIntent)) { return false; } } else if (other.requestIntent != null) { return false; } } } } }
至此,PendingIntent 就在系统进程中以 PendingIntentRecord 记录的形式存在,相当于 PendingIntent 是存在于比当前应用更长生命周期的系统进程中。这就是应用进程退出后,依然不影响消费 PendingIntent 的原因。
消费 PendingIntent 需要使用 PendingIntent#send() 方法,内部会将创建 PendingIntent 时获得的 Binder 对象 IIntentSender 发送给 AMS 服务,用于执行最终的 Intent 操作。关键源码摘要如下:
PendingIntent.java
private final IIntentSender mTarget; // 此处运行在应用进程 public void send(Context context, int code, @Nullable Intent intent, ...) throws CanceledException { if (sendAndReturnResult(context, code, intent, onFinished, handler, requiredPermission,options) < 0) { throw new CanceledException(); } } public int sendAndReturnResult(Context context, int code, @Nullable Intent intent, ...) throws CanceledException { // 通过 Binder 通信执行 IIntentSender return ActivityManager.getService().sendIntentSender(mTarget, mWhitelistToken, code, intent, resolvedType, ...); }
ActivityManagerService.java
// 此处运行在 AMS 系统服务进程 @Override public int sendIntentSender(IIntentSender target, IBinder whitelistToken, int code, Intent intent, String resolvedType, ...) { if (target instanceof PendingIntentRecord) { return ((PendingIntentRecord)target).sendWithResult(code, intent, resolvedType, ...); }else { ... } }
PendingIntentRecord.java
// 此处运行在 AMS 系统服务进程 public int sendInner(int code, Intent intent, String resolvedType, ...) { // 此处处理以下标记位的逻辑 // FLAG_ONE_SHOT // FLAG_MUTABLE // FLAG_IMMUTABLE // FLAG_ONE_SHOT 标记会移除 PendingIntentController 存储的记录 if ((key.flags & PendingIntent.FLAG_ONE_SHOT) != 0) { controller.cancelIntentSender(this, true); } int res = START_SUCCESS; // 关键参数:创建进程的 UserId int userId = key.userId; switch (key.type) { case ActivityManager.INTENT_SENDER_ACTIVITY: res = controller.mAtmInternal.startActivitiesInPackage( uid /*关键参数*/, callingPid, callingUid, key.packageName, key.featureId, allIntents, allResolvedTypes, resultTo, mergedOptions, userId, false /* validateIncomingUser */, this /* originatingPendingIntent */, mAllowBgActivityStartsForActivitySender.contains(whitelistToken)); break; case ActivityManager.INTENT_SENDER_ACTIVITY_RESULT: ... break; case ActivityManager.INTENT_SENDER_BROADCAST: ... break; case ActivityManager.INTENT_SENDER_SERVICE: case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE: ... break; } return res; }
ActivityTaskManagerInternal.java
public abstract class ActivityTaskManagerInternal { public abstract int startActivityInPackage(int uid, int realCallingPid, int realCallingUid, ...); }
ActivityTaskManagerInternal 是一个抽象类,小彭没有找到其最终的实现类,有大佬知道的话请在评论区告诉我。
至此,就完成执行 PendingIntent 中延迟操作的目的。 那么,为什么在当前进程执行,还会以另一个进程(PendingIntent 的创建进程) 的身份执行呢,关键在于使用了保存在 PendingIntentRecord 记录中的 userId,这与我们通过常规的 Activity#startActivityAsUser() 是类似的。
Activity.java
@Override public void startActivityAsUser(Intent intent, UserHandle user) { startActivityAsUser(intent, null, user); }
到这里,PendingIntent 的内容就讲完了,相信你对 PendingIntent 的理解已经超过绝大部分同学,你认同吗?关注我,带你了解更多,我们下次见。
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!