Android 11 packages.xml全面拆解:PMS读写与权限持久化
做Android系统开发的同学多多少少都跟/data/system/packages.xml打过照面但真正把它每一项字段看明白的人不多。上一篇我们聊了Android 11 PMS的整体启动流程这篇就顺着启动过程里最关键的一个环节展开PMS到底怎么把已安装应用的状态“记”下来又是怎么在重启之后把它们“读”回来的。packages.xml就是这份记账本本身理解了它很多系统定制、开机问题排查、包管理异常的场景就能找到头绪。先说清楚这个文件的分量。它不是日志也不是缓存而是PMS运行机制里持久化的核心数据。系统里每次安装、卸载、更新、权限变更最终都要落到这个文件上。它记录的不只是“装了哪些包”还有每个包的uid、版本、签名、权限授予状态、冻结状态、跨用户数据等一连串关键信息。可以说只要设备重启过、开过机PMS的初始化就绕不开它。对做系统开发、ROM定制、性能优化甚至应用兼容性适配的同学来说这都是一份绕不开的图谱。这篇文章是系列的第二篇默认你手里有一份Android 11的源码AOSP分支即可并且对PMS的基本结构有点概念。我会沿着文件本身的结构走先把节点和字段逐一拆开再讲PMS解析和写回的全过程最后把调试时常见的坑和排查思路整理出来方便你直接对照实操。1. 从全景看packages.xmlPMS的“记忆中枢”到底在记什么1.1 文件位置与基本分层packages.xml位于/data/system/packages.xml与之配套的还有同目录下的packages.list、packages-backup.xml以及packages.xml的前一个版本备份packages.xml.bak。它们各自分工不同但packages.xml是唯一一份完整保存包级状态的权威文件。从Android 7.0开始系统对packages.xml做了分区存储机制也就是把文件内容按不同目的拆成几个区域来写。核心区域包括ro只读区域存放系统启动后不再变化的基础字段比如version、fingerprint、sdk编译版本等。system系统区域保存系统级设置和用户级数据这里有一项关键的“被冻结的UID”机制后文单独说。user用户区域按userId维度保存每个用户下应用的安装状态和权限授予情况。这个分层的目的很直接系统升级时PMS只需要更新user区域的数据系统区域大部分不动尽可能减少跨版本迁移时状态丢失的概率。同时这也让多用户场景下的数据隔离变得更干净。从源码层面看packages.xml的读写逻辑主要在Settings类和PackageManagerService的几个方法中。Settings持有这份数据的全部内存态分别用mPackages、mSharedUsers、mPermissions等Map容器维护真正落盘和恢复时走的都是这些数据结构。1.2 为什么不能“不持久化”或“简单重装”有人可能会想包信息而已每次开机扫描一遍/data/app不就行了为什么要搞这么一个大文件这种想法踩过几次坑就明白了。/data/app里面的APK只是静态文件但系统里真正需要的是运行时的稳定状态包括三个层次第一uid的稳定性。每一个应用在安装时都会拿到一个稳定uid这个uid会写入应用的data目录属性和SELinux标签。如果不持久化重启后重新分配uid文件就全乱了应用甚至无法读取自己的私有数据。这个状态必须跨重启保持packages.xml就是载体。第二权限的授予状态。运行时权限的授予情况、默认权限的判定结果这些数据是用户反复确认或开发者预设后产生的。一旦丢失用户要重新面对一遍弹窗部分系统预置的服务也可能因为权限被重置而行为异常。第三系统服务之间的握手依赖。比如某些系统应用需要固定uid联通组件sharedUserId机制更是直接在packages.xml里显式声明。随便“重装”一次这些关系链就断了。所以packages.xml解决的不是“枚举已安装应用”的问题而是“让一个复杂的包级状态机具备跨重启的一致性”。这也是为什么它的结构比一般配置文件复杂得多。2. 逐节点拆解一份packages.xml里到底写了什么2.1 根元素和头部属性打开一份Android 11设备上的packages.xml第一行通常是类似这样的?xml version1.0 encodingutf-8 standaloneyes ? packages version61 fingerprintgoogle/sdk_gphone/x86_64:11/RP1A.200720.011/6569842:userdebug/test-keys sdk30 databaseVersion24 compilationReasonINCREMENTAL这些属性分别是versionpackages.xml自身的格式版本号。PMS启动时如果发现这个值和当前系统期望的版本不一致会走upgradeFileFormat流程做兼容升级。fingerprint构建指纹用于判断是否需要做“安装应用升级”的操作。当系统版本发生变化OTA或刷机时PMS会比对当前fingerprint和记录值不一致时触发reconcilePackages等逻辑。sdk记录写入时对应的SDK版本跨版本升级时用来决定哪些系统包需要重新做兼容处理。databaseVersion权限数据库的版本号和数据迁移有关。compilationReason最后的编译触发原因这通常来自DEX优化记录。头部信息虽然只有几行但它是整个文件版本兼容性的地基。实际排查问题的时候我通常先看这几个属性字对不对。比如设备OTA前后指纹不符导致部分系统应用数据重置这算是很典型的场景。2.2 权限声明区permission-trees与permissions头部之后进入权限声明区形如permission-trees / permissions item nameandroid.permission.READ_EXTERNAL_STORAGE packageandroid typedangerous protectionLevel1 / item namecom.example.demo.permission.SEND_MSG packagecom.example.demo typedangerous / /permissions这里记录的是系统已知的权限定义包括那些由三方应用在安装时动态声明的自定义权限。PMS解析到这里时会把这些信息放进mPermissions并建立名称到权限对象的索引后面任何包申请权限时都要从这里校验来源和等级。需要注意type属性和protectionLevel的区别。type表示权限的类别normal、dangerous、signature、internal等而protectionLevel是权限保护级别的更细粒度编码值。在Android 11里权限的解析路径已经统一走ParsingPermissionUtils但最终写入packages.xml时还是以这两个字段为主。这一区块是排查“权限声明冲突”或“权限未被识别”问题时的重点。比如某些系统应用起不来报Permission Not Found多半是packages.xml把这个包的权限项丢了或者是自定义权限的package属性指错了包名。2.3 包节点核心从package的完整示例看字段语义整个文件的主体是package节点。下面我用一个简化的示例模拟一个叫com.example.demo的三方包在文件中的样子package namecom.example.demo codePath/data/app/~~xxxx/com.example.demo-xxxx/base.apk nativeLibraryPath/data/app/~~xxxx/com.example.demo-xxxx/lib primaryCpuAbiarm64-v8a userId10123 sigs count1 cert index0 key-----BEGIN PUBLIC KEY-----.../ /sigs perms item nameandroid.permission.INTERNET grantedtrue flags0 / item nameandroid.permission.WRITE_EXTERNAL_STORAGE grantedtrue flags3 / /perms properites item namesignatureScheme value2 / /properites signing-keys scheme2 key-set cert index0 / /key-set /signing-keys disabled-components item namecom.example.demo.MainActivity / /disabled-components updated-package namecom.example.old codePath/system/app/old / /package每个字段都有自己的消费场景codePath和nativeLibraryPath安装路径的绝对位置。PMS启动时会根据这个路径判断应用是否还存在这里也直接关联到“卸载后残留目录”的清理逻辑。primaryCpuAbi主ABI标识决定应用加载哪些so库。部分混合架构设备上这个字段不一致会导致运行时加载崩溃。user-id就是uid的十进制值。它就是刚才说的“稳定身份”掉电不丢。sigs签名公钥的内容或摘要信息。PMS对比新旧签名时直接读取这里的cert内容。perms这个包被授予的权限列表。granted表示是否授予flags记录授予来源0默认、1系统固定、2用户、3安装器、4设备所有者。关键是flags如果丢失系统可能无法判断哪些权限是“用户刻意选的”进而在部分场景下重置授予状态。properites包属性键值对常见的有signatureScheme表示签名方案是v1还是v2/v3。这个字段直接影响后续包更新的签名校验方式。disabled-components被禁用的组件列表。比如用户手动停用的Activity或者被系统冻结的组件都会记录在这里开机解析后直接生效。updated-package当某个系统包被更新通过覆盖安装或adb install -r替代时记录原始系统包的位置。这样卸载更新后还能回滚到系统版本。在源码里这些字段对应的是PackageSetting对象的成员。解析时PMS会逐项set进去脏了之后再通过PackageSetting.write方法序列化出来。所以你在packages.xml里看到的每个节点基本都能在PackageSetting.java、PackageKeySetData.java、ArrayMap系列容器中找到对应的存取逻辑。2.4 sharedUser分组与签名集合的写法说一个很多人容易忽略的地方就是sharedUser节点。它的样子是这样sharedUser namecom.example.shared userId10055 sigs count1 cert index0 key-----BEGIN PUBLIC KEY-----.../ /sigs perms item nameandroid.permission.READ_PHONE_STATE grantedtrue flags1 / /perms /sharedUser凡是声明了同一个sharedUserId的包它们在这个文件里并不会各自写入多份权限状态而是共享同一个SharedUserSetting。这个设计直接影响PMS的uid申请逻辑一组共享uid的应用必须具有相同的签名这就是为什么“两个不同签名的APK想共用sharedUserId”会直接安装失败。共享uid还有一个容易被忽略的后果权限合并。一个包通过sharedUser拿到权限等于面向同一组内所有包开放。这是很多多进程架构应用喜欢用sharedUserId的原因但也意味着一旦其中一个包被授予了高敏感权限另外几个包在黑盒视角上也“沾了光”。排查权限问题时记得把sharedUser组一起看。signing-keys节点在Android 11里和sigs并存但语义略有不同。简单理解sigs是当前有效签名的摘要signing-keys记录的是一套或多套可用于后续签名的密钥集合支持APK Signature Scheme v2/v3里的密钥轮换场景。做系统升级包签名验证的时候判断的是signing-keys里的keySet是否覆盖了新的签名公钥。3. PMS怎么把XML读进来解析流程与版本升级逻辑3.1 Settings构造阶段与readLPw入口PMS初始化时会在构造方法里直接创建Settings对象。这个构造方法的开头就是读取packages.xml的关键路径核心入口是readLPw。流程大体是这样的优先尝试读取/data/system/packages.xml如果文件不存在再找packages-backup.xml。找到文件后先用XmlUtils.readThisUtf8XmlFile开启一个轻量级XML解析器注意这里不是标准XmlPullParser而是自己封装的一套快速读取逻辑。逐标签读取遇到package就调用readPackageLPw遇到shared-user就调用readSharedUserLPw遇到permissions则逐条添加权限项。读到package时实际上还分两种情况。第一种是普通的三方包路径直接解析其codePath指向的APK第二种是“系统包已被覆盖更新”的场景这时会额外解析/system或/vendor下的原始系统包生成两份PackageSetting一份是“updated”状态一份是“system”原始状态分别维护。这里要提一个关键点readLPw并不是简单地把每行塞到内存就完事。它要一边解析一边做数据自检。比如某个包记录了codePath但实际路径已不存在解析阶段不会直接报错而是先标记为PLATFORM_PACKAGE_ERROR之类的状态后续在PMS的reconcile阶段再统一处理。这么设计是为了尽量保留数据而不是一遇到坏项就整体崩溃。3.2 版本属性校验与跨版本数据迁移packages.xml里version属性不是摆设。PMS会在解析开始前拿到文件里写的版本号和当前Settings里的SETTINGS_VERSIONAndroid 11上通常是61做对比。假如差得太远比如一个Android 8的设备刷了Android 11的system镜像但保留老数据PMS会尝试调用一系列upgrade路径来补救字段缺失的问题。常见的一类迁移是把旧版package节点中的codePath从/data/app/com.example-1格式修正为带随机化目录的新格式Android 11的codePath里有~~xxxx这种前缀。如果不做这一步旧数据直接拿来用包管理器就找不到APK应用列表直接全空。另一个重点是权限数据库的迁移。Android 11里权限默认值、dangerous级别划分和权限组归属发生过调整databaseVersion变了之后PMS会把旧权限状态映射到新体系。比如旧版本的存储权限可能被自动拆成“读取”和“访问媒体”两类databaseVersion升级代码里会明确做这个拆分避免老应用因权限语义变化而出现功能缺失。如果你的设备是userdebug或eng版本还可以在Settings类里看到一整套日志输出逻辑。一旦版本不对logcat里通常会出现类似Upgrading settings from version N to M的提示跟着这些提示去源码里找对应升级函数比干看代码要快得多。3.3 解析期间的内存态构建PackageSetting与密钥集解析结束后Settings内部会形成一张完整的内存表。每个package对应一个PackageSetting它继承自ComponentEnabledSetting是PackageSettingBase和SettingBase等基类的组合体。SettingBase里主要维护了mPermissionsState也就是那份权限授予状态PackageSettingBase则管着一堆大杂烩字段从codePath、resourcePath到installerPackageName、volumeUuid都在这里。密钥集是另一个容易被忽略的数据结构。PackageSetting里有keySetData成员它保存了当前包所有可用的签名公钥集合以及签名方案。这个数据在安装更新包时起决定作用你装一个覆盖旧APK的新版本PMS会用新签名公钥和keySetData里的key set做交集判断。如果交集为空直接拒绝安装这就是所谓的签名一致性保护。内存态构建完成之后Settings还会顺手做一次writeLPr的全量写回目的是把刚刚解析出来的内存态刷新到磁盘上确保即使后续进程异常崩溃磁盘上的数据也处于一致状态。这一步虽然听起来简单但它解决了一个真实问题如果解析中途系统直接断电或者掉进程磁盘上可能残留半写状态的packages.xml。所以你会看到文件系统里一直有个packages.xml.bak兜底PMS在启动时会检查主文件有效性不可用时自动切换备份文件。4. 高频字段背后的数据语义源码里为什么这么设计4.1 uid持久化稳定身份如何贯穿系统在packages.xml里package节点的userId是它最核心的字段之一但很多人不知道它背后还有个firstInstallTime和lastUpdateTime的组合坑。firstInstallTime是应用第一次安装到该设备的时间戳lastUpdateTime是最近一次更新的时间戳。它们的用途不止是展示“安装日期”。系统里有些行为逻辑直接依赖这两个字段比如权限决策的默认值判断、部分系统组件的rollback判断、以及“新安装应用”标识。如果你在做备份恢复时间戳丢了恢复出来的包可能被系统当成新装应用原本用户已经拒绝过的权限弹窗会再次出现。这里的本质是PMS把时间也当成了撤销或恢复操作的线索。比如某个权限是用户手动拒绝的和包重新安装后的默认状态完全不同而PMS区分二者的依据之一就是时间戳和flags的组合。在排查“为什么应用一直弹权限”的问题时先看看firstInstallTime和lastUpdateTime是否异常往往比追代码更快。4.2 perms节点的flags位运算到底在表达什么perms节点里的flags是一个整型位掩码每一位表达不同的授予来源或状态。Android 11的PermissionManager和PermissionController就是靠解析这个值来决定权限的“可撤销性”和默认状态。常用位含义大致如下flags值含义0默认授予通常通过静态声明授予1系统固定授予不允许用户撤销2用户手动授予3安装器授予通常是权限组级别的4设备所有者或资料所有者授予16预授权32从设备所有者策略派生这里最容易踩坑的是修改节点的flags并不能让系统立即按新值工作。因为PMS在内存里已经有一份PermissionsState副本修改XML后需要重启并触发重新解析才会覆盖内存状态。我在实际调试中为了强制重置某个权限状态一般是直接删掉对应节点、删掉包数据、然后重装而不是去手改flags。手改的话权限组件里的很多缓存和数据库记录可能对不上反而更难排查。4.3 signing-keys与证书轮换的兼容策略前面提过signing-keys这里展开一下它的应用场景。Android 11的PackageKeySetData支持多keySet并存也就是说一个应用在生命周期内可以换签名前提是旧签名能证明新签名来自同一归属方。这套机制对应到packages.xml里就是signing-keys scheme2 key-set cert index0 / /key-set key-set cert index1 / /key-set /signing-keyssigs节点的count1表示当前APK包里实际用到的签名证书数量而signing-keys则允许记录多套。如果你做系统预置应用升级要注意新APK的签名是否落在key-set集合里。如果不在即使你是root直接文件覆盖PMS也会在扫描时判定签名不一致然后把应用标记为“签名冲突”状态。这套机制说到底是牺牲一部分“灵活性”换取的“安全性”。平台对应用签名的追溯能力越强恶意覆盖的门槛就越高。对开发者来说唯一需要的习惯是维护好自己应用长期稳定的签名证书别频繁轮换尤其别在key-set里删掉老证书记录否则停产设备的存量用户更新就会大面积失败。5. 写回机制PMS什么时候把内存态刷回packages.xml5.1 主动写回与延迟合并的触发场景packages.xml不会实时、逐字段地写磁盘写回有明确时机。最核心的入口是Settings.writeLPr它在以下场景被调用安装、卸载、更新操作完成后这是最常见的写回触发点PMS在installPackage或deletePackage流程收尾时主动刷盘。权限状态变更后比如用户打开或关闭某个运行时权限updatePermissionFlags或grantRuntimePermission流程会触发对应PackageSetting的标记然后在合适时机写回。系统启动过程中的一次整体同步PMSsystemReady之后主线程会处理一个WRITE_PACKAGE_RESTRICTIONS之类的消息把启动期间累积的改动统一落盘。需要注意写回不是每次变更都立即全量写而是有合并和批量的策略。PMS内部维护了一个mDirty标记或类似机制多个变更会打成一个包统一刷。这样能有效减少存储磨损和IO抖动但代价是“拔电后丢失最近几次变动”的概率上升。不过PMS的应对办法就是那两份备份文件主文件写坏或写半截崩溃恢复时还有第二轮机会。写回过程中XML序列化由XmlSerializer落到packages.xml的临时文件然后通过rename或copy替换正式文件。这中间任何一步被系统杀死都可能留下残缺版本。所以你会看到Settings里还有少量防御性逻辑比如对关键字段在写前做一次有效性检查避免把垃圾值写进正式文件。5.2 从updatePermissionFlags到最终落盘的一条调用链拿权限变更的例子走一遍调用链用户点开某个应用的通知权限或运行时权限进入系统设置界面最终调用的是PackageManagerService.updatePermissionFlags。这个函数首先在内存里修改PermissionsState的对应标志位然后把PackageSetting标记为dirty最后通过ScheduleWriteSettings或直接mSettings.writeLPr把数据刷到磁盘。这中间的主干方法包括updatePermissionFlags修改permission flags记录变更来源。grantRuntimePermission/revokeRuntimePermission更新授予状态同时更新permissions节点里的granted属性和部分flags。writeLPw遍历所有PackageSetting把每个对象的getPermissionsState()序列化成XML节点。排查“权限改了但重启后丢失”这一类问题时先确认日志里writeLPw是否真的执行了。如果执行了但数据没变再看是不是改动的是另一个用户空间多用户模式下每个userId有独立states记录而不是当前正在读写的user。这个坑我印象很深刻某次改UserHandle.USER_SYSTEM的权限却一直在查当前用户目录下的packages.xml自然对不上。5.3 多用户场景下的独立读写路径Android 11的packages.xml并不是只有一份多用户模式下用户维度的数据实际上存在/data/system/users/userId/package-restrictions.xml中而packages.xml里保存的是所有用户都需要的通用包信息。所以在分析权限相关问题时经常要同时看两份文件。packages.xml里记录每个包已经声明的权限列表package-restrictions.xml则记录具体到某个用户上哪些组件被禁用、哪些权限被允许或拒绝。二者是互补关系而不是全量冗余。你真要看某个运行时权限在当前用户下为什么没生效就去package-restrictions.xml里翻了。写回逻辑也据此拆分全局包状态写packages.xml用户级状态写各自的restrictions文件。这解释了为什么PMS在多用户状态的切换上能做到相对干净不会因为一个用户改权限而污染另一个用户的授予状态。6. 常见问题与排查技巧实录6.1 packages.xml损坏后的启动恢复机制packages.xml损坏并不是开机必死Android在加载阶段有容错设计。PMS会先检查主文件能否正常打开和解析失败时尝试packages-backup.xml再不行就重置为空白状态。这在实际操作中表现为三种情况主文件正常直接解析主文件启动加载包状态。主文件缺失或解析失败自动fallback到backup文件同时把backup改名为正式文件。两份都不可用packages.xml将以“零包”状态初始化开机后所有应用需要重新扫描但之前的uid状态、权限记录会全部丢失。出现前两种时logcat里会看到明显的Read settings from backup或Failed to read packages.xml之类记录。出现第三种就是大坑了用户所有已安装应用虽然还在/data/app里但uid会重新分配应用的私有数据基本等同作废还会伴随着大面积的“应用被重置”体验。所以做开发或维修时碰运气前一定要先备份。我自己的习惯是手动操作packages.xml前先执行adb shell cp /data/system/packages.xml /data/system/packages.xml.bak adb shell cp /data/system/packages.xml /sdcard/packages.xml.manual.bak这样不仅留一份设备内的备份还把文件拷贝到可导出目录防止后续设备无法开机时连备份都取不出。6.2 手改XML后重启生效但状态“分裂”的排查思路不少同学喜欢用root权限直接编辑packages.xml来达到某种隐藏状态比如伪造已授权权限、修改包版本号等。这种方法在一些低版本Android上行得通但Android 11的PMS在解析之外还会做大量二次校验特别是在核心字段不一致时。举个例子你把某个包节点里的userId改了但没有同步修改/data/user/0/包名目录的所有者和SELinux标签那么重启后应用虽然能启动但读取不到原有数据目录表现就是“应用像新装的一样”或者“一直崩溃”。表面的packages.xml只是改了引用但实际数据归属和SELinux上下文是内核层强制约束的纸上改改补不齐差额。真要验证某个想法我的建议是从源头走正规流程adb shell am force-stop com.example.demo adb shell pm clear com.example.demo adb shell pm revoke com.example.demo android.permission.READ_EXTERNAL_STORAGE然后再观察重启后的结果。这样至少权限状态和目录状态是一致的排错范围也小。6.3 从packages.xml反推安装来源和包路径最后分享一个实际调试中很顺手的小技巧。当你在做系统定制需要确认某个包到底是系统预置的还是用户后来装的不用去翻一堆build配置直接在packages.xml里对比几个字段就行。installerPackageName记录是通过谁安装的常见的有com.android.vending、com.google.android.packageinstaller或系统自己。codePath系统级包一般指向/system/app或/vendor/overlay用户包则在/data/app。updated-package如果存在这个节点说明基础包在系统分区当前状态是被覆盖更新过的。这一条信息链对定位“为什么系统升级后应用被重置”很有帮助。通常流程是OTA升级后fingerprint变化触发PMS重新扫描所有codePath。如果某个系统包被记录为updated-package但新系统镜像里的对应包版本比记录里的还低PMS就会根据策略决定是保留更新还是回滚到系统版。这个决定就藏在packages.xml和PackageManagerService的scanAvailablePackages逻辑里。我个人的体会是packages.xml这份文件像系统的“车况记录仪”每一个节点都不是孤立存在的它和/data/app目录、/data/system/users下的状态文件、系统分区里的预置应用时时联动。真正高效的做法不是背字段而是带着“这条数据在哪个场景被消费、被谁消费”的视角去读。当你下次再遇到系统应用莫名重置、权限状态丢失、uid错乱这类问题先从这份文件里对照一下这几个核心字段排查路径会清晰很多。