Intent 显式、隐式与 PendingIntent
前言
Intent 不是"页面跳转参数"这么简单。真实项目里,很多线上问题都和 Intent 边界有关:
- 从通知点击进入详情页,偶发打开旧订单。
- 分享文本时,本机能弹出选择器,线上某些机型却没有可用应用。
- Android 12 以后安装失败,提示带
intent-filter的组件缺少android:exported。 - 第三方 SDK 在 Android 12+ 崩溃,日志里出现
FLAG_IMMUTABLE或FLAG_MUTABLE。 - 用隐式 Intent 启动自家 Service,结果被系统限制,或者被错误组件处理。
- 从后台发起 PendingIntent,希望拉起页面,但 Android 14/15 上被后台启动限制挡住。
这些现象背后其实是三件事:
- Intent 是一次操作请求的结构化描述。
- 隐式 Intent 需要系统根据 action、data、category、package visibility 和 intent filter 做解析。
- PendingIntent 是把一次未来操作交给系统或另一个进程执行的授权令牌,它的安全边界比普通 Intent 更严格。
本文基于 Android Developers 官方 Intent / PendingIntent / Package Visibility / Activity BAL 文档,以及 AOSP frameworks/base 主分支源码复查。版本相关结论以 2026-06-29 的公开文档和源码为准;不同 Android 发行版或厂商系统可能在策略细节上有差异,安全策略以目标 SDK、设备系统版本和兼容性开关共同决定。
前置知识
建议先掌握:
- A020:Activity 生命周期完整解析。
- A021:Activity 启动模式与任务栈。
- A022:Fragment 生命周期与状态恢复。
- Android 四大组件、Manifest、Task 和进程的基础概念。
- Kotlin 基础语法,以及
Uri、Bundle、Parcelable的基本用法。
A020/A021 解决"Activity 什么时候活着、在哪个任务栈里"。本文继续回答:一个组件请求如何被描述、如何被系统解析、如何跨进程延迟执行。
核心概念
Intent 是请求,不是调用
普通函数调用的目标在编译期基本确定;Intent 更像一份运行时请求单。它描述"我想做什么",系统再根据请求内容、组件声明和安全策略决定能不能做、由谁做。
常用字段可以这样理解:
| 字段 | 作用 | 典型例子 | 参与隐式匹配 |
|---|---|---|---|
| component | 明确目标组件 | com.example.DetailActivity |
否,显式目标优先 |
| package | 限定目标包 | setPackage("com.example") |
约束解析范围 |
| action | 想执行的动作 | ACTION_VIEW、ACTION_SEND |
是 |
| data/type | 操作的数据和 MIME | https://...、text/plain |
是 |
| category | 场景分类 | CATEGORY_DEFAULT、CATEGORY_BROWSABLE |
是 |
| extras | 附加业务参数 | orderId、source |
否 |
| flags | 启动/传递策略 | FLAG_ACTIVITY_NEW_TASK |
影响启动行为 |
extras 不参与 intent filter 匹配。这一点很重要:不要指望通过 extra 区分两个隐式目标;需要被系统解析的条件应放在 action、data、type、category 或明确的 component/package 上。
显式 Intent:目标已经确定
显式 Intent 指定了组件名,常用于应用内部页面跳转、启动自家 Service、发送自家 Broadcast。
kotlin
fun openOrderDetail(context: Context, orderId: String) {
val intent = Intent(context, OrderDetailActivity::class.java)
.putExtra(OrderDetailActivity.EXTRA_ORDER_ID, orderId)
context.startActivity(intent)
}
显式 Intent 的优点是可预测、安全边界清晰、解析成本低。官方文档也提醒:启动自家 Service 时应使用显式 Intent,避免意外运行其他应用的 Service。
但显式不等于绝对安全。如果目标组件 exported=true,其他应用同样可以构造显式 Intent 访问它。因此安全控制应依赖 android:exported、权限、参数校验和业务鉴权,而不是只依赖"别人不知道组件名"。
隐式 Intent:能力由系统选择
隐式 Intent 不指定具体组件,而是声明 action、data、type、category,让系统查找可处理的组件。
kotlin
fun sharePlainText(context: Context, text: String) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
val chooser = Intent.createChooser(sendIntent, "分享到")
context.startActivity(chooser)
}
接收方需要在 Manifest 中声明 intent-filter:
xml
<activity
android:name=".ShareTextActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
startActivity() 解析隐式 Intent 时会按 CATEGORY_DEFAULT 处理;接收 Activity 如果没有在 filter 中声明 android.intent.category.DEFAULT,通常不会被普通隐式启动解析到。Android 12 以后,只要 Activity、Service 或 Receiver 声明了 intent-filter,就必须显式设置 android:exported,否则应用无法安装到 Android 12+ 设备。
PendingIntent:未来由别人代你执行
PendingIntent 常见于通知、AlarmManager、AppWidget、快捷方式和跨进程回调。它不是"延迟版 Intent",而是系统保存的一张授权票据:持有者以后可以让系统以创建者的身份执行指定操作。
常见创建方式:
kotlin
fun orderNotificationPendingIntent(
context: Context,
orderId: String,
): PendingIntent {
val intent = Intent(context, OrderDetailActivity::class.java).apply {
putExtra(OrderDetailActivity.EXTRA_ORDER_ID, orderId)
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(
context,
orderId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
这里有三个关键点:
- 尽量使用显式 Intent,避免授权票据被解析到非预期组件。
requestCode参与 PendingIntent 复用匹配,多个业务对象不能都写0。- Android 12+ 目标 SDK 创建 PendingIntent 时必须明确
FLAG_IMMUTABLE或FLAG_MUTABLE。
默认策略是优先不可变:除非远端必须填充 Intent 的空字段,例如通知内联回复、气泡或某些系统回调,否则使用 FLAG_IMMUTABLE。
整体架构
Intent 机制横跨应用进程、PackageManager、ActivityTaskManager、ActivityManager 和目标组件。可以先看整体边界:
普通 Intent 是调用方即时发起;PendingIntent 则先由创建者交给系统保存,之后由通知栏、闹钟、桌面组件或其他进程触发 send()。两者都可能启动同一个组件,但授权模型不同。
工作流程
显式 Activity 启动流程
对于显式 Activity,系统不需要用 intent filter 选择候选应用,但仍会检查组件存在性、导出状态、权限、用户、任务栈和后台启动策略。也就是说,显式 Intent 只跳过"谁能处理"这一步,不跳过系统安全和任务管理。
隐式 Intent 解析流程
隐式 Intent 的解析可以简化为四步:
- 调用方构造 action、data/type、category。
- 系统在调用方可见的包范围内查询候选组件。
- 每个候选组件的 intent filter 必须通过 action、data/type、category 三类匹配。
- 如果多个 Activity 匹配,系统可能显示选择器或使用默认处理器。
Android 11 起,面向 API 30+ 的应用查询其他应用时默认会被 package visibility 过滤。queryIntentActivities() 看不到某个应用,并不一定代表设备没有安装它;也可能是调用方没有在 <queries> 中声明交互需求,或者该包不属于自动可见范围。
例如,应用确实需要预检查地图应用时,可以声明可见性需求:
xml
<manifest ...>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
</queries>
</manifest>
这不是权限授权,而是查询可见性声明。真正启动时仍要经过 intent resolution、组件导出、权限和用户选择。
PendingIntent 创建与发送流程
PendingIntent 的重点在系统保存的 PendingIntentRecord。创建时,系统会根据 type、package、requestCode、Intent 的 filter 信息、flags、user 等生成 Key。后续创建"等价"的 PendingIntent 时,可能返回同一个系统记录,并根据 FLAG_UPDATE_CURRENT 或 FLAG_CANCEL_CURRENT 更新或替换。
源码中 PendingIntentRecord.sendInner() 会先复制创建时的 requestIntent,如果不是 immutable,再用发送方传入的 Intent 调用 fillIn() 补齐允许变更的字段;随后根据类型调用 startActivityInPackage()、broadcastIntentInPackage() 或 startServiceInPackage()。这也是为什么可变 PendingIntent 必须谨慎:它允许持有者影响最终执行的 Intent。
API 使用
1. 应用内页面跳转:显式 Intent + 明确参数契约
kotlin
class OrderDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val orderId = intent.getStringExtra(EXTRA_ORDER_ID)
require(!orderId.isNullOrBlank()) { "Missing order id" }
// 根据 orderId 加载页面,不信任外部传入的完整业务对象。
}
companion object {
const val EXTRA_ORDER_ID = "com.example.extra.ORDER_ID"
fun createIntent(context: Context, orderId: String): Intent =
Intent(context, OrderDetailActivity::class.java)
.putExtra(EXTRA_ORDER_ID, orderId)
}
}
建议把 Intent 构造入口放在目标页面附近,避免调用方散落字符串 key。即使 Activity 不导出,也要校验必需参数,因为进程恢复、测试代码或未来重构都可能构造出不完整 Intent。
2. 打开外部链接:隐式 Intent + resolve/chooser
kotlin
fun openPrivacyPolicy(activity: Activity, url: String) {
val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri)
val chooser = Intent.createChooser(intent, "选择浏览器")
if (intent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(chooser)
} else {
Toast.makeText(activity, "未找到可打开链接的应用", Toast.LENGTH_SHORT).show()
}
}
resolveActivity() 的结果会受 package visibility 影响。如果只是直接调用 startActivity(),系统仍可能打开用户可用的处理器;如果业务依赖预查询列表,就要在 API 30+ 下检查 <queries>。
3. 接收分享:intent-filter + DEFAULT + exported
xml
<activity
android:name=".ShareImportActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
接收外部 Intent 的 Activity 应把所有输入视为不可信:
kotlin
class ShareImportActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") {
finish()
return
}
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
?.take(MAX_SHARE_TEXT_LENGTH)
?.trim()
.orEmpty()
if (text.isBlank()) {
finish()
return
}
// 进入导入确认页,而不是直接执行敏感操作。
}
companion object {
private const val MAX_SHARE_TEXT_LENGTH = 20_000
}
}
不要把外部分享内容直接写入账号、支付、设备控制等敏感流程。正确做法是先进入确认页,展示来源和内容摘要,再由用户确认。
4. 通知点击:唯一 requestCode + immutable PendingIntent
kotlin
fun buildOrderNotification(
context: Context,
orderId: String,
): NotificationCompat.Builder {
val pendingIntent = PendingIntent.getActivity(
context,
orderId.hashCode(),
OrderDetailActivity.createIntent(context, orderId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(context, "orders")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("订单状态更新")
.setContentText("订单 $orderId 有新的处理进度")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
}
如果所有订单通知都使用 requestCode = 0,并且 Intent 的 filter 信息相同,系统可能复用同一个 PendingIntent。结果就是用户点击 A 通知却打开 B 订单。实践中可以用稳定业务 ID 生成 requestCode,或在 action/data 中放入可参与匹配的唯一值。
5. 内联回复:只有必要时使用 mutable
通知内联回复需要系统把用户输入填充进发送时的 Intent,这类场景需要可变 PendingIntent。
kotlin
fun replyPendingIntent(context: Context, conversationId: String): PendingIntent {
val intent = Intent(context, ReplyReceiver::class.java).apply {
action = ReplyReceiver.ACTION_REPLY
putExtra(ReplyReceiver.EXTRA_CONVERSATION_ID, conversationId)
}
return PendingIntent.getBroadcast(
context,
conversationId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)
}
即便使用 FLAG_MUTABLE,也应让 Intent 尽量显式,并在 Receiver 里校验 action、必需 extra、调用场景和业务权限。
源码分析
Intent:filter 匹配不看 extras
源码路径:
frameworks/base/core/java/android/content/Intent.java
Intent 本质上是一个可序列化、可跨 Binder 传递的数据容器。它的 filter 相关方法会关注 action、data、type、identifier、package、component、categories 等字段;业务 extras 主要用于目标组件消费,不参与普通 intent filter 匹配。
这解释了两个常见问题:
- 两个 PendingIntent 只有 extra 不同时,可能被系统认为是等价请求。
- 隐式 Intent 的接收者不能靠 extra 参与系统解析,必须用 action/data/type/category 表达能力边界。
PendingIntent:公开 API 是 token 包装
源码路径:
frameworks/base/core/java/android/app/PendingIntent.java
公开 API 提供 getActivity()、getBroadcast()、getService()、getForegroundService() 等入口。API 文档明确建议:出于安全原因,传给 PendingIntent 的 Intent 几乎总应是显式 Intent。
send() 系列方法允许发送方提供额外 Intent 数据,但文档也说明:如果创建时设置了 FLAG_IMMUTABLE,发送时传入的 Intent 参数会被忽略。这是不可变 PendingIntent 的核心价值:持有者只能触发创建者预先定义的操作,不能改写目标或数据。
PendingIntentRecord:Key 复用与 fillIn()
源码路径:
frameworks/base/services/core/java/com/android/server/am/PendingIntentRecord.java
PendingIntentRecord.Key 保存了 type、packageName、requestCode、requestIntent、resolvedType、flags、userId 等字段。equals() 中会比较 requestCode、flags、包名、类型和 requestIntent.filterEquals() 等信息。
这里的工程含义是:PendingIntent 不是按"整个 Intent 完全相等"复用,而是按 filter 语义和 key 字段复用。因此,只改 extras 并不能保证拿到新 PendingIntent。
sendInner() 的主线可以概括为:
- 如果记录已取消,返回
START_CANCELED。 - 如果是
FLAG_ONE_SHOT,发送后取消。 - 从创建时的
requestIntent复制出finalIntent。 - 如果不是 immutable,并且发送方传入了 fill-in Intent,则调用
finalIntent.fillIn(...)。 - 合并 ActivityOptions 与后台启动权限信息。
- 按 type 分发到 Activity、Broadcast、Service 或 ForegroundService。
这段源码把 PendingIntent 的设计讲得很清楚:创建者定义基础操作,发送者在允许范围内触发或补齐,系统负责用创建者身份和用户上下文执行。
ActivityStarter 与后台启动限制
源码路径:
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.javaframeworks/base/services/core/java/com/android/server/am/PendingIntentRecord.java
从 Android 10 开始,后台直接启动 Activity 已经受到严格限制;后续版本继续收紧 PendingIntent 相关授权。官方 Activity security 文档指出:
- 面向 Android 14/API 34+ 时,发送 PendingIntent 的应用默认不再自动授予自己的后台启动权限,除非显式 opt-in 或创建者已授予。
- 面向 Android 15/API 35+ 时,创建 PendingIntent 的应用默认不再把自己的后台启动权限授给发送者,需要显式 opt-in。
因此,通知点击、闹钟响铃、系统 UI 触发这类用户可见场景通常仍应走系统推荐入口;后台服务静默拉起 Activity 则不要依赖 PendingIntent 绕过限制。产品上需要用户注意力时,应优先使用通知、全屏 Intent 的合规场景、前台服务通知或应用内状态恢复。
ComputerEngine / PackageManager:可见性会改变查询结果
源码路径:
frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java
PackageManager 相关查询会经过包可见性过滤。Android Developers 的 Package Visibility 文档明确说明,面向 Android 11/API 30+ 的应用查询其他已安装应用时,系统默认过滤结果;这会影响 queryIntentActivities()、getPackageInfo()、getInstalledApplications() 等方法,也会影响和其他应用的显式交互可见性。
工程上不要把"查询不到处理器"简单解释成"设备没有这个应用"。如果你的功能必须提前枚举候选处理器,先检查 <queries>;如果只是普通用户动作,通常直接构造 Intent 并让系统处理失败结果更稳。
实战案例
案例一:通知点击打开错订单
问题代码:
kotlin
val pendingIntent = PendingIntent.getActivity(
context,
0,
OrderDetailActivity.createIntent(context, orderId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
多个订单通知的 requestCode 都是 0,Intent 的 component 相同,extra 不参与 filter 匹配。后创建的 PendingIntent 更新了旧记录,导致所有通知可能指向最后一个订单。
修复策略:
- 用
orderId.hashCode()或业务自增 ID 作为 requestCode。 - 或者给 Intent 设置唯一 data:
intent.data = Uri.parse("app://orders/$orderId")。 - 保留
FLAG_UPDATE_CURRENT,让同一订单的通知更新详情,而不是生成重复票据。
案例二:分享入口被外部构造恶意参数
接收分享的 Activity 常常 exported=true。如果它直接读取 extra 并执行导入、绑定或支付类操作,外部应用就可能构造 Intent 触发敏感行为。
更稳的设计:
- 接收 Activity 只做解析和确认,不直接执行敏感操作。
- 限制 MIME type、文本长度、URI scheme、文件大小。
- 读取
content://时使用ContentResolver,捕获异常,并避免长时间占用主线程。 - 对登录态、组织、设备权限重新鉴权。
案例三:Android 11+ 预查询分享应用为空
某些应用会先调用 queryIntentActivities() 判断是否显示"分享到某应用"。升级 targetSdk 到 30+ 后,查询结果可能变少。
处理方式:
- 如果只是分享文本,不要硬编码某个应用,直接使用
Intent.createChooser()。 - 如果确实要识别特定包或特定 scheme,在 Manifest 中声明
<queries>。 - 不要申请
QUERY_ALL_PACKAGES作为默认方案;Google Play 将已安装应用列表视为敏感数据,广泛查询需要有充分理由。
案例四:PendingIntent mutable 被滥用
危险写法:
kotlin
PendingIntent.getActivity(
context,
0,
Intent(), // 空 Intent
PendingIntent.FLAG_MUTABLE,
)
这相当于把大量决定权留给持有者。更稳的写法是:
kotlin
PendingIntent.getActivity(
context,
0,
Intent(context, SafeEntryActivity::class.java).apply {
action = SafeEntryActivity.ACTION_OPEN_FROM_NOTIFICATION
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
如果必须 mutable,也要让 component/action/data 尽可能固定,只允许系统填充确实需要的字段。
性能优化
Intent 本身通常不是性能瓶颈,但它常把慢操作引入主线程或跨进程边界。
1. 避免在 extras 中塞大对象
Intent 需要跨 Binder 传递,Bundle 过大可能带来序列化开销,甚至触发 TransactionTooLargeException。页面跳转建议传稳定 ID,目标页从 Repository、数据库或缓存加载数据。
kotlin
// 推荐:传 ID
intent.putExtra(EXTRA_ORDER_ID, orderId)
// 不推荐:传超大的完整对象、图片字节数组、长列表
2. 隐式查询不要放在高频 UI 路径
queryIntentActivities() 涉及 PackageManager 查询和可见性过滤,不适合在列表 item 绑定、动画帧或输入框实时变化中反复调用。可以在页面初始化或用户点击时查询,并缓存短生命周期结果。
3. content:// 读取放到后台线程
分享、文件选择、拍照返回经常带来 content:// URI。打开流、探测 MIME、读取 EXIF、拷贝文件都不应在主线程执行。
kotlin
suspend fun copySharedTextFile(
context: Context,
uri: Uri,
target: File,
) = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(uri).use { input ->
requireNotNull(input) { "Cannot open shared uri: $uri" }
target.outputStream().use { output ->
input.copyTo(output)
}
}
}
4. PendingIntent 复用要可控
不要为了"省对象"复用一个全局 requestCode。PendingIntent 由系统管理,正确性优先于微小分配成本。按业务对象设计 requestCode 和 data,反而能减少更新错乱、重复通知和用户误跳转。
5. 避免用隐式 Intent 启动自家后台组件
启动自家 Service、Receiver 或内部 Activity 应使用显式 Intent。隐式解析不仅多一步查询,也增加了安全和兼容性风险。
常见问题
显式 Intent 会不会检查 intent-filter?
普通显式启动不依赖 intent-filter 选择目标。官方文档也说明,显式 Intent 会被投递给目标组件,而不管该组件声明了哪些 intent filter。但系统仍会检查导出状态、权限、组件存在性、用户和后台启动策略。Android 15/16 的 Safer Intents 相关变化还在继续加强 intent 处理安全,跨应用显式启动应避免构造和目标 filter 明显不一致的请求。
setPackage() 算显式 Intent 吗?
严格说,setPackage() 限定的是包,不是具体组件。它可以把隐式解析范围限制在某个包内,但包内仍可能有多个组件匹配。需要唯一目标时使用 setClass()、setClassName() 或 setComponent()。
为什么隐式 Activity filter 要加 CATEGORY_DEFAULT?
普通 startActivity() 会按带有 CATEGORY_DEFAULT 的方式解析隐式 Intent。接收 Activity 的 filter 如果没有声明 DEFAULT,通常不会成为普通隐式启动候选。CATEGORY_BROWSABLE 是另一类场景,常用于浏览器或 App Links 从外部打开。
FLAG_UPDATE_CURRENT 和 FLAG_IMMUTABLE 冲突吗?
不冲突。FLAG_IMMUTABLE 限制的是发送方不能在 send() 时修改最终 Intent;FLAG_UPDATE_CURRENT 表示创建者再次获取等价 PendingIntent 时,可以更新系统中保存的 extras。创建者仍然可以更新自己的 PendingIntent 记录。
什么时候必须用 FLAG_MUTABLE?
只有持有者或系统必须在发送时填充字段时才用。例如通知内联回复需要系统写入用户输入,某些 bubbles、remote input、特定系统回调也可能需要 mutable。普通通知点击、闹钟回调、自家 Receiver 通常优先 immutable。
requestCode 真的有用吗?
有用。requestCode 是 PendingIntent Key 的一部分。多个业务对象如果共用 requestCode,且 Intent filter 信息相同,就可能复用同一个记录。通知、闹钟、桌面组件都应设计稳定且区分业务对象的 requestCode。
Android 11+ 查询不到应用,直接启动也会失败吗?
不一定。Package visibility 主要影响调用方能"看见"和"查询"哪些包。直接启动仍会经过系统解析和安全检查;但如果你的逻辑依赖 queryIntentActivities() 的结果来决定 UI,就需要用 <queries> 声明必要交互范围。
PendingIntent 能绕过后台启动限制吗?
不能把它当绕过手段。Android 14/15 对 PendingIntent 背景 Activity 启动授权做了更明确的 opt-in 约束。用户点击通知这类可见交互和后台静默拉起 Activity 是不同场景,后者应重新设计为通知、前台服务或延迟到用户回到应用后处理。
面试考点
基础题
- Intent 的 action、data、category、extras 各自作用是什么?
- 显式 Intent 和隐式 Intent 的区别是什么?
- 为什么隐式 Activity 的 intent-filter 通常要声明
CATEGORY_DEFAULT? android:exported和 intent-filter 有什么关系?setPackage()和setComponent()的区别是什么?
进阶题
- 为什么两个 PendingIntent 只有 extra 不同时可能被系统复用?
FLAG_UPDATE_CURRENT、FLAG_CANCEL_CURRENT、FLAG_ONE_SHOT分别适合什么场景?- Android 12+ 为什么要求 PendingIntent 声明 mutability?
- Android 11 package visibility 会如何影响
queryIntentActivities()? - 接收外部分享 Intent 时应该做哪些安全校验?
源码题
Intent.filterEquals()与 extras 的关系是什么?PendingIntentRecord.Key由哪些字段决定?PendingIntentRecord.sendInner()如何处理 immutable 与 fill-in Intent?- PendingIntent 发送 Activity、Broadcast、Service 时分别走向哪些系统入口?
- Android 14/15 的 PendingIntent 背景启动授权变化解决了什么安全问题?
系统设计题
设计一个"订单状态通知点击进入详情页"的方案,要求:
- 多个订单通知互不串单。
- 点击后复用已有详情页或回到正确任务栈。
- Android 12+ 不触发 PendingIntent mutability 崩溃。
- Android 14/15 上不依赖后台静默拉起页面。
- 外部应用不能伪造订单跳转绕过鉴权。
一个合格答案应包含:显式 Intent、稳定 requestCode 或 data、FLAG_IMMUTABLE、目标页参数校验、登录态/权限复核、合理 task flags,以及通知点击作为用户可见入口的说明。
总结
可以用三句话记住本文:
- Intent 是请求描述,显式 Intent 直接指定目标,隐式 Intent 依赖 action/data/category 与 intent filter 解析。
- PendingIntent 是系统保存的未来授权令牌,创建者身份、Key 复用、mutability 和 requestCode 都会影响最终行为。
- Android 11 以后 package visibility、Android 12 exported/mutability、Android 14/15 背景启动授权,让 Intent 相关代码越来越偏向"显式、最小授权、可验证输入"。
工程实践上,内部组件优先显式 Intent;外部能力用隐式 Intent + chooser;跨进程延迟触发用不可变 PendingIntent;所有外部输入都重新校验。这样写出来的 Intent 代码不一定最短,但在版本升级、厂商差异和安全审查中最稳。
扩展阅读
- Android Developers: Intents and intent filters, developer.android.com/guide/compo...
- Android Developers:
IntentAPI reference, developer.android.com/reference/a... - Android Developers:
PendingIntentAPI reference, developer.android.com/reference/a... - Android Developers: Pending intents security risks, developer.android.com/privacy-and...
- Android Developers: Package visibility filtering on Android, developer.android.com/training/pa...
- Android Developers: Activity security and background activity launch restrictions, developer.android.com/guide/compo...
- Android Developers: Android 15 behavior changes, developer.android.com/about/versi...
- Android Developers: Android 16 behavior changes, developer.android.com/about/versi...
- AOSP:
frameworks/base/core/java/android/content/Intent.java - AOSP:
frameworks/base/core/java/android/app/PendingIntent.java - AOSP:
frameworks/base/services/core/java/com/android/server/am/PendingIntentRecord.java - AOSP:
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java - AOSP:
frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java - 系列文章:A020 Activity 生命周期完整解析
- 系列文章:A021 Activity 启动模式与任务栈
- 系列文章:A022 Fragment 生命周期与状态恢复
- 下一篇:A024 Service、前台服务与后台限制