PackageVisibility 的查询白名单,Android 11 后的适配
PackageVisibility 的查询白名单,Android 11 后的适配
Android 11(API 30)引入的 Package Visibility 限制,是我这几年做应用适配时踩坑最多、也最被低估的一个变更。很多人知道它,但真到线上出问题的时候,往往是崩溃日志里突然冒出一个 PackageManager.NameNotFoundException,或者某个第三方 SDK 的分享功能莫名其妙失效,才回头来补这个功课。我自己也是这么过来的——2020 年 Android 11 正式发布前,Google 在 Beta 阶段就推送了这个改动,但当时文档写得模糊,<queries> 标签的行为和预期不完全一致,导致我前后花了将近两周时间才把几个核心模块的适配彻底搞明白。
从一次线上崩溃说起
事情是这样的。我们当时有一个功能,需要检测用户是否安装了某几个特定的社交应用,来决定分享面板的按钮展示逻辑。代码写得非常直接:
List<ResolveInfo> activities = getPackageManager()
.queryIntentActivities(shareIntent, 0);这段代码在 Android 10 及以下跑了好几年,没有任何问题。Android 11 的测试机上,分享面板突然少了一半的图标。更诡异的是,不是全部消失,而是部分应用能看到,部分看不到。我第一反应是 Intent filter 匹配出了问题,折腾了半天才发现,能看到的那几个,恰好是我们应用通过 <uses-permission> 或者隐式依赖已经建立关联的应用。
Google 的官方文档里,这个变更被描述为"提升隐私性",核心逻辑是:默认情况下,你的应用只能看到系统认为与它"相关"的其他应用。这个"相关"的定义非常具体——你的目标 API 是 30 或更高时,以下三种情况可以免白名单查询:
<uses-permission> 声明了需要与之交互的应用(比如声明了 CAMERA 权限,就能查到相机应用)除此之外,任何显式的 getPackageInfo()、queryIntentActivities()、queryIntentServices() 调用,如果目标应用不在白名单里,返回结果就会被过滤,甚至直接抛异常。
`<queries>` 标签的语法细节与编译期行为
适配的核心是在 AndroidManifest.xml 里加 <queries> 节点。这个节点是 Android 11 新增的,但有个容易忽略的前提:它只在 compileSdkVersion 和 build-tools 足够新的时候才生效。
我当时的项目 compileSdkVersion 是 29,build-tools 也是 29.x,直接在 manifest 里写 <queries>,编译居然通过了,但打包后的 APK 里完全找不到这个节点的痕迹。APK 解析工具(aapt2 dump badging)看不到,运行时自然也不生效。这是因为旧版本的 aapt2 根本不识别这个标签,直接静默丢弃了。
升级到 compileSdkVersion 30 和对应的 build-tools 30.0.0 之后,编译行为才正常。但这里又有一个坑:如果你的 minSdkVersion 低于 30,<queries> 标签在旧系统上怎么办?实测下来,Android 10 及以下的系统会忽略这个不认识的标签,不会导致安装失败,这是 Google 特意做的兼容处理。但前提是你要用新版本的构建工具来打包,否则标签被丢弃,高版本系统也看不到。
<queries> 的语法有三种形式,对应不同的查询需求。
第一种是指定具体的包名:
<queries>
<package android:name="com.example.app" />
</queries>这是最精确、最推荐的方式。只声明你确实需要交互的应用,系统过滤时开销最小,审核时也最不容易被 Google Play 挑刺。
第二种是通过 Intent filter 来声明:
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
</queries>这种形式用于你需要查询能处理某类 Intent 的应用列表。我们分享面板的适配就是用的这种。但要注意,这里的 Intent 声明必须和你实际查询时用的 Intent 匹配,不是随便写的。我试过把 mimeType 写成 __PLACEHOLDER_ITALIC_0__,结果在 Android 11 上返回的列表比预期多了很多系统组件,而在 Android 12 上又被收紧了,行为不一致。
第三种是声明特定的权限:
<queries>
<permission android:name="android.permission.BLUETOOTH" />
</queries>这种用得比较少,它允许你查询持有特定权限的应用。实际开发中,我几乎没有遇到过必须用这种方式的场景。
`PackageManager` 各 API 的行为差异
真正让我头疼的是,Package Visibility 限制并不是均匀地作用在所有 PackageManager API 上的。不同 API 的返回行为有微妙差别,有些返回空列表,有些返回部分过滤结果,有些直接抛异常。
getPackageInfo(String packageName, int flags) 是最危险的。目标应用不可见时,它直接抛 PackageManager.NameNotFoundException。这个异常在 Android 10 及以下只意味着应用未安装,现在多了一个含义:应用可能装了,但你没权限看到。我们的崩溃日志里,这个异常的出现频率在 Android 11 用户中涨了将近三倍,都是旧代码没适配。
getApplicationInfo() 的行为和 getPackageInfo() 一致,也是抛异常。
queryIntentActivities() 和 queryIntentServices() 相对温和,返回过滤后的列表,不会抛异常。但这里有个陷阱:如果你查询的 Intent 没有匹配到任何可见应用,返回的列表是空的。这和"应用未安装"从调用方视角无法区分。我们的分享面板就是因此出现了"按钮消失"的 bug,而不是崩溃。
resolveActivity() 的行为更隐蔽。如果最佳匹配的应用不可见,它返回 null。这个 null 在旧系统上意味着没有应用能处理这个 Intent,现在多了一个可能的原因。
还有一个我踩过的坑:getInstalledApplications() 和 getInstalledPackages()。这两个 API 在 Android 11 上返回的列表也会被过滤,但过滤逻辑和前面几个 API 不完全一致。系统应用、签名相同的应用,即使不在 <queries> 里,也可能出现在结果中。我写过一段代码,用 getInstalledPackages() 来遍历所有应用做某种统计,Android 11 上结果直接少了 60% 以上,而且剩余的应用分布毫无规律,排查了很久才发现是这个原因。
与 `targetSdkVersion` 的关联
Package Visibility 限制的实际生效和 targetSdkVersion 强相关,但不是唯一的决定因素。
如果你的 targetSdkVersion < 30,即使在 Android 11+ 的设备上运行,系统也会给你一种"兼容模式":部分 API 的查询限制会放宽,但不是全部。具体来说,queryIntentActivities() 这类返回列表的 API 在兼容模式下基本能正常工作,但 getPackageInfo() 这种直接查指定包名的,仍然会受限制。
这个设计让我非常困惑。Google 的文档里说"targetSdkVersion 低于 30 的应用不会受到 Package Visibility 限制",但实际测试下来并非如此。我在 Pixel 4 Android 12 设备上做了详细测试:
targetSdkVersion 29,调用 getPackageInfo("com.whatsapp", 0),WhatsApp 已安装但 <queries> 未声明,结果:抛 NameNotFoundExceptiontargetSdkVersion 29,调用 queryIntentActivities(shareIntent, 0),结果:正常返回 WhatsApp也就是说,兼容模式只覆盖了部分 API。这个细节在官方文档里几乎没有明确说明,我是通过实际测试和看 AOSP 源码才确认的。源码路径在 frameworks/base/core/java/android/app/ApplicationPackageManager.java,具体逻辑涉及 PackageManagerService 的 canViewInstantApp() 和 filterAppAccess() 方法,判断条件里同时检查了调用方的 targetSdkVersion 和 API 类型。
第三方 SDK 的适配困境
Package Visibility 的限制不只是影响你自己的代码,第三方 SDK 的适配才是真正的灾难。
2020 年底到 2021 年上半年,我接触到的 SDK 里,大概有一半完全没有适配这个变更。表现各异:有的直接崩溃,有的功能静默失效,有的在日志里疯狂打印异常但表面上看起来正常。
最典型的例子是某主流社交分享 SDK(具体名字不说了,国内开发者应该都用过)。它的分享功能内部实现是:
try {
context.getPackageManager().getPackageInfo("com.xxx.app", 0);
// 认为已安装,展示分享按钮
} catch (NameNotFoundException e) {
// 认为未安装,隐藏按钮
}Android 11 上,如果宿主应用没有在 <queries> 里声明这个包名,直接进 catch 块,分享按钮消失。SDK 的文档里直到 2021 年 3 月才补上说明,要求宿主应用自行添加 <queries> 声明。但问题是,SDK 对接了十几个分享目标,宿主应用不可能全部声明,也不应该全部声明——用户没装的应用,声明了也没意义,还增加审核风险。
更坑的是某些广告 SDK。它们为了"精准投放",会遍历设备上安装的应用列表来构建用户画像。Android 11 之后,这种操作直接失效。我见过一个 SDK 的适配方案:在 AndroidManifest.xml 里声明了超过 200 个 <package> 节点,几乎覆盖了国内主流应用。这种写法技术上能工作,但 Google Play 的审核政策明确禁止"过度声明查询范围",有被下架的风险。国内渠道没这个限制,但包体积和安装时的解析开销都增加了。
我自己的处理方式是:对于核心分享功能,把 <queries> 声明和分享面板的配置做了解耦。应用启动时根据实际安装情况动态决定展示哪些按钮,而不是在 manifest 里写死。具体实现是用 queryIntentActivities() 来替代 getPackageInfo(),前者只需要声明 Intent filter,不需要枚举具体包名,灵活性高很多。
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
</queries>运行时根据返回的 ResolveInfo 列表,匹配 activityInfo.packageName 来识别具体应用。这样即使某个新出现的分享目标应用,只要它能处理标准的 SEND Intent,不需要更新 manifest 就能支持。
`QUERY_ALL_PACKAGES` 权限的争议与替代方案
如果确实需要查询设备上所有应用,Android 11 提供了一个 nuclear option:android.permission.QUERY_ALL_PACKAGES。
这个权限的声明很简单:
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />但它的风险极高。Google Play 从 2021 年开始对这个权限做严格限制,要求应用的核心功能必须依赖"查询任意已安装应用"的能力才能申请。常见的被批准场景包括:文件管理器、设备管理应用、防病毒软件等。普通应用几乎不可能通过审核。
我做过一个实验。我们有一个内部工具应用,确实需要遍历所有应用来做某种自动化测试,申请了 QUERY_ALL_PACKAGES。2021 年 6 月提交 Google Play,直接被拒,原因是"应用的核心功能不需要此权限"。申诉时提供了详细的技术说明,包括为什么不能用 <queries> 替代(测试目标不固定,需要覆盖任意应用),仍然被拒。最终这个权限只能在内部分发渠道使用。
对于需要"知道某个特定应用是否安装"的场景,如果那个应用不在白名单里,有没有不申请 QUERY_ALL_PACKAGES 的替代方案?
我试过几种 hack,效果都不理想。
一种思路是通过 PackageManager 的 getPackagesForUid() 间接查询。但这个 API 的行为在 Android 11 上同样受限,返回结果会被过滤。
另一种思路是尝试启动目标应用的特定 Activity,通过 startActivity() 的异常来判断。ActivityNotFoundException 确实能区分"应用不存在"和"应用存在但入口不对",但这需要目标应用有 exported 的 Activity,而且很多应用没有。更麻烦的是,这种方式会触发系统的应用启动动画,用户体验极差,只能作为后台判断的话,需要额外处理 Intent.FLAG_ACTIVITY_NEW_TASK 等标志,复杂度很高。
还有一种思路是通过 ContentProvider 查询。如果目标应用暴露了特定的 ContentProvider,可以尝试 getContentResolver().query()。但 ContentProvider 的可见性在 Android 11 上同样受限制,而且这种方式依赖目标应用的具体实现,不具备通用性。
实际项目中,我的建议是:重新审视产品需求,为什么一定要知道某个应用是否安装?如果是为了功能入口的展示,能否改为"尝试调用,失败则降级"?比如分享功能,不要预检测,直接唤起系统分享面板(Intent.createChooser()),让系统来处理应用选择。这样既规避了 Package Visibility 限制,也符合 Android 的设计哲学。
Android 12 的进一步收紧
Android 12(API 31)在 Package Visibility 上又加了一层限制,这是很多人没注意到的。
首先是 targetSdkVersion 31 的应用,<queries> 标签的解析更加严格。之前 Android 11 上某些"模糊匹配"的写法,比如 <intent> 里只写 action 不写 data,在 Android 12 上可能不再生效。我遇到的具体 case 是查询浏览器应用:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
</intent>
</queries>Android 11 上这个声明能返回大部分浏览器,Android 12 上返回空列表。必须补全 data 的 scheme:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
</queries>这个改动没有在任何 release note 里明确提到,我是通过对比 AOSP 源码发现的。Android 12 的 IntentFilter.java 里对 match() 逻辑做了调整,<queries> 里的 Intent 必须足够具体才能匹配成功。
其次是 QUERY_ALL_PACKAGES 的审核进一步收紧。2022 年初,Google Play 开始要求申请此权限的应用必须提供视频演示,证明核心功能确实依赖该权限。很多之前"蒙混过关"的应用被下架或要求整改。
还有一个更隐蔽的变更:Android 12 引入了 android:usesPermissionFlags 属性,可以和 <uses-permission> 配合来影响 Package Visibility。具体语法是:
<uses-permission android:name="android.permission.BLUETOOTH"
android:usesPermissionFlags="neverForLocation" />这个 flag 的本意是声明蓝牙权限不用于定位,但它在 Package Visibility 的语境下有个副作用:声明了某些权限的应用会被视为"相关应用",从而绕过 <queries> 限制。这个机制的设计意图是合理的——如果你声明了蓝牙权限,系统认为你可能需要和蓝牙相关的应用交互——但实际行为很难预测,我不建议在适配策略中依赖这个机制。
实际项目中的适配 checklist
经过这几年的迭代,我现在处理 Package Visibility 相关需求时,会按这个流程来:
第一步,确定 compileSdkVersion 至少 30,build-tools 对应升级。这是基础,否则 <queries> 标签不生效。
第二步,梳理所有用到 PackageManager 查询 API 的代码。重点排查 getPackageInfo()、getApplicationInfo()、resolveActivity(),这些在限制生效时会抛异常或返回 null。queryIntentActivities() 等返回列表的 API 相对安全,但要确认业务逻辑是否能处理空列表。
第三步,把"查包名"的调用尽量改为"查 Intent"。后者只需要在 <queries> 里声明 Intent filter,不需要维护具体的包名列表。对于确实需要查特定包名的场景(比如和某个 SDK 的深度集成),用 <package> 节点精确声明。
第四步,测试覆盖。必须在 Android 11+ 的真机上测试,模拟器的行为和真机有差异。我遇到过模拟器上 queryIntentActivities() 返回完整列表,但真机上被过滤的情况,原因是模拟器预装的应用签名和系统签名一致,被判定为"相关应用"。
第五步,第三方 SDK 的 manifest 合并检查。用 manifest-merger 工具查看最终合并后的 manifest,确认 SDK 有没有偷偷声明 <queries> 或 QUERY_ALL_PACKAGES。有些 SDK 为了自身功能正常,会在 aar 的 manifest 里写很宽的查询声明,这可能带来审核风险。
./gradlew app:processDebugManifest
# 查看 build/outputs/logs/manifest-merger-debug-report.txt第六步,对于确实需要 QUERY_ALL_PACKAGES 的场景,准备充分的审核材料。包括功能演示视频、技术说明文档、替代方案不可行的论证。普通应用建议直接放弃这个权限,重新设计产品流程。
一个性能相关的发现
最后分享一个不太为人知的细节。<queries> 声明的数量和复杂度,会影响应用冷启动时的 PackageManagerService 解析开销。
我在 Android 12 设备上做过测量,用 Trace.beginSection() 和 Trace.endSection() 包裹 Application.onCreate() 到第一个 Activity 可见的完整流程。<queries> 里声明 50 个 <package> 节点时,冷启动时间大约 380ms;增加到 200 个节点时,冷启动时间涨到 520ms 左右。这个开销来自 PackageManagerService 在应用启动时对查询白名单的预解析和缓存构建。
当然,这个测试是在控制其他变量的情况下做的,实际项目中影响启动时间的因素很多。但结论是明确的:<queries> 不是越多越好,应该精确声明真正需要查询的应用。那种为了兼容第三方 SDK 而大量枚举包名的做法,有隐性的性能代价。
关于国内生态的特殊性
Package Visibility 是 AOSP 的机制,但国内各厂商的定制系统在这个基础上做了不同程度的二次限制。
华为 HarmonyOS 2.0/3.0 兼容 Android 11 的 API 行为,但 <queries> 的解析在某些早期版本有 bug,表现为 Intent filter 匹配时 scheme 和 host 的组合判断错误。我们在 HarmonyOS 2.0.1 上遇到过 http:// 匹配不到浏览器应用的情况,升级到 2.0.1.120 后修复。
小米 MIUI 12.5 基于 Android 11,但在 QUERY_ALL_PACKAGES 权限上比 Google Play 更严格。即使不通过 Google Play 分发,MIUI 的应用商店审核也会检查这个权限的合理性。我们有一个内部测试工具,通过小米企业分发渠道发布,同样因为 QUERY_ALL_PACKAGES 被拒,理由和 Google Play 类似。
OPPO ColorOS 11 有个特殊行为:<queries> 里声明的系统应用包名,在某些情况下仍然查询不到。具体是哪些系统应用不固定,和 ColorOS 的预装应用策略有关。我们的 workaround 是对于系统级功能(比如相机、电话),不依赖 <queries> 预检测,直接尝试调用,失败时给用户明确的错误提示。
这些厂商差异没有统一的文档,只能靠实际测试和社区反馈来积累。我维护了一个内部 wiki,记录各厂商在 Package Visibility 上的已知问题,每次发版前会针对性验证。
回到那个分享面板
文章开头提到的分享面板问题,最终的解决方案是这样的:
Manifest 里只声明通用的 Intent filter:
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="image/*" />
</intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
</queries>运行时查询:
List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);然后根据 ResolveInfo 的 loadLabel() 和 loadIcon() 动态构建分享面板。不再预检测"微信是否安装"、"微博是否安装",而是让系统返回能处理该 Intent 的所有可见应用,再按我们的优先级排序展示。
这个方案在 Android 10 及以下行为不变,Android 11+ 自动适配 Package Visibility 限制,也不需要维护具体的包名列表。唯一的代价是分享面板的排序逻辑变复杂了,因为返回的应用集合不确定,但这个是可接受的 trade-off。
对于确实需要深度集成的特定应用(比如微信分享需要调用 SDK 而不是标准 Intent),我们在用户点击对应按钮时才做检测,而不是在面板展示时预检测。检测方式也从 getPackageInfo() 改为尝试 bind 微信的特定 Service,失败则认为未安装或不可见,给用户提示"请安装微信"。
整个适配周期大约两周,但涉及的代码改动其实不多。大部分时间花在理解 <queries> 的精确行为、测试各厂商的差异、以及和第三方 SDK 供应商沟通他们的适配计划上。现在回头看,Package Visibility 是一个设计合理的隐私特性,但 Google 在推出时的文档完备性和开发者沟通做得不够好,导致适配成本被放大了。如果当时有现在这么详细的官方指南和示例代码,可能几天就能搞定。