文章

Intent 与组件通信深度解析——跨平台开发者的 Android 原生交互指南

面试高频:显式 vs 隐式 Intent、startActivityForResult 为何被弃用、Activity Result API 怎么写、Deep Link 与 PendingIntent 可变性的坑。

Intent 与组件通信深度解析——跨平台开发者的 Android 原生交互指南

一句话概括

Intent 是 Android 组件之间、应用与应用之间的”消息信封”——启动页面、打开相机、分享文本、响应 Deep Link,背后都是它在封装「要做什么 + 带什么数据 + 找谁处理」。你写的 image_picker、Share.share、推送拉起,底层全是 Intent。

核心结论:显式 Intent 指名道姓(内部跳转),隐式 Intent 描述意图(跨应用调用);startActivityForResult 已废弃,统一用 Activity Result API;接收隐式 Intent 的 Activity 必须带 DEFAULT 分类;Android 12+ 的 PendingIntent 必须声明可变性。 这四句是面试常考点。

核心知识点

1. 显式 vs 隐式 Intent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 显式:明确指定目标类,用于应用内部跳转,不经系统匹配、最安全
val i = Intent(this, DetailActivity::class.java)
i.putExtra("id", 123)
startActivity(i)

// ✅ 隐式:只说"我要干啥",系统按 Intent Filter 找接收方,用于调用系统/其他 App
val view = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
startActivity(view)

val share = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "来自我的 App")
}
startActivity(Intent.createChooser(share, "分享给"))
1
2
显式:this ──▶ DetailActivity        (直接、无匹配开销)
隐式:ACTION_VIEW ──▶ 系统匹配 ──▶ 浏览器/相机/选择器(解耦、可多选)

// ❌ 用隐式 Intent 启动自己内部的页面(多此一举还易被劫持) // ✅ 内部跳转用显式,跨应用能力(拍照/分享/拨号)用隐式

2. 匹配规则:Action / Category / Data

隐式 Intent 靠这三件套匹配 <intent-filter>:

  • Action:要执行的动作,如 ACTION_VIEW、ACTION_SEND、ACTION_PICK
  • Category:额外约束,如 CATEGORY_BROWSABLE、CATEGORY_DEFAULT
  • Data:scheme/host/path/mimeType

最易踩的坑:系统给每个隐式 Intent 自动补 CATEGORY_DEFAULT。所以你的 Activity 想被隐式唤起,filter 里必须显式声明它,否则永远匹配不上。

1
2
3
4
5
6
7
8
9
<activity android:name=".DeepLinkActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <!-- 漏掉 DEFAULT,Deep Link 永远打不开你的页面 -->
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="myapp.com" android:pathPrefix="/product" />
    </intent-filter>
</activity>

3. 页面间传值:Bundle 与 Activity Result API

putExtra 本质就是往 Intent 内部的 Bundle 里塞数据。重点:startActivityForResult 已在 AndroidX 中废弃,改用 Activity Result API(AndroidX Activity 1.2 / Fragment 1.3 起)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : ComponentActivity() {

    // 选图:内置契约 GetContent,返回 content:// URI
    private val pickPhoto = registerForActivityResult(
        ActivityResultContracts.GetContent()
    ) { uri: Uri? ->
        uri?.let { viewModel.onImagePicked(it) }   // 用户取消则为 null
    }

    // 通用:打开另一个 Activity 并拿返回结果
    private val editName = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val name = result.data?.getStringExtra("name")
        }
    }

    fun openGallery() = pickPhoto.launch("image/*")
}
1
2
3
4
// 目标 Activity 回传
val back = Intent().putExtra("name", "张三")
setResult(RESULT_OK, back)
finish()

// ❌ 还在用 startActivityForResult + onActivityResult 那套 requestCode 分支 // ✅ registerForActivityResult 注册契约,生命周期安全、不丢回调、支持旋转屏

普通 https 链接直接拉起指定页面,比自定义 scheme 更稳。Android 6(API 23)起 android:autoVerify="true" 要求你在网站 .well-known/assetlinks.json 放好 SHA256 指纹,验证通过后系统直接路由、不弹选择器。

1
2
3
4
5
6
7
8
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["14:6D:E9:83:...:34"]
  }
}]
1
2
3
4
5
# 本地模拟 Deep Link 跳转(无需真点链接)
adb shell am start -W -a android.intent.action.VIEW \
  -d "https://myapp.com/product?id=123" com.example.app
# 检查自动校验状态
adb shell dumpsys package com.example.app | grep autoVerify

5. PendingIntent:让系统替你”代发”

PendingIntent 是把一个 Intent 包装成”授权给别的组件(通知栏、系统)以你的身份执行”。Android 12(API 31)起必须显式声明可变性,否则抛异常。

1
2
3
4
5
6
7
8
val intent = Intent(this, MainActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
    putExtra("from_notif", true)
}
val pi = PendingIntent.getActivity(
    this, 0, intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE  // 必须二选一
)

// ❌ 不写 FLAG_IMMUTABLE / FLAG_MUTABLE → Android 12 上直接崩溃 // ✅ 默认用 IMMUTABLE;只有需要被别处改写 extra 时才用 MUTABLE

6. Flags:从 Service/Receiver 起 Activity 的硬规则

从 Service、BroadcastReceiver 启动 Activity,必须加 FLAG_ACTIVITY_NEW_TASK,否则抛异常——因为它们没有”任务栈”上下文。

1
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)

其实你每天都在用

  • image_picker 拍照:底层发 MediaStore.ACTION_IMAGE_CAPTURE 隐式 Intent,系统去匹配相机 App
  • Share.share('hi'):底层是 ACTION_SEND 隐式 Intent + 系统选择器
  • 推送点击进详情页:通知的 setContentIntent(PendingIntent) 在 Android 12+ 必须带 FLAG_IMMUTABLE
  • 网页链接拉起 App:Deep Link + assetlinks.json 自动校验,用户无感跳转
  • Flutter 调原生拿返回值:MethodChannel 到原生后,原生用 Activity Result API 起系统 UI,再回传 Dart
  • 后台收到广播弹页:Intent 必须加 NEW_TASK,否则崩
  • adb shell am start:手动构造 Intent 启动任意组件做测试

常见误解(FAQ)

❌ 误区一:”startActivityForResult 还能用,照旧写没问题”

它确实没被删,但已在 AndroidX 中废弃:requestCode 分支冗长、Fragment/Activity 耦合、旋转屏易丢回调。面试里说”用 Activity Result API”才是标准答案,契约式写法(GetContent/RequestPermission)也更简洁。

❌ 误区二:”Deep Link 配了 scheme 和 host 就能从浏览器打开”

还差两步:filter 里必须有 CATEGORY_DEFAULT(系统自动补的这一条,你漏声明就匹配不上),以及 launchMode 通常设 singleTask 避免重复建栈;要”无弹窗直跳”还得配 autoVerify + 正确的 assetlinks.json 指纹。

❌ 误区三:”PendingIntent 不写可变性只是警告”

在 Android 12+ 上是直接崩溃(IllegalArgumentException)。任何通知、桌面小组件、Alarm 里的 PendingIntent 都要显式 FLAG_IMMUTABLE(或确需改写时 FLAG_MUTABLE)。这是上线必过的硬门槛。

❌ 误区四:”隐式 Intent 一定安全”

错。隐式 Intent 可能被恶意 App 劫持(同 Action 抢答),敏感操作建议显式指定包名/组件,或校验 resolveActivity() 的结果再 startActivity,避免 ActivityNotFoundException 与中间人。

一句话总结

Intent 是 Android 组件通信的”信封”:显式指名、隐式描述意图;把 startActivityForResult 换成 Activity Result API、给接收隐式调用的 Activity 补上 DEFAULT、给 Android 12+ 的 PendingIntent 标好可变性——这三点守住,组件通信的坑基本就绕开了。

本文由作者按照 CC BY 4.0 进行授权