1. "被列出来"这件事,本质就是 Intent 匹配
很多人第一次做这个需求,是想让自家安卓应用出现在系统分享面板、文件"打开方式"、或者别的应用的应用选择器里。折腾半天发现代码写了、应用装了、重启了,列表里就是不见踪影。问题的根子往往不在代码本身,而在于没搞明白系统是怎么"找到"你的应用的——它从来不主动扫描你,它只做一件事:拿一个 Intent,去问所有已安装应用里的 IntentFilter,谁能接,谁就上列表。
所以这篇东西不讲虚的,就讲清楚安卓应用列表、系统分享面板、打开方式列表这三类列表各自靠什么机制把应用拉进去,以及为什么你的应用没被拉进去。核心关键词就两个:安卓、应用列表。适合已经能写 Activity、但没系统啃过 Manifest 匹配规则的开发,也适合做 SDK 集成、需要让宿主应用发现自己能力的同学。
我默认你已经会基本的 Android Studio 操作,懂 Kotlin 或 Java 其中一门,Manifest 也能看懂。全文的配置我都在compileSdk 34、targetSdk 34、Android 10 到 14 的真机上实测过,部分行为在国产 ROM 上有差异,我会单独标出来。
1.1 三种典型的"应用列表"场景
先把场景分清,因为不同场景用的 action 和匹配规则差别很大,混在一起写必然出问题。
第一种是系统分享列表(Share Sheet)。用户在相册点分享、在浏览器点分享、在文件管理器长按分享,弹出来的那个横向或纵向应用条,就是它。背后是ACTION_SEND和ACTION_SEND_MULTIPLE两个隐式 Intent,发送方构造 Intent,系统查询谁能处理,把结果渲染成列表。
第二种是"打开方式"列表。用户点开一个 PDF、一个自定义后缀文件,系统问"用什么打开",或者点某个链接想跳到你的应用。背后是ACTION_VIEW,靠 MIME 类型和 URI 结构匹配。
第三种是别的应用主动查询出来的列表。比如某个编辑器要做"导入来源",它自己调queryIntentActivities拿一个列表展示。这种场景下,你的应用要能被查到,除了 IntentFilter 写对,还得考虑 Android 11 之后的包可见性规则——但注意,这条规则约束的是"查询方",不是你。你只要把 filter 声明好,被别人查到这件事本身不受限制。
1.2 Intent 与 IntentFilter 的匹配规则
Intent 有三个可匹配维度:action、category、data。IntentFilter 也是这三样。匹配逻辑是"与"关系,三条全过才算命中。
- action:Intent 的 action 必须在 filter 声明的 action 集合里。filter 不声明 action 就永远匹配不上任何带 action 的 Intent。
- category:Intent 里的每一个 category,都必须在 filter 的 category 集合里。注意方向,是 Intent 的 category 被 filter 包含,不是反过来。而且系统在解析隐式 Intent 时会自动给 Intent 加一个
android.intent.category.DEFAULT,所以你的 filter 里如果不写CATEGORY_DEFAULT,从外面来的隐式 Intent 基本都匹配不上。这是新手最容易漏的一行。 - data:这块最绕。
<data>元素里的属性会被拆成两部分看——URI 部分(scheme、host、port、path、pathPrefix、pathPattern)和类型部分(mimeType)。同一个 intent-filter 里写多个<data>,同维度的属性取并集。也就是说,Intent 的 data URI 只要匹配上 filter 的 URI 集合中任意一条,Intent 的 type 只要匹配上 filter 的 mimeType 集合中任意一条,data 这一项就算过。
正因为是"分维度取并集",会出现一个反直觉的结果:你写了<data android:scheme="content" />和<data android:mimeType="image/*" />,很多人以为意思是"content 的图片",实际上匹配的是"content 的任意类型,或者任意 scheme 的图片"。这个坑我在给一个图片编辑 SDK 做集成时踩过,结果应用莫名其妙出现在了一堆文档类文件的打开方式里。
注意:如果你只想匹配特定 scheme 下的特定类型,最好在每个
<data>里同时写 scheme 和 mimeType,可读性更好,也方便后期维护时一眼看懂意图。
1.3 一个被忽略的前提:exported 与包可见性
android:exported这个属性,targetSdk 31 之后,只要组件带了 intent-filter,就必须显式声明,不声明直接装不上,报错信息是INSTALL_PARSE_FAILED_MANIFEST_MALFORMED。这是硬性要求,不是建议。
要让外部应用能唤起你的组件,exported必须是true。设成false的话,就算 filter 写得再漂亮,系统也不会把你的组件对外暴露,列表里自然没有。
另一个容易搞混的是 Android 11 引入的包可见性。它的作用是限制应用能"看到"哪些别的应用,影响的是getPackageInfo、queryIntentActivities这类查询 API 的返回结果。你的应用要出现在别人的列表里,不受这条限制;但你的应用如果自己要查"这个文件能用哪些应用打开",那就得声明<queries>,否则查出来只有你自己和少数系统应用。这两件事方向相反,别弄反了。
2. 出现在系统分享列表:ACTION_SEND 完整实现
分享列表是最常见的需求,也是细节最多的一个。下面从头到尾捋一遍,包括最小配置、多类型、文件传输和 Direct Share 这种进阶玩法。
2.1 最小可用配置与 CATEGORY_DEFAULT 的坑
先从能跑起来的最小配置开始。假设你有一个ShareReceiverActivity用来接分享进来的内容:
<activity android:name=".share.ShareReceiverActivity" android:exported="true" android:label="@string/share_target_label" android:excludeFromRecents="true" android:theme="@style/Theme.Transparent"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> </activity>这几行里,有几个点必须说清楚。
android:exported="true"是前提,前面讲过。android:label决定的是分享面板里显示的名字。如果你不写,系统会往上找 Application 的 label,再找不到就显示包名。我见过有团队为了多语言把 label 设成空字符串,结果分享面板里显示成一串包名,用户根本不知道那是什么。
android:excludeFromRecents="true"配合透明主题,是为了让这个中转页不进入最近任务列表。分享是个"用完即走"的动作,如果用户从分享面板点了你的应用,处理完回到桌面,一按多任务发现多了个空白卡片,体验很差。透明主题的写法:
<style name="Theme.Transparent" parent="android:Theme.Translucent.NoTitleBar"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowNoTitle">true</item> </style>CATEGORY_DEFAULT那行是必须的。系统在调用startActivity处理隐式 Intent 时,会先给 Intent 加CATEGORY_DEFAULT,filter 里不声明就匹配不上。这个规则在PackageManager的匹配逻辑里是写死的,我试过用命令行手动指定 category 绕过,没必要。
关于mimeType,text/plain只覆盖纯文本。微信、系统浏览器分享文字走的就是这个类型。但有些应用分享文本时会带一个 URL,type 还是text/plain,内容在EXTRA_TEXT里。
2.2 多类型、多文件:SEND_MULTIPLE 与 data 的合并规则
要让你的应用同时支持文本、图片、视频、任意文件,就要写多个<data>或者多个 intent-filter。
<intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> <data android:mimeType="application/pdf" /> </intent-filter>这是单文件分享。多文件分享是另一个 action:
<intent-filter> <action android:name="android.intent.action.SEND_MULTIPLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> </intent-filter>注意SEND和SEND_MULTIPLE是两个独立 action,不能互相覆盖。有些应用只发SEND_MULTIPLE(比如相册多选分享),你只注册了SEND,用户在相册多选几张图点分享,你的应用就不见了。这个现象特别容易被当成"偶现 bug",其实是 action 没注册全。
接收端处理多文件时:
if (intent.action == Intent.ACTION_SEND_MULTIPLE) { val uris: List<Uri> = if (Build.VERSION.SDK_INT >= 33) { intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) ?: emptyList() } else { @Suppress("DEPRECATION") intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM) ?: emptyList() } }关于application/*这种通配,我建议慎用。*/*更是要命,写上去你的应用几乎会出现在所有分享面板里,包括那些跟你业务八竿子打不着的场景。而且部分 ROM 会把过于宽泛的匹配排在列表很靠后的位置,甚至直接折叠进"更多"里。真要接"任意文件",我一般会列具体的类型清单,再加一个application/octet-stream兜底。
2.3 分享大文件:FileProvider 与 URI 授权
如果你不只是接收,还要主动分享文件出去,Android 7.0 之后直接传file://会抛FileUriExposedException,必须用FileProvider转成content://。
先声明 Provider:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>${applicationId}这个占位符建议保留,避免多渠道打包时 authorities 冲突。android:exported="false"是对的,Provider 不需要对外暴露,靠临时授权就够了。
res/xml/file_paths.xml:
<paths> <files-path name="internal_files" path="." /> <cache-path name="internal_cache" path="." /> <external-files-path name="ext_files" path="." /> <external-cache-path name="ext_cache" path="." /> </paths>这几个标签对应的是不同目录,files-path指向Context.getFilesDir(),cache-path指向getCacheDir(),external-files-path指向外部存储的应用私有目录。传文件的时候一定要确认文件真的在这些目录下,路径没对上会抛IllegalArgumentException: Failed to find configured root that contains ...,这个报错信息还算友好,直接看路径就能定位。
发送端:
val file = File(cacheDir, "export_${System.currentTimeMillis()}.jpg") val uri = FileProvider.getUriForFile( this, "${packageName}.fileprovider", file ) val send = Intent(Intent.ACTION_SEND).apply { type = "image/jpeg" putExtra(Intent.EXTRA_STREAM, uri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } startActivity(Intent.createChooser(send, "分享到"))FLAG_GRANT_READ_URI_PERMISSION这一行不能省。它给接收方应用授予一次性的读权限,没有它,接收方拿到 URI 也读不出内容,会抛SecurityException。这是我在对接某个第三方阅读器时反复遇到的问题——对方反馈"你们分享的文件打不开",最后发现是我们漏了这个 flag。
顺带说一句createChooser的一个小技巧:可以用EXTRA_EXCLUDE_COMPONENTS把自己从列表里排除掉,避免用户分享到自己应用又绕回来的尴尬:
val chooser = Intent.createChooser(send, "分享到").apply { putExtra( Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(ComponentName(this@MainActivity, ShareReceiverActivity::class.java)) ) }2.4 把入口做进分享面板头部的 Direct Share
Android 10 之后,系统分享面板顶部会有一排头像式的快捷入口,这是 Direct Share。它比普通的应用图标更醒目,适合做"分享到某个具体联系人/项目/会话"这种场景。
实现方式是用ShortcutManager配一个 share target XML。先在 Manifest 里给接收 Activity 加 meta-data:
<activity android:name=".share.ShareReceiverActivity" 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> <meta-data android:name="android.service.chooser.chooser_target_service" android:value="androidx.sharetarget.ChooserTargetServiceCompat" /> </activity>再定义res/xml/shortcuts.xml:
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <share-target android:targetClass="com.example.app.share.ShareReceiverActivity"> <data android:mimeType="text/plain" /> <category android:name="com.example.app.category.TEXT_SHARE_TARGET" /> </share-target> </shortcuts>然后在代码里动态发布 Shortcut:
val shortcut = ShortcutInfoCompat.Builder(context, "target_$id") .setShortLabel(name) .setIcon(IconCompat.createWithBitmap(avatarBitmap)) .setIntent( Intent(context, ShareReceiverActivity::class.java).apply { action = Intent.ACTION_DEFAULT putExtra("target_id", id) } ) .setCategories(setOf("com.example.app.category.TEXT_SHARE_TARGET")) .build() ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)这套东西有个前提:Direct Share 的条目是系统根据用户使用频率动态排序的,发布之后不保证立刻出现,也不保证排在前面。而且每个应用最多只能在分享面板里出现有限个 Direct Share 条目(一般是 4 到 8 个,看 ROM)。我实测下来,同一个 Shortcut 用几次之后排名会明显上升,属于"用得越多越靠前"的机制。
3. 接管"打开方式":文件关联的精确控制
分享是"我把内容送出去",打开方式是"用户点文件找到我"。这一块对匹配精度的要求更高,写宽了会抢占别人的入口,写窄了自己进不去。
3.1 ACTION_VIEW 与 MIME、扩展名的匹配组合
最基础的配置:
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="content" /> <data android:scheme="file" /> <data android:mimeType="application/pdf" /> </intent-filter>这里我特意分开写scheme和mimeType,是因为想强调前面说的分维度规则。content和file是 Android 里文件 URI 最常见的两种 scheme,content://是 ContentProvider 提供的,file://是直接文件路径,从第三方文件管理器点开文件时两种都可能出现,所以一般两个都要写。
android:mimeType有大小写敏感的问题。MIME 类型规范上是不区分大小写的,但 Android 的匹配实现里做的是字符串比较,部分 ROM 上Application/PDF和application/pdf匹配不上。稳妥做法是全部小写,接收端再做一次规范化。
如果你的文件是自定义后缀,系统识别不出 MIME,就会走*/*或者application/octet-stream。这时候要么用pathPattern精确匹配后缀,要么在接收端二次校验。
3.2 pathPattern 正则的转义与分段匹配
pathPattern是唯一支持正则的路径匹配方式,但它的正则语法是不完整的,只支持.、*、.*这几种,不支持字符类、分组、量词。而且有个大坑:pathPattern是按路径逐段匹配的,/会被当作段分隔符处理,.*不跨段。
匹配.mydoc后缀的写法:
<data android:scheme="file" android:host="*" android:pathPattern=".*\\.mydoc" />注意 XML 里的\\.,这是两层转义:XML 层面\\表示一个反斜杠,传给匹配引擎后变成\.,也就是正则里的字面点号。如果只写\.,XML 解析器会报错或者解析成别的字符。我第一次写的时候就漏了一层,匹配一直不生效,查了半天才反应过来。
还有个更隐蔽的问题:Android 官方文档里提到,从 Android 6.0 开始,pathPattern匹配时.*不跨/。所以.*\\.mydoc匹配/sdcard/a/b.mydoc是没问题的(.*匹配b),但如果路径里带层级,比如想匹配/sdcard/dir.mydoc/file.txt,就匹配不上了。实际使用中这个限制影响不大,因为后缀一般在最后一段。
如果你要匹配多种后缀,pathPattern得写多条,或者干脆用pathPrefix加接收端判断,后者代码更简单,但会多匹配一些不该匹配的路径。我的取舍是:后缀不超过三个,就用多条pathPattern;超过三个,统一用*/*接收,然后在 onCreate 里根据后缀分发。
3.3 避免抢占系统默认:优先级与 autoVerify 的取舍
android:priority这个属性只对"同一次解析里有多个候选"时起作用,影响的是resolveActivity返回哪个、以及某些系统组件的排序。它不会让系统把用户已经设过的默认应用换掉,也不会在打开方式列表里把别人挤走。很多人误以为调高 priority 就能抢到默认,实际上用户一旦在"打开方式"里选了"始终",默认就固定了,只能靠用户自己去设置里改。
真正要谨慎的是<data android:mimeType="*/*" />加scheme="content"这种组合。它会让你的应用出现在几乎任何文件的打开方式里。如果你的应用不是文件管理器或者通用查看器,这种声明会显著降低用户信任度,也会被应用市场在审核时质疑。
关于autoVerify,它是给 App Links 用的,配合<intent-filter android:autoVerify="true">和android:host声明,让系统自动验证域名归属,验证通过后点击对应链接直接进你的应用,不再弹选择框。这个只对http/https的 scheme 有意义,对file、content无效。如果你的应用有官网,而且需要做"点链接直接进 App",这个值得配;单纯做本地文件关联,不需要。
4. Android 11 之后的包可见性
这一节的核心是:想清楚你的应用是"被查方"还是"查询方"。前面提过,被查方不受限制,但很多需求其实是双向的——你既要被别人找到,自己也要列出一份"能处理这个 Intent 的应用列表"给用户选择。
4.1 queries 声明怎么写
Android 11 开始,不声明<queries>的话,queryIntentActivities、getPackageInfo、resolveActivity这些 API 只能看到自己、系统框架和少数几个系统应用。这个限制确实让一批老代码直接失效,表现是"原来能拿到一堆应用,升级后只剩两三个"。
按 Intent 查询的写法:
<queries> <intent> <action android:name="android.intent.action.SEND" /> <data android:mimeType="image/*" /> </intent> <intent> <action android:name="android.intent.action.VIEW" /> <data android:scheme="https" /> </intent> </queries>注意<queries>里<intent>的写法跟<intent-filter>不一样,它不支持<category>声明,也不支持pathPattern这类路径匹配。所以能表达的查询范围比 filter 窄。如果你需要按包名精确查询,用<package android:name="com.example.target" />。
还有一个场景需要额外注意:如果你的应用要跟某个特定应用(比如合作的厂商应用)互相唤起,而那个应用不在<queries>的 Intent 覆盖范围内,最好直接加<package>声明。这个比靠 Intent 匹配更稳,也不依赖对方的 filter 是否写对。
4.2 QUERY_ALL_PACKAGES 的适用边界
QUERY_ALL_PACKAGES是一个普通权限,声明了就能拿到所有已安装应用列表:
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />但它的适用范围被严格限制在特定类别,比如安全软件、文件管理器、启动器、设备管理类应用。普通应用在应用市场上架时,声明这个权限大概率会被打回,要求你说明用途并提供更精确的<queries>方案。我在一个工具类应用上试过,审核反馈明确要求改成<queries>按需查询。
所以在动手之前先想清楚:你是真的需要"全量应用列表",还是只是需要"能处理某个操作的应用列表"。后者用<queries>加<intent>完全够用,而且用户体验上更合理——用户看到的列表里只有真正相关的应用,不是一屏没用的图标。
5. 拿到数据之后:解析 Intent 与常见崩溃点
配置写对只是第一步,真正出问题的地方往往在接收端。这一节说几个我踩得最多的坑。
5.1 Intent 解析的边界处理
先看一段看起来很正常的代码:
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) val inputStream = contentResolver.openInputStream(uri!!)这段代码在生产环境下有三种崩法。
第一种,intent.action可能是SEND也可能是SEND_MULTIPLE,取EXTRA_STREAM的方式完全不同。如果是SEND_MULTIPLE,getParcelableExtra返回 null,后面uri!!直接 NPE。
第二种,某些应用的实现不规范,会把EXTRA_STREAM塞成 String 而不是 Uri。这种情况getParcelableExtra返回 null,或者在某些低版本 ROM 上抛 ClassCastException。稳妥做法是兼容处理:
private fun extractSingleUri(intent: Intent): Uri? { val raw = intent.extras?.get(Intent.EXTRA_STREAM) ?: return null return when (raw) { is Uri -> raw is String -> runCatching { Uri.parse(raw) }.getOrNull() else -> null } }第三种,contentResolver.openInputStream抛SecurityException。原因是发送方没加FLAG_GRANT_READ_URI_PERMISSION,或者你的 Activity 不是通过startActivity直接拉起的(比如被某个中转页转了一手,权限没传递下来)。这种只能 try-catch 并给用户提示,没法从接收端补救。
Android 13 之后getParcelableExtra(String)被标记了 deprecation,推荐用带 class 参数的重载:
val uri = if (Build.VERSION.SDK_INT >= 33) { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_STREAM) }写起来啰嗦,但能避免一堆类型相关的警告。
5.2 权限授予与 URI 持久化
FLAG_GRANT_READ_URI_PERMISSION授予的是临时权限,作用范围是"这一次 startActivity 拉起来的组件"。如果你的接收 Activity 处理完内容后要启动另一个 Activity 继续处理,那个新 Activity 拿同一个 URI 还能读,因为授权是跟着 URI 走的,在整个任务栈生命周期内有效。但如果你的 Activity 处理完就 finish 了,把 URI 存在数据库里,下次启动再读,就会抛SecurityException。
要长期持有,得让发送方加FLAG_GRANT_PERSISTABLE_URI_PERMISSION,接收方再调:
contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION )但分享场景里发送方基本不会加这个 flag,所以实际上长期持有很难做到。我的做法是:分享进来之后立刻把内容复制到应用私有目录,之后操作副本。多了点 IO,但省掉一堆权限问题。特别是图片、视频这类大文件,复制耗时明显,最好放在子线程,并且给个进度提示,不然用户会以为卡死了。
6. 调试与排错实录
配置写完,怎么快速验证有没有生效,比反复安装重启高效得多。
6.1 adb 命令验证 Intent 匹配
最直接的验证方式是让系统帮你跑一次匹配查询:
adb shell cmd package query-activities \ -a android.intent.action.SEND \ -t image/jpeg输出里会列出所有能处理这个 Intent 的组件。如果你的应用不在里面,问题就一定出在 Manifest 配置上,跟代码无关。这个命令比装一圈应用来测试快太多了。
还可以直接构造一次分享调用:
adb shell am start \ -a android.intent.action.SEND \ -t text/plain \ --es android.intent.extra.TEXT "hello from adb"看系统弹出来的分享面板里有没有你的应用,以及点进去之后能不能正常拿到内容。
查自己应用注册了哪些 filter:
adb shell dumpsys package com.example.app | grep -A 30 "IntentFilter"这个输出会比较长,但能清楚看到每个组件实际注册的 action、category、scheme 和 type,是排查"我以为我写了但其实没写"的利器。
还有一个偏门但好用的命令,查某个 action 的解析结果:
adb shell cmd package resolve-activity -a android.intent.action.VIEW -t application/pdf6.2 常见问题速查表
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 分享面板里完全没有我的应用 | 缺CATEGORY_DEFAULT | dumpsys package看 filter 内容 |
| 只支持分享文字,图片不出现 | 只注册了text/*,没注册image/* | query-activities -t image/jpeg |
| 多选图片分享时不出现 | 没注册ACTION_SEND_MULTIPLE | query-activities -a android.intent.action.SEND_MULTIPLE |
| 安装时报 manifest 错误 | targetSdk 31+ 未声明exported | 看 Gradle 报错堆栈 |
| 列表里显示成包名 | android:label为空或未设置 | 检查 Activity 与 Application 的 label |
| 同一应用出现两次 | 多个 Activity 注册了相同 filter | 检查是否有activity-alias或重复组件 |
| 能进列表但点进去崩溃 | 接收端空指针或权限异常 | adb logcat过滤包名和SecurityException |
| 打开方式里能选但文件读不出 | 发送方未授权读 URI | 检查FLAG_GRANT_READ_URI_PERMISSION |
| 升级到 targetSdk 33 后查询列表变少 | 未声明<queries> | 检查 manifest 是否有 queries 节点 |
6.3 几个踩过的坑
坑一:透明主题导致黑屏一闪。用Theme.Translucent.NoTitleBar做中转页,在部分国产 ROM 上会先黑一帧再显示内容。解决办法是给windowBackground设成透明,同时把windowIsTranslucent和windowNoTitle都显式设为 true,不要只依赖父主题。我试过只改父主题,在某个 Android 12 的机器上还是闪,加了三项之后稳定了。
坑二:android:label用了资源引用但没做本地化。分享面板里显示的名字是跟着系统语言走的。如果你的应用只有中文 label,在英文系统上显示中文;反之亦然。如果应用有出海需求,share_target_label这类字符串一定要放进所有语言的 strings 文件里。
坑三:接收端在onCreate里做重活。分享进来之后做文件复制、图片解码、上传,全都塞在onCreate,用户点完应用看起来像卡住了。正确做法是onCreate只解析 Intent 参数,重活放onStart之后的协程或者后台线程,同时给一个加载状态。如果是透明中转页,处理完直接finish(),不要让用户看到界面。
坑四:activity-alias的targetActivity必须存在同名组件。用 alias 让同一个 Activity 出现在多个列表里是很实用的技巧,但android:targetActivity必须指向一个已经声明的 Activity,而且那个 Activity 本身也要在 manifest 里有定义。我见过只写了 alias 没写原 Activity 的,装上去直接报错。
坑五:不要指望 priority 改变分享面板排序。分享面板的排序基本由系统按使用频率决定,android:priority在这里几乎没影响。想让入口靠前,靠谱的做法是 Direct Share 加高频使用,而不是调 priority。
最后分享一个我在实际项目里固定下来的做法:把所有跟"被外部调用"相关的 Activity 集中放在一个external包里,Manifest 里也集中写在一起,每个组件上面都加一行注释说明它是被哪个 Intent、哪个场景调起的。这个习惯是踩过一次大坑之后养成的——当时有个历史遗留的分享 Activity 混在业务包里,改需求时顺手删了,结果用户反馈"分享到我们应用的入口消失了",排查了两个小时才找到原因。集中管理之后,这类问题基本不会再发生。