先把结论放在前面:Intent 是"通信信封",IntentFilter 是"收件人声明的筛选规则",系统靠 action、category、data 三组匹配决定是否投递。 分水岭在于两件事能不能讲清:① 隐式 Intent 必须至少匹配一个 category,且 CATEGORY_DEFAULT 是系统隐式加上的;② PendingIntent 是"待执行的 Intent",它的身份靠 requestCode + Intent.filterEquals 决定,而不是靠引用相等。
Intent 与 IntentFilter 面试官最爱问三个层次:是什么、为什么、怎么用。这篇按这个顺序拆开讲。
匹配规则:三组条件的实际语义
系统匹配 Intent 与 IntentFilter 的规则,看起来简单,实际有三个反直觉点。
第一,action 要"有交集",category 要"被包含"。 具体规则是:Intent 的 action 集合与 Filter 的 action 集合有任一相同即算 action 匹配;Intent 的 category 集合必须包含 Filter 声明的每个 category。而 startActivity 调用时系统会隐式给 Intent 加上 CATEGORY_DEFAULT,所以 Filter 必须声明 DEFAULT 才能被启动:
<activity android:name=".DetailActivity" android:exported="true">
<intent-filter>
<action android:name="com.demo.action.VIEW_DETAIL" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="demo" android:host="detail" />
</intent-filter>
</activity>
val intent = Intent("com.demo.action.VIEW_DETAIL").apply {
data = Uri.parse("demo://detail/42")
}
startActivity(intent)
这就是"隐式跳转没反应"最常见的原因——漏了 DEFAULT category。
第二,data 的匹配靠 URI 的三要素。 scheme + host + path(pathPrefix/pathPattern)必须都匹配,且 Filter 中声明了 scheme 就要求 Intent 的 data 有对应 scheme。注意 data 与 action 的关系是"与"不是"或":如果只写 action 不写 data,Intent 带了 data 也能匹配上;如果只写 data 不写 action,Intent 的 action 为空反而能匹配。要写 action 就顺手写全 data,别指望"或者"关系。
第三,exported 决定外部能否看到。 Android 12 起所有带 intent-filter 的组件必须显式声明 exported,不声明直接安装失败。这是"应用升级后装不上"的常见原因。
最常见的坑是
PendingIntent 用隐式 Intent 且未设 FLAG_IMMUTABLE,在高版本直接异常或被劫持。
Android 12 起,PendingIntent 必须显式指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE,否则抛 IllegalStateException。必须用 FLAG_IMMUTABLE(除非确实需要填充可变数据,比如通知的 RemoteViews),因为不可变能防止别的应用拿到 PendingIntent 后篡改内容——这正是 FLAG_MUTABLE 曾经的漏洞来源(通知劫持攻击)。
// 通知的 PendingIntent
val pi = PendingIntent.getActivity(
context, 0,
Intent(context, DetailActivity::class.java).apply {
putExtra(EXTRA_ID, id)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // 必须加
)
// 多个通知要能区分,requestCode 必须不同
PendingIntent.getActivity(context, id, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
requestCode 这一条也要讲:PendingIntent 的同一性判断是 requestCode + Intent 的 filterEquals(忽略 extras 与其它易变属性)。所以两个 PendingIntent 如果 requestCode 相同且 Intent 过滤后相等,系统会返回同一个实例并把 extras 合并进去,表现为"点了第一个通知,第二个也跳了"。解决就是按业务维度给不同的 requestCode。
还有一个更隐蔽的坑:用到 Intent 与 IntentFilter 的代码旁边要写明适用前提,review 时才有据可依。三条检查项:① 显式 Intent 优先(内部跳转、传敏感数据),隐式只用于"允许第三方接入"的入口;② 所有 intent-filter 的组件显式声明 exported,值由是否对外决定;③ 隐式 Intent 的风险是"可能被恶意应用截获"——传密钥、用户隐私时要用显式 Intent,或至少检查 resolveActivity 之后仍用显式方式启动。
还有两个细节值得单独提,因为它们直接对应线上事故。
第一,setPackage 与 setComponent 的区别。 setPackage 把 Intent 限定在指定包内的所有匹配组件上(包内可能多个组件都匹配),setComponent 直接指定唯一组件(等价于显式 Intent)。做应用内跳转时 setComponent 更精确,做"让包内有能力处理的应用都能接"时 setPackage 更合适。过去很多崩溃来自"隐式 Intent 匹配到 0 个组件"(ActivityNotFoundException),现在 Android 10+ 的包可见性限制让这个问题更容易发生——发之前先 resolveActivity 判空并给兜底,是当下应有的写法。
第二,Intent 的 flags 里最值得记的三个。 ① FLAG_ACTIVITY_NEW_TASK——从 Application 上下文启动 Activity 时必须加,否则崩;② FLAG_ACTIVITY_CLEAR_TOP——目标已在栈中时清掉它上面的实例并复用(onNewIntent 会触发);③ FLAG_ACTIVITY_REORDER_TO_FRONT——只把目标提到前面不销毁中间页。CLEAR_TOP 与 SINGLE_TOP 的组合是"复用已有页面并刷新数据"的标准做法,但要注意复用时走的是 onNewIntent,参数要用 setIntent 更新。
三条高频追问
问:显式与隐式 Intent 怎么选?
判断标准是"是否希望被别的应用响应"。应用内跳转、跨组件传数据、需要携带敏感信息 → 显式 Intent(安全、行为可预测)。提供对外入口让其他应用调用(分享、支付、深链)→ 隐式 Intent,但要配 exported 与权限。实践建议是"默认显式,只在明确的对外入口用隐式"。
问:action 字符串的命名规范是什么?
惯例是 包名.大写动作名,如 com.demo.action.VIEW_DETAIL。这样能避免不同应用之间的 action 撞名。第三方接入的 action 还要配自定义 <permission> 保护,否则任何应用都能发。
问:怎么把参数安全地传过 Intent?
三条。① 大数据别进 extras(Binder 1MB 限制),传 ID 让接收方自己查;② 敏感数据不进 extras——Intent 的 extras 会进 PendingIntent 的 PendingResult,FLAG_MUTABLE 时可被改,即便不可变也可能被别的方式读;③ 复杂对象用 Parcelable(比 Serializable 快)且别在同一个进程内也用 Parcelable,直接传对象引用更快。
问:Deep Link 怎么实现?
本质是"隐式 Intent + data 里的 scheme/host/path"。系统收到点击后会按 App Links(android:autoVerify="true")或 Deep Links(需要用户手动确认)处理。必须处理 onNewIntent(launchMode="singleTask" 或 singleTop 时不会新建实例),否则第二次点击拿不到新数据:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // 关键:用新 Intent 替换旧的
handleDeepLink(intent.data)
}
落到项目里怎么做
一条能直接落地的规则:应用内跳转一律显式 Intent;intent-filter 只用于对外入口,且必须声明 exported 与权限。 这一条能同时解决安全与合规两件事。
配套实践三条:① 对外 action 集中成常量文件,避免拼写错误导致"组件不响应";② PendingIntent 统一封装一个 pi() 方法,把两个 flag 固化,杜绝漏加;③ Deep Link 链接生成走统一入口(带 UTM 与统一路由表),便于埋点与降级。
给正在准备面试的你
这题的分水岭在于"知不知道匹配规则的反直觉之处"。推荐这条答题线:先讲 action 有交集 / category 被包含 / data 三要素匹配 → 重点点出 隐式 Intent 必须匹配 CATEGORY_DEFAULT(这是"没反应"的头号原因)→ 讲 PendingIntent 的同一性由 requestCode + filterEquals 决定、extras 会合并 → 落到 FLAG_IMMUTABLE 必加与安全边界 → 补 Deep Link 的 onNewIntent 收尾。能主动说出"漏了 DEFAULT 就匹配不上"并解释系统为什么加它,答案就有分水岭了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:ContentProvider:跨进程数据共享的标准入口
下一篇预告:AndroidManifest-深度解析:组件声明的门道
有任何问题欢迎在评论区留言交流。