“应用装是装了可微信分享列表里翻不到它”——做安卓开发这些年这个问题我被问到的次数排得进前三。紧随其后的还有两个变种文件管理器里点开自家格式弹出来的“打开方式”列表里没有我的应用以及同事的 App 能出现在某个第三方客户端的“发送到”菜单里我的却不行。遇到这类情况绝大多数人第一反应是去翻代码怀疑是不是 Activity 没注册、是不是签名问题。而实际排查下来十有八九问题出在AndroidManifest.xml的intent-filter写法上剩下的一两成则是 Android 11 之后新增的包可见性规则在起作用。这篇内容聊的就是一件事怎么让自己的安卓应用出现在别的应用列表里。这里的“其他应用列表”不是单指某一个界面而是一整类由系统或第三方应用动态生成的、用来挑选应用去处理某件事的清单。它包含系统分享面板、“打开方式”弹窗、第三方 App 自建的导入导出列表乃至桌面上的快捷方式。适合的读者是已经能跑通 Hello World、正在做应用间互通或内容分享功能的安卓开发者如果你对Intent只有模糊印象也能看懂我会把关键概念拆开讲。1. 先认清“应用列表”的四种形态别一上来就改 Manifest很多人卡在这一步知道自己要“进列表”但不知道进的是哪个列表于是把各种 action 往 Manifest 里堆最后既没生效也看不出哪里错了。安卓里能让你“被别人列出来”的通道至少有四条它们的触发方式、入口 action、以及是否受包可见性影响各不相同。先把这四条分清楚后面改配置才有方向。1.1 系统分享面板ACTION_SEND 组成的清单用户在任意应用里点“分享”系统弹出的那一格格应用图标就是由android.intent.action.SEND这个隐式 Intent 现算出来的。系统会拿着这个 Intent 去问PackageManager谁声明了能处理它然后把所有命中的 Activity 连同它的android:label和图标一起显示出来。这是一个标准的解析过程不依赖任何预注册的“列表文件”所以你只要把intent-filter写对理论上立刻就能出现在里面。关键点在于分享面板的排序和展示由系统控制。你能控制的只有三件事能不能进去匹配规则、显示什么名字和图标label 与 icon、点进去之后干什么目标 Activity。至于排序android:priority在隐式 Intent 解析里是不起作用的它只对有序广播有意义——这个误解每年都要坑掉一批人。1.2 “打开方式”清单ACTION_VIEW 加 MIME 匹配当用户在文件管理器、浏览器或者聊天软件里点开一个文件或链接系统需要一个能“看懂”这个内容的应用用的就是android.intent.action.VIEW。这条路径和分享面板最大的区别在于匹配条件里多了 data 维度。也就是文件的 MIME 类型、URI 的 scheme、host、路径都会参与匹配。写错一个字母你就不在列表里。这里有个容易被忽略的行为差异如果 filter 里只写了mimeType没写scheme系统会默认认为你接受content:和file:两种 scheme反过来如果只写了scheme没写mimeType那 Intent 里的 type 必须为空才能匹配上。这个规则解释了为什么很多人写了content://之后file://的文件就点不开了。1.3 第三方自建列表自定义 Action 与包可见性不是所有“列表”都是系统画的。很多应用——尤其是笔记类、下载类、浏览器——会自己实现一个选择器遍历设备上所有声明了某个 action 的应用自己排版、自己加排序逻辑。这类列表的匹配条件是对方定的可能是标准的ACTION_SEND也可能是完全自定义的字符串比如com.example.notes.action.IMPORT。这一类的坑最深。因为从 Android 11API 30开始发起查询的那一方必须声明queries才能看到你。如果你的应用没出现在某个第三方客户端的列表里而它在别的地方都正常那多半不是你的问题而是对方没做包可见性适配。这一点我在第 5 节会展开讲因为它特别容易让人误判成自己的 bug。1.4 桌面图标与快捷方式另一条通道还有一条通道经常被混进来讨论桌面上的应用图标。它靠的是MAINLAUNCHER这一对组合。这个只要是个正常应用就有不用额外配置。真正需要手动做的是“固定快捷方式”pinned shortcut和“动态快捷方式”前者能让你的应用把自己的入口钉到桌面上后者还能让长按图标时弹出快捷菜单。如果你做的是电视端记得还要加上LEANBACK_LAUNCHER否则在 TV 桌面上找不到你的应用。把四条通道列成一张表看起来更直观列表类型触发动作需要声明的 action是否受包可见性影响系统分享面板用户点“分享”ACTION_SEND/ACTION_SEND_MULTIPLE否系统自身豁免打开方式弹窗用户点文件或链接ACTION_VIEW否第三方自建列表对方调用查询接口对方指定的 action常见是ACTION_SEND是对方需声明queries桌面与快捷方式用户查看桌面或长按图标MAINLAUNCHER、ShortcutManager否2. Intent Filter 的匹配逻辑决定你能不能被“选中”把列表形态分清楚之后真正的重头戏是匹配规则。intent-filter不是“写上就生效”的声明而是一组必须同时满足的约束条件。系统在解析一个隐式 Intent 时会拿它去和每个 filter 做三个维度的比对action、category、data。三个维度全部通过这个组件才会被放进候选列表。2.1 action、category、data 三个维度的判定顺序先说 action。规则很简单Intent 里的 action 必须能在 filter 的action列表里找到同名项。反过来filter 里多写几个 action 是允许的不影响匹配。所以一个 Activity 同时处理ACTION_SEND和ACTION_VIEW是完全可以的只要把它们写在同一个 filter 或多个 filter 里都行。category 的规则稍微绕一点。判定标准是Intent 里带的每一个 category都必须在 filter 里找得到对应项filter 里多出来的 category 不会导致匹配失败。这条规则带来一个非常关键的推论——因为系统在用startActivity()启动隐式 Intent 时会自动帮你加上CATEGORY_DEFAULT所以任何想被隐式调起的 filter都必须写上category android:nameandroid.intent.category.DEFAULT /data 维度是最复杂的它包含 MIME 类型和 URI 结构scheme、host、port、path、pathPattern、pathPrefix两部分。判定原则是filter 里声明了什么Intent 就必须提供对应的东西。声明了 mimeTypeIntent 的 type 就得匹配声明了 schemeIntent 的 data URI 就得带上这个 scheme。2.2 CATEGORY_DEFAULT 缺席是新手第一大坑我见过太多这样的 Manifestaction 写了mimeType 也写了测试的时候用adb shell am start手动能拉起来但一到真实的分享面板里就消失。原因几乎全是漏了CATEGORY_DEFAULT。这里有个细节值得说清楚adb am start如果显式指定了包名和类名那就是显式 Intent压根不经过匹配流程所以它能启动成功并不代表配置正确。要验证匹配必须用不带-n参数的方式adb shell am start -a android.intent.action.SEND -t text/plain \ --es android.intent.extra.TEXT hello from adb这条命令走的是完整的隐式解析流程能弹出来的选择器里如果没你的应用那 Manifest 一定有问题可以放心去改配置不用怀疑别的地方。2.3 mimeType 与 scheme 的组合写法与实际效果接下来是 data 的组合写法这块最容易写出“看起来对、实际只对一半”的配置。举几个我实际调过的例子对照着看会清楚很多。想接收所有文本分享intent-filter action android:nameandroid.intent.action.SEND / category android:nameandroid.intent.category.DEFAULT / data android:mimeTypetext/plain / /intent-filter想接收所有图片分享intent-filter action android:nameandroid.intent.action.SEND / category android:nameandroid.intent.category.DEFAULT / data android:mimeTypeimage/* / /intent-filter想处理 PDF 的“打开方式”intent-filter action android:nameandroid.intent.action.VIEW / category android:nameandroid.intent.category.DEFAULT / data android:schemecontent android:mimeTypeapplication/pdf / /intent-filter第三个例子里scheme和mimeType写在同一个data里意味着两者必须同时满足Intent 的 type 得是 PDFdata URI 得是content://开头。如果你的目标文件是file://开头的那就匹配不上需要再补一个 filter。这是个典型的“自测能过、用户报障”的配置。另外提醒一句mimeType用*/*可以匹配全部但代价是你会出现在几乎所有分享场景里用户体验反而变差。我一般按业务需要精确到image/*、video/*、text/plain这几档够用就行。2.4 exported 与 Android 12 的显式声明要求从 Android 12API 31开始凡是声明了intent-filter的组件必须显式写出android:exported否则安装时会直接失败。很多人升级 targetSdk 之后突然装不上报的就是这个。被外部应用调起的分享接收 Activity、打开方式 Activity都得写android:exportedtrue。反过来也要注意如果你的intent-filter只是为了应用内部跳转那就该写exportedfalse。把不该暴露的组件暴露出去一是增加被恶意调起的风险二是某些应用商店审核会直接卡你。3. 分享接收端怎么写才算“合格”配置写对只是拿到了入场券点进去之后的表现才是用户真正的体验。分享接收这块看着简单实际上有几个点处理不好就会让人觉得很“山寨”白屏卡顿、第二次分享拿到旧数据、分享完还得手动退两层。3.1 用透明 Activity 承接处理完就结束最佳实践是给分享接收单独开一个 Activity主题设为半透明或无界面。它只做一件事从 Intent 里取出内容落到自己的存储或数据库里然后finish()并给一个 Toast 或 Snackbar 反馈。绝对不要让用户跳进你的主界面再自己去找——那种体验非常割裂。主题可以这样配activity android:name.receiver.ShareReceiverActivity android:exportedtrue android:excludeFromRecentstrue android:themestyle/Theme.Transparent android:label存到我的笔记 intent-filter action android:nameandroid.intent.action.SEND / category android:nameandroid.intent.category.DEFAULT / data android:mimeTypetext/plain / /intent-filter /activityandroid:label这一项要特别留意它就是用户在分享面板里看到的文字。不写的话系统会退而使用应用名多个接收入口就会显示成一模一样的名字用户根本分不清点哪个。3.2 EXTRA_TEXT、EXTRA_STREAM 与 ClipData 的真实差异取数据这一步坑主要集中在两种来源不一致上。老式的分享走Intent.EXTRA_TEXT和Intent.EXTRA_STREAM新式的走Intent.getClipData()。同一个内容不同应用可能只填其中一种。稳妥的取值顺序是先看clipData为空再回退到EXTRA_STREAM最后看EXTRA_TEXT。val uriList mutableListOfUri() intent.clipData?.let { clip - for (i in 0 until clip.itemCount) { clip.getItemAt(i).uri?.let(uriList::add) } } if (uriList.isEmpty()) { intent.getParcelableExtraUri(Intent.EXTRA_STREAM)?.let(uriList::add) } val text intent.getStringExtra(Intent.EXTRA_TEXT)这段代码我从老项目里抄出来用了好几年能覆盖绝大多数来源。注意getParcelableExtra在新版本 SDK 上建议用带类型的重载避免泛型擦除带来的警告。3.3 多图分享与 ACTION_SEND_MULTIPLE用户一次选九张图分享出去走的是ACTION_SEND_MULTIPLE取数据的 key 还是EXTRA_STREAM但类型变成了ArrayListUri。这个必须单独声明一个 filter别指望ACTION_SEND那个能兜住intent-filter action android:nameandroid.intent.action.SEND_MULTIPLE / category android:nameandroid.intent.category.DEFAULT / data android:mimeTypeimage/* / /intent-filter有个细节值得单独说多张图片如果尺寸较大接收时不要在主线程一次性全部解码。我就吃过这个亏九张 4K 图直接在主线程BitmapFactory.decodeStreamApp 直接 ANR。正确做法是把 Uri 先落库真正的读取和压缩放到子线程或 WorkManager 里排队做。3.4 URI 授权为什么你的 FileProvider 读不到文件从 Android 7 开始跨应用传file://URI 会抛出FileUriExposedException必须换成content://加FileProvider。这一步对发送方和接收方都有要求发送方要grantUriPermission或者给 Intent 加FLAG_GRANT_READ_URI_PERMISSION接收方才有权读。接收方这边有个常见的误判很多人以为拿到 Uri 就能一直用于是把它存进数据库过两天再读就报SecurityException。原因是这个授权是临时的跟发起方的进程生命周期绑定。要长期持有必须在onCreate里先调用intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)然后在拿到 Uri 之后立刻把它拷贝到你自己的目录或者调用contentResolver.takePersistableUriPermission()申请持久化读权限——后者需要发送方的 Intent 里带了对应的持久化 flag 才有效。我一般直接拷贝省心。4. 直连分享让头像出现在分享面板第一排分享面板的第一排除了应用图标有时候还会冒出一两个带圆形头像的“人”或者“目标”点一下直接把内容发给特定对象。这个能力叫直连分享Direct Share能显著提升你的应用在分享场景的曝光位置值得单独做。4.1 从 ChooserTargetService 到 Sharing Shortcuts 的演进早期的直连分享靠ChooserTargetService实现需要在 Manifest 里注册一个 service 并绑定到某个 Activity 上。这套 API 在 Android 10 就被标记为废弃了现在还在用的基本是历史遗留项目。目前的正确做法是Sharing Shortcuts通过ShortcutManager推送动态快捷方式并给这些快捷方式打上分享分类的标签系统会自动把它们呈现在分享面板的第一排。相比旧方案它不需要额外注册 service直接复用你已经为长按菜单做的那套快捷方式逻辑成本低很多。4.2 动态 Shortcut 的推送与刷新时机推送逻辑长这样val shortcut ShortcutInfoCompat.Builder(context, target_$uid) .setShortLabel(displayName) .setLongLabel(displayName) .setIcon(IconCompat.createWithBitmap(avatarBitmap)) .setIntent( Intent(context, MainActivity::class.java).apply { action Intent.ACTION_VIEW putExtra(target_uid, uid) } ) .setLongLived(true) .setCategories(setOf(android.shortcut.conversation)) .build() ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)这里有几个必须注意的点。第一快捷方式的 id 必须稳定通常是“业务前缀 对象 id”如果你每次都用随机 id系统那边会不断累积再清理头像会闪。第二setLongLived(true)让快捷方式在应用更新后仍然保留不加的话每次升级都清零。第三分类标签的具体取值建议对照当前 SDK 的常量来写不同版本对分类的枚举有过调整别直接照抄网上的旧代码。刷新时机也很讲究。快捷方式本质上是一份缓存系统有数量上限不同厂商 ROM 从 4 个到 15 个不等。我一般在这几个节点刷新应用启动时同步一次、用户打开最近联系人列表时刷新、以及新增交互对象时增量推送。千万不要在列表滚动时同步调用那会拖慢主线程。4.3 头像不显示、排序不变的排查顺序头像不出来的原因排下来大概是这几种图标尺寸不对建议 96dp 以上太小会被裁剪、用了IconCompat.createWithResource但资源是矢量的且带了复杂裁剪路径、以及快捷方式数量超过 ROM 上限被挤掉了。排序不变化则通常是系统缓存没刷新可以先把所有动态快捷方式清掉再重新推一遍ShortcutManagerCompat.removeAllDynamicShortcuts(context)这个操作在开发期很好用正式包里慎用它会把用户已有的快捷方式一起清掉。5. 包可见性Android 11 之后“别人看不到你”的真正原因这是最容易被误判成自己 bug 的一类问题。表现是你的应用在系统分享面板里明明可见但某个第三方客户端自己实现的“导入来源”列表里就是没有你。很多人会反复检查自己的 Manifest怎么改都没用因为问题根本不在你这边。5.1 queries 该由谁声明Android 11 收紧了应用之间的可见性。默认情况下一个应用只能看到自己、系统应用以及少数几个被豁免的包。想看到别的应用必须在自己的 Manifest 里声明queries明确写出你要查询的 action、mimeType 或者具体的包名。queries intent action android:nameandroid.intent.action.SEND / data android:mimeType*/* / /intent /queries所以逻辑是这样的如果你的应用要出现在别人自建的列表里需要对方声明 queries如果你要在自己的应用里列出别人需要你自己声明 queries。这条链路上任何一环缺失列表就是空的。遇到“只有某个 App 看不见我”的情况先去确认对方的 targetSdk 是不是 30 以上基本能定位一半问题。另外提醒一句用QUERY_ALL_PACKAGES这种全量权限虽然一劳永逸但应用商店对它卡得很严必须有明确且必要的业务理由才能通过审核不要图省事就加。5.2 系统面板与第三方自建列表的差异系统组件比如分享面板、打开方式弹窗、设置里的默认应用页面在包可见性上是豁免的它们能看到全部应用。这就是为什么同一份 Manifest在系统面板里正常在第三方列表里消失。还有一类容易被忽略即便对方声明了 queries也可能只声明了某个具体 action。比如对方只查询了ACTION_SEND加text/plain而你的 filter 写的是image/*那你当然不在列表里。遇到这种只能去对照对方文档或者反编译看它的查询条件——当然前提是合规合法地做不要触碰用户隐私相关的边界。5.3 调试用 adb 与 PackageManager 验证清单验证自己到底能被谁看见最靠谱的方式是用PackageManager主动查一遍。写个临时的调试入口val intent Intent(Intent.ACTION_SEND).apply { type text/plain } val list packageManager.queryIntentActivities(intent, 0) list.forEach { Log.d(resolve, ${it.activityInfo.packageName}/${it.activityInfo.name}) }如果这里能查到你自己说明系统层面没问题问题在对方。也可以用命令行快速看某个包的解析表adb shell dumpsys package com.example.myapp | grep -A 30 Activity Resolver Table这份输出会列出该应用所有参与隐式解析的组件包括 action、category、mimeType是排查配置问题的第一手资料比反复读 Manifest 直观得多。6. 一套可复用的自检流程与踩坑记录前面讲的都是分模块的知识点实际排障时更需要一条固定的检查链路。我梳理了一套自己用了很久的流程从最快能验证的一步开始逐层往上能省掉大量来回改配置的时间。6.1 从 am start 到真机点按的六步验证第一步用不带-n的am start命令触发一次隐式匹配看选择器里有没有你的应用。第二步如果第一步失败去dumpsys package里确认 filter 是否被正确解析。第三步检查CATEGORY_DEFAULT和exported是否齐全。第四步用queryIntentActivities在代码里复现一次匹配确认是不是环境问题。第五步真机上从至少两个不同的来源应用发起分享排除单一来源的特殊行为。第六步检查快捷方式的数量和 id 是否稳定。这六步走下来基本上能覆盖九成以上的“进不了列表”问题。剩下的那一成多半是 ROM 定制导致的怪现象。6.2 我踩过的几个坑顺便避一下第一个坑是用显式 Intent 自测。前面提过带包名和类名的am start不经过匹配测出来的“成功”是假象。第二个坑是在 filter 里给了android:priority1000期待排序靠前。这个属性对隐式 Intent 解析无效系统面板的排序有自己的算法别白费力气。第三个坑是接收 Activity 复用了主界面。分享面板点进来直接落在主页用户以为没反应其实是内容存了但没有反馈。一定要独立 Activity 加明确的完成提示。第四个坑是忘了处理ACTION_SEND_MULTIPLE。单图分享正常多图分享列表里就没有你这个差异非常隐蔽。!-- 这两个 filter 缺一不可 -- action android:nameandroid.intent.action.SEND / action android:nameandroid.intent.action.SEND_MULTIPLE /第五个坑是多开或分身环境下包名变了。有些用户用双开工具实际运行的包名带了后缀你按原包名写的 queries 或者快捷方式就全部失效。这类问题只能靠日志和实际包名去对没有通用解法。6.3 模拟器、多开与定制 ROM 的干扰最后说一个心态层面的经验。我在模拟器上验证通过的配置拿到某些定制 ROM 上偶尔会失效。常见原因包括ROM 把分享面板换成了自家的实现、ROM 对快捷方式数量做了更严格的限制、以及某些省电策略把接收 Activity 的后台启动拦掉了。判断是不是环境问题最快的办法是找一台原生系统或者不同品牌的机器交叉验证。如果只有某一台机器不行而它在别的品牌上都正常那就别死磕了把现象记录下来用dumpsys抓一份现场日志再针对性做兼容处理。我自己的项目里分享接收这块就专门留了一层兜底逻辑——如果发现是从分享入口进来的但内容为空就直接弹一个提示而不是静默失败这样即使在某些 ROM 上取不到数据用户至少知道发生了什么而不是怀疑应用坏了。