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 代码不一定最短,但在版本升级、厂商差异和安全审查中最稳。

扩展阅读

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui