隐式intent

隐式intent:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“隐式intent”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“学会用隐式intent调用其他App完成打开网页、发送邮件、分享文字、拨号等常见操作,并能处理无匹配App的异常情况。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用Intent合同、解析目标、权限、返回结果和重复副作用独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么需要隐式intent

你的App不是孤岛。用户想分享一段文字到微信、打开一个网页链接、拨一个电话号码——这些功能如果每个App都自己实现,那每个App都要内置一个浏览器引擎、一个邮件客户端、一个拨号器。不仅体积爆炸,体验也割裂。

Android的设计哲学是"组装胜过重造"——就像一个邮局分拣中心:你不必自己送信,只需把信(intent)投进邮筒(系统),邮局(Android系统)根据信封上写的"要做什么"来自动分拣到正确的收件人(能处理这个操作的App)。没有这层机制,每个App之间就是信息孤岛——你想从自己的App打开一个网页?不好意思,自己写一个浏览器。

隐式intent解决的是跨App协作问题:你只管描述"我想做什么",由系统负责找到"谁能帮我做"

隐式intent是怎么工作的

显式intent vs 隐式intent

像直接快递——你知道收件人地址,直接寄过去。用于App内部跳转:从列表页跳到详情页,你知道目标Activity的类名。

像贴在公告栏的寻人启事——你只描述需求("谁能打开这个网址?"),Android系统广播给所有注册了相应能力的App,让它们来「应聘」。

下面用代码直观对比两者:

// 显式:明确指定目标Activity类名 → App内部跳转
val explicitIntent = Intent(this, DetailActivity::class.java)
explicitIntent.putExtra("item_id", 42)
startActivity(explicitIntent)
 
// 隐式:只描述动作和数据 → 系统匹配合适的App
val implicitIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.android.com"))
startActivity(implicitIntent)
 

隐式intent的三要素:action、data、category

系统匹配隐式intent靠三个维度,对应intent-filter中的三项:

要素作用常见值
action描述"做什么"ACTION_VIEW(查看)、ACTION_SEND(分享)、ACTION_DIAL(拨号)
data描述"操作什么"URI(如 https://...mailto:...)和 MIME 类型(如 text/plain
category描述"在什么场景下可用"CATEGORY_DEFAULT(标准入口)、CATEGORY_BROWSABLE(可从浏览器调起)

一个App通过AndroidManifest中的 <Term def="Manifest XML中的一个标签块,声明本App的哪个组件能处理哪些类型的intent。包含action(动作)、category(类别)、data(数据格式/URI)三要素。系统在匹配隐式intent时遍历所有注册的intent-filter来决定谁能处理。">intent-filter</Term> 来注册自己能处理什么:

<!-- 在 AndroidManifest.xml 的 Activity 标签内声明 -->
<activity android:name=".ShareActivity" 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>

这段XML的意思是:"我的ShareActivity能处理分享纯文本的操作,在标准启动模式下可用"。当你的App发出ACTION_SEND + text/plain的隐式intent,系统就会把这个Activity列在备选清单里。

把整条解析链路连起来看:你只描述"要做什么",剩下的"找谁来做、给不给用户选"全交给系统。下面这张图把一次隐式intent解析拆成四步,可逐帧观察Intent如何从App流到系统、再被分发给候选App:

可交互
你的 AppstartActivity(intent)系统 PackageManager匹配 intent-filter浏览器 Aintent-filter ✓浏览器 Bintent-filter ✓Chooser 选择器让用户选一个隐式 Intentaction = ACTION_VIEWdata = https://...category = DEFAULTaction ∧ category ∧ data 全命中① 发出② 匹配③ 候选④ 选择器

第 1 / 6 步 · ① App 发出隐式 Intent:action=ACTION_VIEW + data=https网址 + category=DEFAULT

点击播放,看一次隐式 Intent 如何从 App 发出、经系统匹配 intent-filter、分发给候选 App、最后弹出选择器;可暂停、单步、拖进度逐帧观察。

隐式 Intent 解析过程:App 只描述「要做什么」(action + data + category)→ 系统 PackageManager 拿它去匹配各 App 的 intent-filter(三要素全命中)→ 列出候选 App → 弹 Chooser 让用户选(唯一匹配则直接启动)。

四大最常用的隐式intent模式

① 打开网页(ACTION_VIEW + http/https URI)

val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.com"))
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

② 发送邮件(ACTION_SENDTO + mailto: URI)

ACTION_SENDTO 只调起邮件App,不会弹出微信、蓝牙等无关选项。

val intent = Intent(Intent.ACTION_SENDTO).apply {
    data = Uri.parse("mailto:")
    putExtra(Intent.EXTRA_EMAIL, arrayOf("support@example.com"))
    putExtra(Intent.EXTRA_SUBJECT, "反馈建议")
    putExtra(Intent.EXTRA_TEXT, "你好,我想反馈...")
}
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

③ 分享文字(ACTION_SEND + MIME类型)

MIME类型 <Term def="Multipurpose Internet Mail Extensions,一种标准化的内容类型标记,如 text/plain 表示纯文本、image/jpeg 表示JPEG图片。在intent中用于告诉系统'我要分享什么类型的数据',系统据此筛选能处理该数据类型的App。">MIME</Term>(如 text/plain)告诉系统"我分享的是纯文本"——这样只会列出微信、短信、备忘录等能处理文字的App,不会弹出一个视频播放器。

val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "这篇文章写得真好!https://example.com/article")
}
startActivity(Intent.createChooser(shareIntent, "分享到"))

弹出分享菜单——比直接 startActivity(intent) 更友好,总是展示选项列表而非静默用默认App。

④ 拨号(ACTION_DIAL + tel: URI)

ACTION_DIAL 打开拨号盘但不自动拨出(安全)。如需直接拨出用 ACTION_CALL(需 CALL_PHONE 权限)。

val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:10086"))
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

动手:完成一个"万能分享"功能

下面带你从零实现一个能安全地分享文字到任何App的功能。每一步都配上代码和效果描述。

猜一猜:如果用户的手机里只有微信和短信能处理"分享文字",直接调 startActivity 和用 createChooser 有什么区别?

分步1 / 4

① 创建分享intent并设置MIME类型

首先创建一个 ACTION_SEND intent,设置 MIME 类型为 text/plain——这告诉系统"我分享的是纯文本"。 此时如果直接发出这个intent,系统可能会直接用上次用户选过的默认App(比如用户之前选过"始终用微信"),而不会弹出选择列表。

代码逐段拆解

安全的隐式intent启动模式

下面的代码封装了一个通用的安全启动方法——覆盖了resolvedActivity检查、异常处理和降级策略这三个关键环节。

fun Context.openUrlSafely(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    // 关键:先检查有没有App能处理
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    } else {
        Toast.makeText(this, "没有能打开链接的应用", Toast.LENGTH_SHORT).show()
    }
}

resolveActivity(packageManager) 向系统查询"有没有App能处理这个intent"。系统遍历所有已安装App的Manifest中的intent-filter,尝试匹配action + category + data三要素。返回 null 说明没有任何App能处理——此时调用 startActivity 会直接抛出 ActivityNotFoundException 崩溃。

另一条安全路径是 try-catch

try {
    startActivity(intent)
} catch (e: ActivityNotFoundException) {
    // 降级:提示用户、或者在WebView里打开
    Toast.makeText(this, "未找到可处理此操作的应用", Toast.LENGTH_SHORT).show()
}

两者选一即可——resolveActivity 是防御式(先检查再行动),try-catch 是事后补救。推荐前者,意图更清晰。

Runtime权限检查(拨号场景)

使用 ACTION_DIAL 打开拨号盘不需要权限。但如果产品需求是直接拨出电话ACTION_CALL),就需要 CALL_PHONE 权限——这是一个运行时权限,必须先动态申请:

// 注册权限请求处理器
private val callPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:10086"))
        startActivity(intent)
    }
}
 
// 拨号前检查权限
fun makeCall() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
== PackageManager.PERMISSION_GRANTED
) {
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:10086"))
startActivity(intent)
} else {
callPermissionLauncher.launch(Manifest.permission.CALL_PHONE)
}
}
 

容易踩的坑

小结

  • 显式intent指定类名→App内部跳转;隐式intent描述需求(action+data+category)→跨App协作
  • 四大常用action:ACTION_VIEW(网页)、ACTION_SEND(分享)、ACTION_DIAL(拨号)、ACTION_SENDTO(邮件)
  • Intent.createChooser() 强制弹出选择列表,避免静默使用默认App
  • 安全铁律:发出隐式intent前,用 resolveActivity(packageManager) != null 检查是否有处理者
  • 在Manifest中声明intent-filter让你的App也能被其他App调起——别忘了加 CATEGORY_DEFAULT + exported="true"

练习

问题 1:“第15章 Implicit Intents”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

本章出现的专业名词,用大白话再讲一遍。

显式intent

直接指定目标组件类名(如 DetailActivity::class.java)的intent,用于App内部的页面跳转。必须知道目标Activity的完整类名。详见本章"显式intent vs 隐式intent"一节。

隐式intent

不指定目标组件类名、只描述"要做什么"的intent。系统通过匹配Manifest中的intent-filter找到能处理的App。用于跨App调用——打开网页、分享、发邮件等。详见本章"显式intent vs 隐式intent"一节。

intent-filter

Manifest XML中的一个标签块,声明本App的哪个组件能处理哪些类型的intent。包含action(动作)、category(类别)、data(数据格式/URI)三要素。系统在匹配隐式intent时遍历所有注册的intent-filter来决定谁能处理。详见本章"隐式intent的三要素"一节。

MIME

Multipurpose Internet Mail Extensions,一种标准化的内容类型标记。在intent中用 setType("text/plain")setType("image/jpeg") 告诉系统"我要分享什么类型的数据",系统据此筛选能处理该数据类型的App。详见本章"分享文字"一节。

createChooser

创建系统分享选择器弹窗——展示所有能处理该intent的App列表并允许用户选择,同时自定义弹窗标题。比直接 startActivity 更友好,且保证总是弹出选择界面而非静默使用默认App。详见本章"分享文字"一节。

“隐式intent”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“隐式intent”必须回到可观察状态

“隐式intent”的学习结果不是记住类名,而是能预测“学会用隐式intent调用其他App完成打开网页、发送邮件、分享文字、拨号等常见操作,并能处理无匹配App的异常情况。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

15. Implicit Intents

在“隐式intent”中,“15. Implicit Intents”以action、data、category和MIME描述能力,由系统解析匹配组件;发起前应检查响应者,接收后必须验证所有外部数据。

Adding Buttons

在“隐式intent”中,验证“Adding Buttons”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Adding a Suspect to the Model Layer

在“隐式intent”中,分析“Adding a Suspect to the Model Layer”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。

Using a Format String

在“隐式intent”中,“Using a Format String”跨应用边界时只授予完成任务所需的URI与组件权限,并在日志中移除联系人等个人信息。

Using Implicit Intents

在“隐式intent”中,验证“Using Implicit Intents”覆盖零个、一个和多个响应者,以及深链重复到达、目标被杀和无权限数据。

Challenge: Another Implicit Intent

在“隐式intent”中,“Challenge: Another Implicit Intent”跨应用边界时只授予完成任务所需的URI与组件权限,并在日志中移除联系人等个人信息。

“隐式intent”验收回顾

“隐式intent”只有在Intent合同、解析目标、权限、返回结果和重复副作用能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:AppBar与菜单 · 下一页:使用intent拍照 →

章专属可重放状态实验

先预测“点击嫌疑人或报告按钮并解析零个、一个或多个响应者”发生后,Intent 发起方、PackageManager 与外部响应应用应怎样改变action、URI、MIME、extras、授权和候选列表;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

隐式intent:状态合同

为电话、联系人和分享动作构造最小隐式 Intent 并处理解析结果

验证场景

第四版正式目录节点

implicit-intents · 正常任务

15. Implicit Intents固定 SDK、设备配置和初始状态,触发“点击嫌疑人或报告按钮并解析零个、一个或多个响应者”

状态所有者Intent 发起方、PackageManager 与外部响应应用
受控状态action、URI、MIME、extras、授权和候选列表
触发事件点击嫌疑人或报告按钮并解析零个、一个或多个响应者

冻结入口:15. Implicit Intents

记录Intent 发起方、PackageManager 与外部响应应用的初始action、URI、MIME、extras、授权和候选列表

观察:Intent 字段、resolve 结果、选择器、授权范围和返回轨迹中的“15. Implicit Intents”轨迹

预期:由Intent 发起方、PackageManager 与外部响应应用提交action、URI、MIME、extras、授权和候选列表,并持续满足“没有响应者时提供可理解回退,敏感数据只授予必要目标”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“点击嫌疑人或报告按钮并解析零个、一个或多个响应者”。每一步只允许Intent 发起方、PackageManager 与外部响应应用按职责提交状态,并持续核对“没有响应者时提供可理解回退,敏感数据只授予必要目标”。

Deterministic event replay

隐式intent:事件轨迹

选择一次状态转换1 / 5

不变量:没有响应者时提供可理解回退,敏感数据只授予必要目标

交付证据:Intent 字段、resolve 结果、选择器、授权范围和返回轨迹

实验三:章专属反例与同输入恢复

注入“无应用能处理 Intent 时仍直接 startActivity,导致崩溃”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有Intent 字段、resolve 结果、选择器、授权范围和返回轨迹一起恢复才算修复。

Fault · cancel · restore

隐式intent:反例与恢复

故障:无应用能处理 Intent 时仍直接 startActivity,导致崩溃

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“无应用能处理 Intent 时仍直接 startActivity,导致崩溃”

3. 检查所有者一致

没有响应者时提供可理解回退,敏感数据只授予必要目标

4. 核对结果一致

Intent 字段、resolve 结果、选择器、授权范围和返回轨迹

讨论

评论区加载中…