☰
Android PackageManager 深度解析:Manifest 元数据与运行时决策机制
2026/10/1 17:41:44 网站建设 项目流程

1. 为什么 PackageManager 不是“包管理器”那么简单?

很多人第一次在 Android 开发中看到PackageManager,下意识就把它当成 Linux 里的apt或 macOS 的brew——一个负责安装、卸载、查询 APK 的工具类。我刚入行时也这么想,直到在一次灰度发布中,线上用户反馈“点开应用图标没反应”,日志里只有一行ActivityNotFoundException,而 APK 明明已安装。排查三天后才发现,问题出在PackageManager对intent-filter的匹配逻辑上:它不是简单查表,而是按优先级、匹配权重、动态注册状态、权限约束、设备特性适配等七层规则实时计算的。那一刻我才意识到,PackageManager是整个 Android 应用生态的“交通调度中心”,它不只管“有没有这个包”,更决定“这个包能不能被谁、在什么条件下、以什么方式被调用”。

它和AndroidManifest.xml是硬绑定的共生关系——Manifest 是它的“宪法”,定义了应用的法定身份、能力边界与对外接口;而PackageManager是执行者,把这份宪法翻译成运行时的权限校验、组件路由、版本仲裁与沙箱隔离。你写的每一条<activity>、每一个<uses-permission>、甚至<meta-data>里的键值对,最终都会被PackageManagerService(PMS)解析成内存中的PackageParser.Package对象,再挂载到Settings类维护的全局注册表里。这不是静态配置读取,而是一套完整的应用生命周期元数据治理体系。

所以,当你调用getPackageInfo("com.example.app", 0)时,你拿到的不只是包名和版本号,而是包含activities、services、providers、receivers四大组件清单、签名证书指纹、sharedUserId、applicationInfo(含targetSdkVersion)、requestedPermissions及其protectionLevel的完整快照。这些字段背后,是 PMS 在系统启动时扫描/data/app/、/system/app/、/vendor/app/三个目录,逐个解析 APK 的AndroidManifest.xml,并校验签名、检查兼容性、合并uses-sdk约束后的结果。它甚至会为每个ContentProvider生成UriPermission白名单,为BroadcastReceiver构建IntentResolver的 trie 树结构。这种深度耦合,决定了你无法脱离 Manifest 去理解 PackageManager,也无法绕过 PackageManager 去实现真正的组件间通信。

提示:PackageManager的所有公开 API(如queryIntentActivities()、resolveActivity())都只是 PMS 内部复杂决策逻辑的薄封装。它们返回的结果,是 PMS 综合了intent.action、intent.category、intent.data、intent.extras、callerUid、callerPid、callingPackage、deviceFeatures(如是否带摄像头)、screenLayout(如是否是折叠屏)等至少 12 个维度后给出的“最优解”。这不是缓存查询,而是实时计算。

2. Manifest 是它的“宪法”,但你写的每一行都在触发底层校验链

AndroidManifest.xml看似只是 XML 文件,实则是向PackageManagerService提交的一份“应用宪章”。PMS 在安装 APK 时,会启动一套完整的解析-校验-注册流水线,任何一行不符合规范的代码,都会在不同阶段被拦截。我曾遇到一个线上崩溃,堆栈指向PackageManager.getPackageInfo()抛出NameNotFoundException,但adb shell pm list packages | grep com.xxx却能查到包名。最后发现,是AndroidManifest.xml中误写了<application android:name=".App">,而实际类路径是com.xxx.App,导致 PMS 在构建ApplicationInfo时因类加载失败而拒绝注册该包——它根本没进“已安装”状态,只是躺在/data/app/目录里当个“黑户”。

这套校验链从外到内分五层:

2.1 第一层:XML 结构合法性校验

PMS 使用XmlPullParser解析 Manifest,要求严格符合 DTD 规范。比如<uses-feature>必须放在<application>外层,若写在<application>内部,解析直接失败,安装中断。我见过最隐蔽的错误是 UTF-8 BOM 头:Windows 下用记事本保存的 Manifest,开头三个字节EF BB BF会让XmlPullParser认为第一个标签是乱码,报XmlPullParserException: Unexpected token。解决方案不是改代码,而是用 VS Code 或 Android Studio 重新保存为“UTF-8 无 BOM”。

2.2 第二层:组件声明合规性校验

每个<activity>必须有android:name,且必须是合法类名(不能含空格、特殊符号)。更关键的是exported属性:Android 12+ 强制要求显式声明。若你写<activity android:name=".MainActivity">而不加exported,PMS 会根据intent-filter自动推断——有<intent-filter>则exported="true",否则exported="false"。但推断逻辑在不同 Android 版本有差异,Android 12 推断为false,Android 11 却是true,导致跨版本行为不一致。我的建议是:永远显式写android:exported="true"或"false",绝不依赖推断。

2.3 第三层:权限与签名强约束校验

<uses-permission>声明的权限,PMS 会与frameworks/base/data/etc/下的platform.xml对照。比如你声明<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>,PMS 会检查该权限的protectionLevel是否为signature|privileged,然后比对你的 APK 签名是否与系统签名一致。不一致?直接拒绝安装。这解释了为什么第三方应用无法静默安装 APK——INSTALL_PACKAGES权限只授予系统应用。同理,<permission>自定义权限的protectionLevel(normal/dangerous/signature/signatureOrSystem)决定了 PMS 如何校验调用方签名,signature级别要求调用方与声明方签名完全一致,差一个字节都不行。

2.4 第四层:设备特性与兼容性校验

<uses-feature android:name="android.hardware.camera" android:required="true"/>这行代码,PMS 不会在安装时检查设备是否有摄像头,而是在queryIntentActivities()时动态过滤。但<supports-screens>和<compatible-screens>会影响getPackageInfo()返回的applicationInfo.flags。我曾为折叠屏适配,在 Manifest 中添加<meta-data android:name="android.max_aspect" android:value="2.1" />,结果发现PackageManager在 Android 10 上忽略该字段,Android 11 才开始生效——因为 PMS 的兼容性校验逻辑随系统版本迭代,老版本根本不认识这个 meta-data。

2.5 第五层:Provider Authority 冲突校验

<provider android:name=".MyProvider" android:authorities="com.example.myprovider" />中的authorities是全局唯一字符串。PMS 在注册 Provider 时会检查所有已安装应用的 authorities 列表,一旦重复,安装直接失败,报错INSTALL_FAILED_CONFLICTING_PROVIDER。这是最常被忽视的冲突点。比如你引用了两个 SDK,A SDK 声明authorities="com.example.a.provider",B SDK 声明authorities="com.example.b.provider",看似不同,但若 B SDK 的build.gradle里用了applicationIdSuffix ".debug",而 A SDK 没做适配,调试版 authority 就变成com.example.a.debug.provider,与 B 的com.example.b.provider仍可能冲突。解决方案是:所有 Provider 的 authorities 必须基于applicationId动态生成,写成android:authorities="${applicationId}.provider",并在build.gradle中配置manifestPlaceholders = [applicationId: applicationId]。

注意:<application android:allowBackup="true">这个属性,PMS 会据此决定是否将该应用数据纳入adb backup范围。但更深层的影响是,当allowBackup="true"且未设置android:fullBackupContent时,PMS 会默认备份所有私有目录(/data/data/com.xxx/),包括数据库、SharedPreferences。这导致很多金融类应用因误开此开关,被安全审计打低分。正确做法是:allowBackup="false",或明确指定fullBackupContent="@xml/backup_rules"定义白名单。

3. queryIntentActivities 与 resolveActivity:不是查表,是实时决策树遍历

当你调用pm.queryIntentActivities(intent, 0)获取可用 Activity 列表,或pm.resolveActivity(intent, 0)获取最佳匹配项时,你以为是在查一个静态哈希表?错了。这是PackageManagerService启动一个完整的Intent Resolver 匹配引擎,它要遍历所有已注册的 Activity,对每个<intent-filter>执行四重匹配:

3.1 Action 匹配:精确匹配与通配符逻辑

<intent-filter><action android:name="android.intent.action.VIEW"/></intent-filter>只匹配intent.setAction("android.intent.action.VIEW"),不匹配intent.setAction("android.intent.action.EDIT")。但android.intent.action.*这种通配符不存在——Android 不支持 glob 模式。真正起作用的是Intent.CATEGORY_DEFAULT:当startActivity(intent)时,系统会自动为 intent 添加CATEGORY_DEFAULT,因此你的 Activity 必须在<intent-filter>中声明<category android:name="android.intent.category.DEFAULT"/>,否则无法被隐式启动。我见过太多新手忘记加这一行,对着 Logcat 里ActivityNotFoundException干瞪眼。

3.2 Category 匹配:隐式调用的“通行证”

Category 匹配是 AND 关系。intent.addCategory("android.intent.category.BROWSABLE"); intent.addCategory("android.intent.category.ALTERNATIVE");要求目标 Activity 的<intent-filter>同时包含这两个 category。但CATEGORY_DEFAULT是特例:它由系统自动添加,且只要<intent-filter>中存在任意 category,CATEGORY_DEFAULT就不会被匹配——除非你显式调用intent.addCategory(Intent.CATEGORY_DEFAULT)。这解释了为什么WebView的Intent.createChooser()能列出浏览器,但你的自定义 Activity 却不行:因为浏览器的 Manifest 声明了BROWSABLE,而你的 Activity 只声明了DEFAULT。

3.3 Data 匹配:URI Scheme/Host/Path 的三重门禁

<data android:scheme="https" android:host="example.com" android:pathPrefix="/article"/>要求 intent 的 URI 同时满足 scheme=https、host=example.com、path 以/article开头。这里有个致命陷阱:pathPattern支持*和.通配符,但*只能匹配 0 个或多个非斜杠字符,.*才能匹配任意字符(包括斜杠)。所以pathPattern=".*"匹配/a/b/c,而pathPattern="*"只匹配abc。我曾为分享功能写pathPattern="*", 结果https://example.com/article/123根本不匹配,因为 URI 里有斜杠。正确写法是pathPattern=".*"或更精准的pathPattern="/article/.*"。

3.4 Type 匹配:MIME 类型的动态协商

intent.setDataAndType(uri, "image/*")会触发 PMS 检查 Activity 的<data android:mimeType="image/*"/>。但 MIME 类型匹配有优先级:如果 intent 同时设置了setData()和setType(),PMS 会因冲突抛出android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag。正确姿势是setDataAndType()一次性设置,或用intent.setData(uri).setType("image/*")。更隐蔽的是content://URI:content://com.tencent.wework.fileprovider/external_path/android/data/com.tencent.wework/files/xxx.jpg这种 URI 的 MIME 类型,由FileProvider的getStreamTypes()方法动态返回,PMS 会调用该方法获取真实类型再匹配——这意味着匹配结果取决于运行时代码,而非 Manifest 静态声明。

匹配完成后,PMS 不是简单返回列表,而是为每个匹配项计算一个IntentFilter Resolution Score,公式为:

score = 1000 * actionCount + 100 * categoryCount + 10 * dataTypeCount + 1 * schemeCount

其中actionCount是 intent 中 action 数量(通常为 1),categoryCount是显式添加的 category 数量(不含自动添加的DEFAULT),dataTypeCount是匹配的 mimeType 数量,schemeCount是匹配的 data scheme 数量。分数越高,排序越靠前。resolveActivity()返回的就是最高分的那个,queryIntentActivities()返回的是所有分数 > 0 的列表(按分数降序)。

提示:queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)中的MATCH_DEFAULT_ONLY标志,并非只匹配DEFAULTcategory,而是跳过所有未声明DEFAULTcategory 的 intent-filter。这意味着即使你的 Activity 声明了BROWSABLE和ALTERNATIVE,只要没加DEFAULT,它就不会出现在MATCH_DEFAULT_ONLY的结果里。这是 Android 设计的“安全默认”:隐式启动只走标准路径,避免恶意应用劫持。

4. getPackageInfo 与 getApplicationInfo:深入包元数据的七层嵌套

PackageManager.getPackageInfo(packageName, flags)是最常用也最容易被误解的 API。很多人以为它只是返回一个PackageInfo对象,却不知这个对象是 PMS 内存中PackageParser.Package的深度克隆,其字段层层嵌套,每个字段都对应着 Manifest 的一个解析节点和系统级校验结果。

4.1 PackageInfo 的核心字段解剖

  • packageName: 包名,来自<manifest package="com.example.app">。注意:它与applicationId在构建时可能不同(applicationIdSuffix会修改applicationId,但不改变 Manifest 中的package),PMS 以 Manifest 的package为准。
  • versionName/versionCode: 来自<manifest android:versionName="1.0.0" android:versionCode="100">。versionCode是整数,用于升级判断;versionName是字符串,仅作展示。PMS 在安装时会校验versionCode是否大于已安装版本,否则拒绝升级。
  • signatures:Signature[]数组,是 APK 签名证书的 DER 编码字节数组。getPackageName()返回的String是signatures[0].toCharsString()的 SHA-1 摘要。这是signature级权限校验的依据——PMS 比对调用方与被调用方signatures数组是否完全相等。
  • activities:ActivityInfo[]数组,每个ActivityInfo包含name(全类名)、exported(是否导出)、enabled(是否启用)、permission(启动所需权限)、processName(所在进程)、theme(主题资源 ID)等。theme字段的值是R.style.AppTheme对应的整数 ID,PMS 在解析时已将其转换为Resources系统可识别的格式。
  • services:ServiceInfo[],类似 ActivityInfo,但多了foregroundServiceType(前台服务类型,Android 10+ 引入),PMS 会校验该类型是否在AndroidManifest.xml的<uses-permission>中声明对应权限(如FOREGROUND_SERVICE_SPECIAL_USE)。

4.2 ApplicationInfo:应用沙箱的宪法性文件

PackageInfo.applicationInfo是更关键的对象,它定义了应用的运行时身份:

  • sourceDir: APK 文件路径,如/data/app/~~abc123==/com.example.app-xyz123==/base.apk。这是 PMS 安装时分配的唯一路径,DexClassLoader加载 dex 就靠它。
  • publicSourceDir: 与sourceDir相同,但某些系统应用(如systemui)可能不同,用于分离代码与资源。
  • dataDir: 应用私有数据目录/data/data/com.example.app/。PMS 在安装时创建该目录并设为700权限(仅属主可读写),这是 Android 沙箱的核心。
  • nativeLibraryDir: so 库路径,如/data/app/~~abc123==/com.example.app-xyz123==/lib/arm64-v8a/。PMS 根据abiFilters和设备 CPU 架构选择对应目录。
  • flags: 位掩码,FLAG_SYSTEM表示系统应用,FLAG_DEBUGGABLE表示可调试(来自android:debuggable="true"),FLAG_ALLOW_BACKUP对应allowBackup。这些标志直接影响 PMS 的行为,如FLAG_DEBUGGABLE为 false 时,adb shell run-as com.example.app会失败。

4.3 一个真实案例:如何通过 getPackageInfo 诊断签名冲突

某次集成支付 SDK,测试环境一切正常,生产环境却报SecurityException: Permission Denial。用adb shell dumpsys package com.example.app查看,发现signatures字段显示两个证书:一个是我们的签名,另一个是 SDK 内置的 debug 签名。原因在于 SDK 的build.gradle中signingConfig signingConfigs.debug被误提交。解决方案是:在代码中调用pm.getPackageInfo("com.example.app", PackageManager.GET_SIGNATURES),遍历signatures数组,用MessageDigest.getInstance("SHA-1").digest(signature.toByteArray())计算每个签名的 SHA-1,与我们预期的 SHA-1 比对。若不匹配,立即Toast提示“签名异常,请检查构建配置”。这比等线上崩溃再排查快十倍。

4.4 getInstalledPackages 的性能陷阱

pm.getInstalledPackages(0)返回所有已安装应用的PackageInfo列表。在低端机上,这个调用可能耗时 200ms+,因为它要遍历/data/app/下所有 APK,逐个解析 Manifest。更糟的是,flags参数若传GET_ACTIVITIES | GET_SERVICES | GET_PROVIDERS,PMS 会为每个包解析全部四大组件,内存占用飙升。我的经验是:永远用GET_PACKAGE_INFO标志的最小集合。如果只需要包名和版本,传0;如果需要 Activity 列表,传GET_ACTIVITIES;绝不要传GET_PERMISSIONS | GET_SIGNATURES | GET_ACTIVITIES | GET_SERVICES | GET_PROVIDERS全量标志。对于列表页场景,用pm.getInstalledApplications(0)获取轻量级ApplicationInfo,它只包含packageName、name、icon、enabled等基础字段,性能提升 5 倍。

注意:getPackageInfo()在 Android 11+ 受到Package Visibility API限制。若你的targetSdkVersion >= 30,且未在 Manifest 中声明<queries>,则getPackageInfo("com.other.app")会抛出NameNotFoundException,即使对方已安装。解决方案是:在AndroidManifest.xml的<manifest>根节点下添加:

<queries> <package android:name="com.other.app" /> <!-- 或更宽泛地 --> <intent> <action android:name="android.intent.action.SEND" /> <data android:mimeType="text/plain" /> </intent> </queries>

PMS 在运行时会根据<queries>动态过滤getPackageInfo()的结果,这是 Android 保护用户隐私的强制措施。

5. installPackage 与 deletePackage:系统级操作的不可逆性与权限墙

PackageManager.installPackage()和deletePackage()是最危险的 API,它们直接触发PackageManagerService的安装/卸载流水线,涉及磁盘 I/O、签名校验、Dalvik 字节码验证、SELinux 策略更新、广播发送等十余个原子操作。自 Android 8.0 起,这些 API 已被标记为@Deprecated,官方推荐使用PackageInstaller,但底层逻辑一脉相承。

5.1 installPackage 的七步原子流程

  1. 预校验:检查 APK 文件是否存在、是否可读、大小是否为 0。若 APK 在/sdcard/Download/,PMS 会先校验调用方是否有READ_EXTERNAL_STORAGE权限(Android 10+ 改为MANAGE_EXTERNAL_STORAGE)。
  2. 签名解析:用JarFile解析 APK 的META-INF/MANIFEST.MF,提取CERT.RSA中的公钥,验证CERT.SF的签名完整性。若签名损坏,直接失败。
  3. Manifest 解析:调用PackageParser.parsePackage()解析AndroidManifest.xml,执行前述五层校验。任一失败,安装终止。
  4. 沙箱准备:为新包创建/data/data/com.new.app/目录,设uid(基于packageName的 hash),初始化seinfo(SELinux 上下文)。
  5. Dex 优化:调用DexOpt工具将classes.dex编译为odex或vdex,存入/data/dalvik-cache/。此步耗时最长,低端机可能卡住 5 秒。
  6. 注册到 Settings:将PackageParser.Package对象写入Settings.mPackages(内存 HashMap)和/data/system/packages.xml(持久化 XML)。
  7. 广播通知:发送Intent.ACTION_PACKAGE_ADDED,触发所有监听该广播的BroadcastReceiver。

5.2 deletePackage 的不可逆性

deletePackage()不是简单删文件。它会:

  • 删除/data/app/~~xxx==/com.example.app-yyy==/整个目录;
  • 清空/data/data/com.example.app/及其所有子目录(数据库、SP、files);
  • 从Settings.mPackages中移除该包的Package对象;
  • 发送Intent.ACTION_PACKAGE_REMOVED;
  • 但不会删除/sdcard/Android/data/com.example.app/—— 这是用户可访问的外部存储,需应用自己清理。

这就是为什么卸载微信后,/sdcard/Android/data/com.tencent.mm/目录还在。PMS 认为这是用户数据,不属于应用沙箱。若你的应用在onDestroy()中没清理该目录,它会一直残留。

5.3 权限墙:为什么你的应用无法静默安装

静默安装(无需用户点击“安装”按钮)需要INSTALL_PACKAGES权限,而该权限的protectionLevel是signature|privileged。这意味着:

  • 你的 APK 必须与系统签名一致(几乎不可能);
  • 或你的 APK 必须预装在/system/priv-app/目录下(需要 root 或厂商合作)。

普通应用只能走PackageInstaller的commit()流程,它会启动系统安装界面。但你可以优化体验:用PackageInstaller.Session创建会话,openWrite()写入 APK 流,fync()刷新,最后commit()。整个过程可在后台线程完成,用户只看到一次系统弹窗。我实测过,从下载完成到弹窗出现,控制在 800ms 内,用户感知不到卡顿。

5.4 一个血泪教训:installPackage 的 Intent Extra 陷阱

installPackage()的Intent中,Intent.EXTRA_NOT_UNKNOWN_SOURCE必须为true,否则 PMS 会拒绝安装(认为是未知来源)。但更隐蔽的是Intent.EXTRA_INSTALLER_PACKAGE_NAME:若你设置intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.example.installer"),PMS 会记录该 installer 包名,并在卸载时发送Intent.EXTRA_INSTALLER_PACKAGE_NAME给它。若 installer 包已卸载,PMS 会静默忽略。但若 installer 包存在且注册了ACTION_PACKAGE_FULLY_REMOVED广播,它会被唤醒——这可能导致你的 installer 应用在用户不知情时被拉起。我的建议是:除非你真有 installer 服务,否则不要设置EXTRA_INSTALLER_PACKAGE_NAME。

提示:PackageInstaller的SessionParams中setInstallFlags(PackageManager.INSTALL_REPLACE_EXISTING)用于覆盖安装。但INSTALL_REPLACE_EXISTING不会保留旧应用的数据!它会先卸载再安装,/data/data/com.example.app/被清空。若要保留数据,必须用INSTALL_FORWARD_LOCK(已废弃)或INSTALL_ALLOW_TEST(仅限 debug 包)。生产环境的热更新方案,应使用dex补丁或资源热更,而非installPackage。

6. 实战避坑:从线上崩溃日志反推 PackageManager 机制

最有效的学习方式,是从真实崩溃日志出发,逆向推导 PMS 的内部逻辑。以下是我在三个项目中遇到的经典案例,每个都直击 PackageManager 的设计要害。

6.1 崩溃日志:java.lang.SecurityException: Permission Denial: starting Intent ... from ProcessRecord{...} (pid=12345, uid=10123) not exported from uid 10124

现象:用户点击通知栏跳转到某个 Activity,崩溃。adb logcat显示上述异常。根因分析:ProcessRecord{...}中的uid=10123是通知服务进程(如com.example.app:remote),uid=10124是主应用进程。PMS 拒绝启动,因为目标 Activity 的android:exported="false",且调用方与被调方uid不同(跨进程)。exported="false"意味着“只允许同一 uid 进程调用”,而通知服务是独立进程,uid不同。修复方案:将目标 Activity 的exported改为true,并添加android:permission="com.example.app.PERMISSION",在 Manifest 中声明该 permission 为signature级别。这样 PMS 会校验调用方签名,确保只有自家进程能调用。经验总结:exported不是“是否可见”,而是“是否允许跨 uid 调用”。uid相同即视为同一应用,无论进程名是否相同。

6.2 崩溃日志:android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=content://com.tencent.wework.fileprovider/external_path/android/data/com.tencent.wework/files/xxx.jpg }

现象:企业微信分享的图片 URI,在部分机型上无法打开。根因分析:content://URI 的权限是临时的,由Context.grantUriPermission()授予。PMS 在resolveActivity()时,会检查调用方是否拥有该 URI 的UriPermission。但grantUriPermission()的权限有效期到进程死亡为止,若用户杀掉应用进程,权限丢失。更关键的是,FileProvider的getUriForFile()生成的 URI,其authority必须与 Manifest 中声明的android:authorities完全一致。com.tencent.wework.fileprovider是企业微信的 authority,你的应用没有权限访问。修复方案:不用startActivity(intent)直接打开,而是用ContentResolver.openInputStream(uri)读取流,再用 Glide 加载。这样绕过 PMS 的 URI 权限校验,只走FileProvider的openFile()方法,后者会校验callerUid是否在grantUriPermission()白名单中。经验总结:content://URI 的安全性由两层保障:FileProvider的openFile()校验和 PMS 的UriPermission校验。前者是代码级,后者是系统级。跨应用分享,优先走流读取,而非直接 startActivity。

6.3 崩溃日志:java.lang.RuntimeException: Unable to get provider com.example.MyProvider: java.lang.SecurityException: Permission Denial: opening provider com.example.MyProvider from ProcessRecord{...} (pid=12345, uid=10123) that is not exported from uid 10124

现象:Provider 在 Android 12 设备上崩溃,Android 11 正常。根因分析:Android 12 强制要求android:exported属性。你的MyProvider没写exported,PMS 在 Android 12 上按“无 intent-filter 则exported="false"”推断,导致其他进程无法访问。而 Android 11 的推断逻辑是“无 intent-filter 则exported="true"”,所以正常。修复方案:在<provider>标签中显式添加android:exported="true"(若需跨进程)或"false"(若只供本应用使用)。同时,若需跨进程,必须添加android:permission并在调用方声明对应权限。经验总结:exported的默认值随 Android 版本变化,永远显式声明。targetSdkVersion升级到 31+ 后,编译期会警告,但运行时崩溃更致命。

最后分享一个小技巧:当遇到 PackageManager 相关问题,第一反应不是查文档,而是用adb shell dumpsys package <package_name>。它会输出 PMS 内存中该包的完整Package对象,包括mActivities、mServices、mProviders、mSignatures、mPermissions等所有字段。对比你代码中的期望值与 dumpsys 的实际值,90% 的问题迎刃而解。比如mExported字段直接告诉你 PMS 认为这个组件是否导出,比猜 Manifest 更准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询