隐式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
↡直接指定目标组件类名(如 `DetailActivity::class.java`)的intent,用于App内部的页面跳转。需知道目标Activity的完整类名。像直接快递——你知道收件人地址,直接寄过去。用于App内部跳转:从列表页跳到详情页,你知道目标Activity的类名。
↡不指定目标组件类名、只描述「要做什么」的intent。系统通过匹配Manifest中的intent-filter找到能处理的App。用于跨App调用——打开网页、分享、发邮件等。像贴在公告栏的寻人启事——你只描述需求("谁能打开这个网址?"),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)
// 显式:明确指定目标Activity类名 → App内部跳转
Intent explicitIntent = new Intent(this, DetailActivity.class);
explicitIntent.putExtra("item_id", 42);
startActivity(explicitIntent);
// 隐式:只描述动作和数据 → 系统匹配合适的App
Intent implicitIntent = new 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:
第 1 / 6 步 · ① App 发出隐式 Intent:action=ACTION_VIEW + data=https网址 + category=DEFAULT
点击播放,看一次隐式 Intent 如何从 App 发出、经系统匹配 intent-filter、分发给候选 App、最后弹出选择器;可暂停、单步、拖进度逐帧观察。
四大最常用的隐式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)
}Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.com"));
if (intent.resolveActivity(getPackageManager()) != 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)
}Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"support@example.com"});
intent.putExtra(Intent.EXTRA_SUBJECT, "反馈建议");
intent.putExtra(Intent.EXTRA_TEXT, "你好,我想反馈...");
if (intent.resolveActivity(getPackageManager()) != 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, "分享到"))Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.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)
}Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:10086"));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}动手:完成一个"万能分享"功能
下面带你从零实现一个能安全地分享文字到任何App的功能。每一步都配上代码和效果描述。
猜一猜:如果用户的手机里只有微信和短信能处理"分享文字",直接调
startActivity和用createChooser有什么区别?
① 创建分享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()
}
}public static void openUrlSafely(Context context, String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
// 关键:先检查有没有App能处理
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
} else {
Toast.makeText(context, "没有能打开链接的应用", 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)
}
}
private final ActivityResultLauncher<String> callPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
if (granted) {
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:10086"));
startActivity(intent);
}
});
public void makeCall() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
== PackageManager.PERMISSION_GRANTED) {
Intent intent = new 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、设备配置和初始状态,触发“点击嫌疑人或报告按钮并解析零个、一个或多个响应者”
冻结入口: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:事件轨迹
不变量:没有响应者时提供可理解回退,敏感数据只授予必要目标
交付证据:Intent 字段、resolve 结果、选择器、授权范围和返回轨迹
实验三:章专属反例与同输入恢复
注入“无应用能处理 Intent 时仍直接 startActivity,导致崩溃”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有Intent 字段、resolve 结果、选择器、授权范围和返回轨迹一起恢复才算修复。
Fault · cancel · restore
隐式intent:反例与恢复
故障:无应用能处理 Intent 时仍直接 startActivity,导致崩溃
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“无应用能处理 Intent 时仍直接 startActivity,导致崩溃”
没有响应者时提供可理解回退,敏感数据只授予必要目标
Intent 字段、resolve 结果、选择器、授权范围和返回轨迹