第133篇Intent 与 IntentFilter:显式隐式跳转与匹配规则

简介: 本文深入解析Android中Intent与IntentFilter的核心机制:Intent是通信信封,IntentFilter是收件规则;重点厘清隐式Intent必须匹配CATEGORY_DEFAULT、PendingIntent同一性由requestCode+filterEquals决定等高频误区,并涵盖exported声明、FLAG_IMMUTABLE强制要求、Deep Link实现等实战要点。

先把结论放在前面: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-深度解析:组件声明的门道

有任何问题欢迎在评论区留言交流。

相关文章
|
2天前
|
缓存 编译器 API
第119篇 扩展属性与扩展伴生:工具方法的进阶形态
本节详解Kotlin扩展属性与伴生对象扩展:扩展属性无backing field,本质是静态getter/setter方法,每次访问均重计算;扩展伴生对象则为第三方类“添加静态方法”,语法简洁、语义清晰。二者均属编译期静态分派,不可覆盖,是检验Kotlin底层理解的关键点。
21 2
|
2天前
|
传感器 API Android开发
第121篇 Activity 生命周期全解:七个回调的成对关系
本文深入解析Activity生命周期的两大维度:视图生命周期(6个回调)与进程生命周期(状态保存/恢复),直击面试高频考点——“为何如此设计”及“进程被杀后如何恢复”。厘清`onSaveInstanceState`触发条件、Bundle大小限制、ViewModel与状态保存的边界,并给出工程最佳实践与避坑指南。
23 1
|
23小时前
|
缓存 前端开发 Java
第138篇自定义 View 进阶:onDraw 与 Paint 的进阶用法
本文深入解析自定义 View 进阶核心:Paint 三大能力(属性、Shader、Xfermode)、Canvas 变换与图层、属性动画驱动。强调效果实现原理而非死记 API,直击 onDraw 零分配、Xfermode 隔离、内存泄漏、RTL 支持等高频坑点,助你真正掌握“效果是怎么算出来的”。
18 0
|
23小时前
|
XML 安全 Java
第134篇AndroidManifest 深度解析:组件声明的门道
AndroidManifest 不是普通配置文件,而是应用的**组件注册表+权限申请表+系统启动契约**。核心含 `package`、`application`、`component`、`uses-permission` 四部分;关键在**清单合并规则**:权限取并集、`application` 属性按优先级覆盖、组件安全属性需显式声明。掌握 `tools:` 标记干预与 `merged_manifest` 排查,方为真深度理解。
24 0
|
2天前
|
Android开发
第124篇Fragment 生命周期:与 Activity 的联动陷阱
Fragment生命周期含三套独立流程:实例、视图、上下文。关键分界是`onCreateView`至`onDestroyView`为视图生命周期,此时Fragment实例仍存活;`onDestroy`后实例才真正销毁。常见坑如重复弹窗、内存泄漏,根源在于混淆三者时机。推荐实践:视图相关操作绑定`viewLifecycleOwner`,数据交由`ViewModel`管理。
17 0
|
2天前
|
编译器 测试技术 调度
第110篇 Flow 与 RxJava 对比:响应式迁移指南
本文深度对比 Flow 与 RxJava 的设计哲学、背压机制、Subject/SharedFlow 语义差异及迁移陷阱,直击面试高频考点。指出二者“问题重叠、取向相反”:Flow 借协程挂起实现天然背压,Rx 依赖显式策略;强调 `PublishSubject ≠ SharedFlow(replay=0)` 等关键误区,附对照表与实战代码。
19 0
|
2天前
|
缓存 Java API
第107篇 协程与线程性能对比:为什么更轻
本文深入剖析协程性能误区:协程“轻量”仅指创建/切换成本低(纳秒级、用户态),而非单任务执行更快;它优化的是高并发I/O场景的资源占用,而非CPU算力。关键区分协程(挂起单元)与线程(调度单位),并指出三大常见坑:误信协程能加速阻塞调用、盲目用于CPU密集型任务、过度细粒度切换。
18 0
|
2天前
|
JSON 监控 Java
第081篇 inline 内联函数:高阶函数零开销的秘密
`inline` 本质是编译期函数体展开,消除 Lambda 分配与虚调用,但带来字节码膨胀与方法数增长。需权衡收益:仅对≤10行、纯转发、热路径的薄包装使用。`noinline`(传/存函数)、`crossinline`(禁非局部返回)、`reified`(保留泛型运行时类型)各解特定语义问题。Android 中须警惕 R8 与 dex 65536 限制。
15 0
|
2天前
|
缓存 安全 Java
第031篇 ConcurrentHashMap:从分段锁到 CAS 加 synchronized
ConcurrentHashMap(JDK8)通过CAS初始化空桶、桶头节点加synchronized锁实现细粒度并发写,get无锁依赖volatile可见性;size采用baseCount+CounterCell分片计数;禁止null键值,复合操作须用compute/merge等原子方法——兼顾高性能与线程安全。
25 0
|
2天前
|
安全 Java API
第027篇 ArrayList 源码与扩容机制:1.5 倍增长的细节
ArrayList面试高频题:JDK8中无参构造不分配数组,首次add才扩容至10;后续按1.5倍增长,不足时直取所需容量,上限为Integer.MAX_VALUE-8。扩容本质是Arrays.copyOf整段拷贝。关键要懂原理、避坑(如预估容量、非线程安全、subList陷阱)并结合工程实践。
27 0

热门文章

最新文章