Intent 显式、隐式与 PendingIntent-《Android深水区(五)》

Intent 显式、隐式与 PendingIntent

前言

Intent 不是"页面跳转参数"这么简单。真实项目里,很多线上问题都和 Intent 边界有关:

  • 从通知点击进入详情页,偶发打开旧订单。
  • 分享文本时,本机能弹出选择器,线上某些机型却没有可用应用。
  • Android 12 以后安装失败,提示带 intent-filter 的组件缺少 android:exported
  • 第三方 SDK 在 Android 12+ 崩溃,日志里出现 FLAG_IMMUTABLEFLAG_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 基础语法,以及 UriBundleParcelable 的基本用法。

A020/A021 解决"Activity 什么时候活着、在哪个任务栈里"。本文继续回答:一个组件请求如何被描述、如何被系统解析、如何跨进程延迟执行。

核心概念

Intent 是请求,不是调用

普通函数调用的目标在编译期基本确定;Intent 更像一份运行时请求单。它描述"我想做什么",系统再根据请求内容、组件声明和安全策略决定能不能做、由谁做。

常用字段可以这样理解:

字段 作用 典型例子 参与隐式匹配
component 明确目标组件 com.example.DetailActivity 否,显式目标优先
package 限定目标包 setPackage("com.example") 约束解析范围
action 想执行的动作 ACTION_VIEWACTION_SEND
data/type 操作的数据和 MIME https://...text/plain
category 场景分类 CATEGORY_DEFAULTCATEGORY_BROWSABLE
extras 附加业务参数 orderIdsource
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_IMMUTABLEFLAG_MUTABLE

默认策略是优先不可变:除非远端必须填充 Intent 的空字段,例如通知内联回复、气泡或某些系统回调,否则使用 FLAG_IMMUTABLE

整体架构

Intent 机制横跨应用进程、PackageManager、ActivityTaskManager、ActivityManager 和目标组件。可以先看整体边界:

flowchart TD App[&#34;调用方 App&#34;] IntentObj[&#34;Intent 对象&#34;] PM[&#34;PackageManager\n解析 action/data/category&#34;] ATM[&#34;ActivityTaskManager\nActivity 启动与任务栈&#34;] AMS[&#34;ActivityManager\nBroadcast/Service/PendingIntent&#34;] Target[&#34;目标组件&#34;] PI[&#34;PendingIntentRecord\n系统保存的授权令牌&#34;] App --> IntentObj IntentObj -->|显式 Activity| ATM IntentObj -->|隐式查询| PM PM --> ATM ATM --> Target IntentObj -->|Service/Broadcast| AMS AMS --> Target App -->|创建| PI PI -->|send 后按类型分发| ATM PI -->|send 后按类型分发| AMS

普通 Intent 是调用方即时发起;PendingIntent 则先由创建者交给系统保存,之后由通知栏、闹钟、桌面组件或其他进程触发 send()。两者都可能启动同一个组件,但授权模型不同。

工作流程

显式 Activity 启动流程

sequenceDiagram participant App as App Process participant ATMS as ActivityTaskManager participant Starter as ActivityStarter participant Target as Target Activity App->>ATMS: startActivity(explicit Intent) ATMS->>Starter: executeRequest Starter->>Starter: resolveActivity / task policy Starter-->>App: launch accepted or error ATMS->>Target: schedule launch/resume

对于显式 Activity,系统不需要用 intent filter 选择候选应用,但仍会检查组件存在性、导出状态、权限、用户、任务栈和后台启动策略。也就是说,显式 Intent 只跳过"谁能处理"这一步,不跳过系统安全和任务管理。

隐式 Intent 解析流程

隐式 Intent 的解析可以简化为四步:

  1. 调用方构造 action、data/type、category。
  2. 系统在调用方可见的包范围内查询候选组件。
  3. 每个候选组件的 intent filter 必须通过 action、data/type、category 三类匹配。
  4. 如果多个 Activity 匹配,系统可能显示选择器或使用默认处理器。
flowchart TD I[&#34;隐式 Intent&#34;] V[&#34;Package Visibility\n可见包范围&#34;] A[&#34;Action 匹配&#34;] D[&#34;Data/Type 匹配&#34;] C[&#34;Category 匹配&#34;] R[&#34;ResolveInfo 候选结果&#34;] Ch[&#34;Chooser / 默认应用&#34;] I --> V --> A --> D --> C --> R --> Ch

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_CURRENTFLAG_CANCEL_CURRENT 更新或替换。

sequenceDiagram participant Creator as 创建者 App participant AMS as ActivityManager participant Record as PendingIntentRecord participant Sender as 持有者/系统 UI participant Target as 目标组件 Creator->>AMS: getActivity/getBroadcast/getService AMS->>Record: 创建或复用 Key AMS-->>Creator: PendingIntent token Creator-->>Sender: 交出 token Sender->>Record: send(fillIn Intent?) Record->>Record: 检查 canceled/oneShot/immutable Record->>Target: 按 type 启动 Activity/Broadcast/Service

源码中 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() 的主线可以概括为:

  1. 如果记录已取消,返回 START_CANCELED
  2. 如果是 FLAG_ONE_SHOT,发送后取消。
  3. 从创建时的 requestIntent 复制出 finalIntent
  4. 如果不是 immutable,并且发送方传入了 fill-in Intent,则调用 finalIntent.fillIn(...)
  5. 合并 ActivityOptions 与后台启动权限信息。
  6. 按 type 分发到 Activity、Broadcast、Service 或 ForegroundService。

这段源码把 PendingIntent 的设计讲得很清楚:创建者定义基础操作,发送者在允许范围内触发或补齐,系统负责用创建者身份和用户上下文执行。

ActivityStarter 与后台启动限制

源码路径:

  • frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
  • frameworks/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_CURRENTFLAG_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 是不同场景,后者应重新设计为通知、前台服务或延迟到用户回到应用后处理。

面试考点

基础题

  1. Intent 的 action、data、category、extras 各自作用是什么?
  2. 显式 Intent 和隐式 Intent 的区别是什么?
  3. 为什么隐式 Activity 的 intent-filter 通常要声明 CATEGORY_DEFAULT
  4. android:exported 和 intent-filter 有什么关系?
  5. setPackage()setComponent() 的区别是什么?

进阶题

  1. 为什么两个 PendingIntent 只有 extra 不同时可能被系统复用?
  2. FLAG_UPDATE_CURRENTFLAG_CANCEL_CURRENTFLAG_ONE_SHOT 分别适合什么场景?
  3. Android 12+ 为什么要求 PendingIntent 声明 mutability?
  4. Android 11 package visibility 会如何影响 queryIntentActivities()
  5. 接收外部分享 Intent 时应该做哪些安全校验?

源码题

  1. Intent.filterEquals() 与 extras 的关系是什么?
  2. PendingIntentRecord.Key 由哪些字段决定?
  3. PendingIntentRecord.sendInner() 如何处理 immutable 与 fill-in Intent?
  4. PendingIntent 发送 Activity、Broadcast、Service 时分别走向哪些系统入口?
  5. 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 代码不一定最短,但在版本升级、厂商差异和安全审查中最稳。

扩展阅读

相关推荐
Kapaseker3 小时前
一文吃透 Kotlin 集合操作符
android·kotlin
三少爷的鞋5 小时前
Main-safe:现代Android 架构真正的分水岭
android
沐怡旸13 小时前
深入解析 Android Performance Analyzer (APA) 底层架构与技术原理
android
李斯维20 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
plainGeekDev1 天前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户41659673693551 天前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev1 天前
onActivityResult → ActivityResult API
android·java·kotlin
随遇丿而安1 天前
第10周:Activity 基础功能与生命周期优化
android
alexhilton2 天前
Android车载OS中的Remote Compose
android·kotlin·android jetpack