资讯详情

资讯详情

PackageManagerService深度解析:Android包管理与APK安装全流程

搞客户端开发久了的人多少都遇到过“应用装不上”或者“刚装上就秒闪退”的怪事。查了一圈网络、缓存、存储空间最后发现真正的原因往往藏在系统层所有跟APK安装、卸载、升级、权限授予、组件注册相关的操作最终都会汇聚到 system_server 进程里的一个服务——PackageManagerService简称 PMS。它管着包信息的“总账本”也管着安装和管理的全流程执行算是Android系统里最核心、也最容易让人一头雾水的服务之一。这篇文章我打算按自己实战排查时的思路来写把PMS的启动链路、APK安装的完整流程、包管理涉及的数据结构和状态变化以及日常定位问题时会用的经验都串一遍。不看官方文档散落的细节也不逐行贴代码而是讲清楚“它为什么这么设计”和“遇到问题怎么用它”适合刚转系统开发的工程师也适合想搞懂安装失败原理的应用层同学。1. PMS到底在系统里扮演什么角色1.1 一句话理解PackageManagerServicePackageManagerService并不是一个独立进程它是运行在 system_server 进程里的一个 Binder 服务对外通过 IPackageManager 接口给所有应用提供能力。应用层调用的getPackageManager()底层握住的其实就是这棵Binder树的远端代理。你可以把它理解成酒店的“前台档案室”。客人要订房、退房、改房、查房都得找前台前台背后有一大堆纸质档案记录每个房间住过谁、什么时候登记的、门禁卡权限开到哪一层。对Android系统来说每个APK就是一个“客人”PMS负责登记客人信息、安排房间UID和数据目录、发门禁卡权限和签名校验还要在客人退房时把档案销掉。1.2 它负责的几类核心事务把PMS的活拆开看主要是下面几块互相之间有很强的关联性元信息管理记录每个已安装包的包名、版本号、UID/GID、签名证书、代码路径、data目录、安装位置等。组件注册把APK解析出来的 Activity、Service、Receiver、Provider 和 Instrumentation 等组件登记到内部表供 ActivityManager、Launcher、startActivity 等查询。安装与卸载主担全量安装、覆盖升级、卸载、清除数据、安装会话管理。权限管理处理权限申请记录、签名权限校验、运行时权限授予和撤销以及默认权限规则。Intent解析queryIntentActivities()、resolveActivity()这类接口的最终执行者决定一个隐式Intent会命中哪个组件。与installd协同创建/删除应用数据目录、处理DEX优化依赖的是一个叫 installd 的native守护进程。这套职责很重的系统设计上并非把每个细节都自己做完而是更像一个“调度中枢”。比如创建目录这种底层操作PMS自己不碰文件系统而是通知 installd 去做原因后面会讲到。1.3 PMS不在独立进程里这才是关键很多刚接触系统源码的同学会有一瞬间的困惑PMS这么大一个服务怎么没有自己的独立进程它跟 ActivityManagerServiceAMS、WindowManagerServiceWMS一样全部住在 system_server 里。这个设计有历史原因也有性能考量。PMS跟AMS之间的交互非常频繁比如启动Activity时要查包信息、要判断权限如果拆成两个独立进程每次都是一次完整Binder跨进程调用系统关键路径上的延迟会高得离谱。放在同进程里它们可以直接调用对方的方法只是需要在锁上做好同步。代价也很明显system_server是个“巨兽进程”PMS一旦出现严重的死锁或内存问题整个系统可能都会重启而不是只挂掉一个服务。理解这点之后再看“为什么安装一个大App时系统偶尔会卡一下”就好理解了PMS在扫描APK、解析Manifest、写packages.xml的时候是拿着自己的全局锁的这个锁会跟AMS的很多操作互相等待。系统整体出现瞬时卡顿往往就是这些重量级操作在打架。2. PMS的启动链路开机时的“清点资产”2.1 system_server里的启动入口PMS不是在应用安装时才被创建的而是开机时由 SystemServer 在startBootstrapServices()阶段启动。这一步非常重要因为它是整个系统“包知识”的起点。启动入口是静态方法PackageManagerService.main()传入几个关键参数installer连到installd的桥factoryTest是不是工厂测试模式onlyCore是否只加载核心包。main()方法内部会 new 出一个 PMS 实例构造函数做完一大堆初始化后再把自己注册到 ServiceManager这样其他模块就能通过Binder拿到它。换句话说系统进程起来之后PMS是“先整理完自己家里的账本才开门营业”的。构造函数里干的事非常多我按顺序挑重点讲先初始化 Settings 对象从/data/system/packages.xml和/data/system/packages.list读入上次关机前的包状态然后检查系统目录结构接着逐个扫描系统分区和用户数据分区的APK把解析结果注册进内存表最后把权限同步一遍再写回 packages.xml保证本次扫描结果不丢。2.2 扫描目录的顺序为什么不能乱PMS在开机时会对以下几类目录做扫描顺序很讲究/system/framework框架级APK和共享库最先加载。/system/priv-app特权系统应用拥有普通系统应用没有的权限。/system/app普通系统应用。/vendor/app、/product/app、/system_ext/app厂商和产品分区应用。/data/app用户安装的应用目录。顺序之所以不能乱是因为系统应用要先注册好才能给后扫描的包提供签名权限和共享库依赖。比如一个三方应用声明了某个签名权限而这个权限是系统应用定义的那系统应用必须先完成注册三方应用的权限校验才能通过。另外factoryTest参数决定了是否扫描/data/app。工厂测试模式只加载核心系统包不加载用户数据这也是产线测试机器启动更快的原因之一。正常情况下扫描完系统包还会继续扫描用户分区并对开机前没有完成的DEX优化任务做收尾。2.3 首启与二次开机的差别第一次开机和后面每次开机的开销差别巨大。首次开机/data分区是空的PMS没有任何历史包袱每个系统APK都要从头解析一遍Manifest还要给大量框架包做DEX优化所以首启慢是正常的。二次及以后的开机PMS可以借助上次扫描生成的缓存和优化结果把一部分工作省掉但仍然会重新解析APK并不是完全走“读缓存”这条路。不少ROM团队在优化开机速度时都会集中在PMS扫描这块做文章比如增加并行扫描线程、调整DEX优化时机、延迟部分预装应用到后台再解析。我自己调过一台低端机仅仅是把不必要的预装应用从/system/app挪到/data/app按需安装开机时间就肉眼可见地快了不少。背后的道理就是PMS扫描的包越少、优化任务越分散开机压力就越小。3. APK安装流程拆到最细3.1 安装入口PackageInstaller会话机制说到安装很多人第一反应是adb install但走到系统内部几乎所有安装入口都收敛到 PackageInstaller 这套会话机制上。它从Android 5.0开始引入设计思路很像一个“事务”先创建会话再往里写APK数据最后提交提交失败可以整段回滚。整个流程拆开是这样的安装器比如应用商店或者我们通过pm install触发的 shell 层调用IPackageInstaller的createSession()传入包名、安装模式、大小、安装位置等参数得到一个 sessionId。拿 sessionId 打开PackageInstaller.Session通过它的 OutputStream 把APK内容写进去。此时文件只会落到一个临时暂存目录通常是/data/app-staging/vmdlXXX.tmp。数据写完安装器调用commit()真正触发PMS做校验和安装。这种设计的好处非常明显大数据量的传输和PMS核心逻辑解耦安装器可以先下载完整APK再提交一个会话还能传入多个split APK提交前可以随时放弃而不污染系统状态。对普通开发者来说这个模型也解释了为什么“文件还没拷贝完就commit”会导致INSTALL_FAILED_SESSION_INVALID这类错误。3.2 会话提交后发生了什么commit()被调用后PackageInstallerSession 会先做一次“安全检查”确认会话里的文件真实存在、大小匹配、包名跟SessionParams一致。随后PMS会把这次提交包装成一个安装任务交给内部的消息循环PackageHandler去排队处理。PackageHandler 是PMS里面一个容易被人忽略的角色它维护了一个安装请求队列。所有安装操作不会直接在当前线程执行而是投递到INIT_COPY消息里排队。这样做的好处是安装请求可以被串行化避免多个安装任务同时改/data/app导致文件状态错乱。在实际调试中如果你同时adb install两个App后一个往往会等前一个完成这就是PackageHandler在排队。任务轮到之后handleStartCopy就上场了把暂存目录里的APK文件拷贝到正式的/data/app/包名-一串随机后缀/目录下同时解析一遍基础信息校验签名最后进入installPackageTracedLI这个核心方法。3.3 解析APK从二进制Manifest到对象APK本质是一个ZIP包AndroidManifest.xml是二进制XML格式不能简单地用文本解析器读。PMS内部使用的解析器是 PackageParser它专门负责把这套二进制格式读成人能看懂的包对象AndroidPackage。解析过程不是只读一个Manifest那么简单它要处理非常多的字段包名、版本号、minSdk/targetSdk、uses-permission、application下的各种组件、sharedUserId、installLocation、以及 split 分包配置等。遇到有 split APK 的应用还要解析出 base APK 和各个 feature/config split最后组合成一个完整的包视图。这里有个经典坑Manifest里如果写了系统不认识的新标签旧版本Android往往会忽略但如果是标签属性值非法比如minSdkVersion不是整数解析阶段就会直接抛异常安装中断返回INSTALL_PARSE_FAILED_MANIFEST_MALFORMED。所以当你看到一个安装错误带PARSE_FAILED字样第一反应应该是APK的Manifest文件本身有问题而不是系统空间不够。3.4 逐项校验签名、ABI、版本、SharedUserId解析出包信息后真正的“审查环节”才开始PMS会做一整套兼容性和安全性检查。我用实际项目里遇到的问题一个个说签名校验安装新包时如果是覆盖旧版本必须保证新旧签名一致否则返回INSTALL_FAILED_UPDATE_INCOMPATIBLE。Android 7.0之后系统优先使用 APK Signature Scheme v2/v3 的签名信息做整体校验老应用没有v2签名才会回退到v1的JAR签名校验。targetSdk越高的应用对签名方案的要求就越严格。ABI检查APK的lib/目录里如果有 native soPMS会跟设备支持的ro.product.cpu.abi列表做匹配。旧设备经常出现“APK在测试机上能装在真机上 INSTALL_FAILED_NO_MATCHING_ABIS”基本都是so的ABI目录不匹配。版本检查覆盖安装时如果新版本号低于旧版本默认会拒绝除非显式带上INSTALL_ALLOW_DOWNGRADE标志。SharedUserId冲突APK声明了sharedUserId如果跟已有包不是同一个签名会直接被拒绝因为同一个共享UID组里的包必须签名一致才能互相“信任”。存储空间和目录状态检查/data分区可用空间、目标安装位置是否合法。空间不足时返回INSTALL_FAILED_INSUFFICIENT_STORAGE。这些检查看似零散但设计上有个共同目标在改动系统状态之前把一切能拒绝的理由都找到。这样才能保证安装过程要么成功要么保持系统原状不会出现装到一半发现签名不对留下半残文件的局面。3.5 扫描与注册组件正式“上户口”校验通过后PMS会调用scanPackageTracedLI()把包完整注册进系统。这一步是安装流程里最重的一环检查内存里是否已经有同名包有的话走更新逻辑没有就走新增逻辑。目标ABI确定后通过mInstaller创建或更新应用数据目录/data/user/0/包名/设备保护存储则是/data/user_de/0/包名/。把解析出来的组件注册进mActivities、mServices、mReceivers、mProviders这些内部表之后任何 Intent 查询都能命中它。把包加进mPackages这个以包名为key的Map同时在mSettings里生成或更新对应的PackageSetting。如果有 sharedUserId还要归并到共享用户组统一分配 UID。更新权限记录把Manifest里声明的权限、gid等同步给 PermissionManagerService。处理DEX优化任务performDexOpt()或通过后台任务执行 dex2oat。最后把mSettings写回/data/system/packages.xml发广播通知系统内外这个包已经可用了。很多开发者以为“安装成功”就是文件被拷贝了其实PMS视角里的成功是整套索引全部更新完毕。文件拷贝只是前菜组件注册、权限绑定、数据目录创建、持久化、广播通知每一步不到位应用都算不上真正“安装好了”。3.6 installd真正落盘的那个家伙前面好几次提到 installd这里单独展开。既然PMS是系统进程权限已经很高了为什么创建目录这种事还要麻烦另一个守护进程答案是SELinux和用户数据隔离。/data/user/0/包名这种目录不是随便建一个文件夹就行它需要按照system_app、untrusted_app、platform_app等不同的SELinux domain设置标签还要设置正确的UID、GID、目录权限。这些涉及内核级安全的操作如果都放到 system_server 里做等于把高风险的文件系统操作暴露在核心进程里出问题就是大问题。installd 是一个独立的native守护进程以root身份运行通过Binder接收 PMS 发来的命令执行createAppData、restoreconData、clearAppData、dexopt等底层操作。PMS只负责“决定”要不要建目录、建在哪个用户下installd 负责“执行”并保证结果符合安全策略。这个分层也是Android“系统服务只逻辑处理底层操作用单独进程”这一思想的典型代表。4. 包管理安装之外的“改状态”功夫4.1 PMS的核心数据结构做PMS相关开发最需要先认识的是这几个内存数据结构mPackagesArrayMapString, AndroidPackage包名到包解析结果的映射是所有包信息查询的第一站。mSettings持久化配置载体管着所有包的状态包括安装路径、版本、disabled状态、共享UID组等。PMS每次关键变更后都会把mSettings写盘。PackageSetting对应一个已安装包是“这次安装留下的档案卡”。里面包括codePath、resourcePath、pkgFlags、uid、versionCode、signatures、enabledSetting是否被禁用、suspend是否被冻结等。mActivities/mServices/mReceivers/mProviders组件索引表按组件名查Activity、Service、Receiver、Provider是它干的事。这些结构之间的关系可以理解为mPackages是“活的包信息”mSettings是“落盘的包档案”组件索引表是“对外提供检索的目录”。排查问题时如果dumpsys里包还在但组件查不到大概率是组件注册表出问题这不常见但一旦发生表现就是“应用设置里能看到包Launcher却找不到图标startActivity也报错”。4.2 packages.xml与packages.list/data/system/packages.xml是PMS持久化状态的心脏。里面记录了每个包的代码路径、版本号、UID、签名摘要、权限列表、enabled状态等。PMS每次安装、卸载、更新、授权都会更新这个文件。注意它是先写备份文件packages-backup.xml再替换主文件防止写一半断电导致文件损坏。如果你在调试中手动改坏了这个文件轻则所有包状态丢失重则系统起不来。/data/system/packages.list则偏向给native层使用内容更精简一行一个包字段包括包名、UID、GID、SELinux label、data目录等。installd、netd这些native守护进程都以它为重要输入。实操建议在排查“重启后包信息不对”这种问题前先备份packages.xml改错了好恢复。另外不要用文本编辑器直接乱改格式稍微不对PMS解析失败就会回退到备份甚至重置状态。4.3 升级、卸载、清除数据对状态的影响同样是对包做操作完整安装、覆盖升级、卸载对PMS状态的影响完全不同完整安装新增UID新增PackageSetting文件进入/data/apppackages.xml增加一条记录。覆盖升级PackageSetting里的版本号和codePath会被更新但UID和data目录保持不变所以应用数据能保留下来。这也是为什么“升级不上数据丢失”的诉求通常要靠备份机制而不是靠PMS。卸载PMS从mPackages里移除Package对象从mSettings里移除PackageSetting调用 installd 删除用户数据目录。但pm uninstall -k带-k参数时会保留数据目录只删代码和注册信息。清除数据pm clear或设置里的“清除存储”不会移除包注册信息只清空/data/user/0/包名下的数据并重置权限状态。我在实际项目里遇到过一种场景某应用被卸载后重新安装竟然还能看到旧数据。排查下来是安装时调了pm uninstall -k虽然界面显示应用没了但数据目录还在。对普通用户这可能是个隐私隐患对系统集成商这就是一个明确的产品规则是否需要持久化数据要在卸载流程里显式决定不能把“卸载”和“清数据”完全划等号。4.4 禁用与停用不是删除的“软卸载”PMS还支持一种“半卸载”状态禁用。pm disable-user --user 0 com.xxx可以把某个应用置为DISABLED_USER表现是从Launcher消失、无法启动但包还留在系统里。对应的还有pm enable恢复。这里有个容易混淆的点DISABLED状态分好几种包括DISABLED_UNTIL_USED。后者是系统为了“预装应用首次被使用后再启用”设计的状态常见于一些默认关闭的系统组件。查看包当前状态可以直接dumpsys package com.xxx里面有一行enabled字段。禁用不等于卸载这是排查问题时特别值得记住的如果一个预装应用“不见了”先用pm list packages -d看看是不是被禁用而不是直接怀疑安装失败。4.5 权限授予与安装的关系APK安装完成后Manifest里声明的权限会被解析进权限系统。从Android 6.0开始危险权限不再在安装时全部授予而是运行时由用户决定。但这不代表PMS就完全不管了普通权限会在安装时自动授予签名权限会根据安装者签名和系统签名是否匹配做自动判定并被记录在权限管理服务里。安装流程中grantPermissions那一步会遍历APK声明的权限逐个决定是“默认授予”还是“等待运行时申请”。对于预装应用系统还可能通过default-permissions.xml预授权一些敏感权限。很多“首启就要定位权限不授权连默认页都进不去”的现象其实都是目标Sdk版本和权限模型共同决定的。调试时adb shell pm grant 包名 权限名和pm revoke 包名 权限名是绕开弹窗直接改权限状态的好工具。改完后重启会失效但排查权限问题非常快。5. 日常排查错误码速查与实战经验5.1 dumpsys package的正确打开方式adb shell dumpsys package是PMS体检的第一入口但直接输出非常长学会缩小范围比会看全量更有用。dumpsys package com.xxx只看指定包的信息包含安装用户、data目录、版本、signing keys、请求的权限等。dumpsys package packages只看包名列表和基础标识适合确认包是否被识别。dumpsys package permissions查看权限授予状态排查“这个权限到底给没给”。dumpsys package dexopt查看DEX优化状态能发现“包还在但odex没生成”一类的问题。dumpsys package installer查看当前是否有残留的安装会话。我通常的流程是先pm list packages | grep 包名确认包是否在再用dumpsys package 包名看细节最后根据错误码决定要不要继续查 logcat。大部分安装问题在dumpsys package的输出里就露馅了。5.2 常见安装错误码对照表下面是我整理的高频错误码按我遇到的频率排了序错误码含义常见触发原因INSTALL_FAILED_UPDATE_INCOMPATIBLE覆盖更新不兼容新旧包签名不一致、组件冲突INSTALL_FAILED_ALREADY_EXISTS包已存在安装了同名包且未带替换标志INSTALL_FAILED_SIGNATURE_MISMATCH签名不匹配同一个包名用不同证书签两次INSTALL_FAILED_VERSION_DOWNGRADE版本降级新版本号低于已安装版本INSTALL_FAILED_NO_MATCHING_ABIS没有匹配的ABIAPK只含armeabi设备是纯arm64INSTALL_FAILED_INSUFFICIENT_STORAGE存储空间不足/data分区满或inode耗尽INSTALL_FAILED_DEXOPTDEX优化失败dex2oat异常、ART缓存损坏INSTALL_PARSE_FAILED_MANIFEST_MALFORMEDManifest解析失败APK的Manifest二进制格式损坏INSTALL_FAILED_INVALID_APKAPK无效文件损坏、不是合法ZIPINSTALL_FAILED_USER_RESTRICTED用户受限多用户策略禁止该用户安装INSTALL_FAILED_SESSION_INVALID安装会话无效commit前数据未写完或会话过期保存这张表很有用。看到错误码先定位到“哪一类”再去查具体原因比直接翻源码高效得多。5.3 几个实战排查案例我随手分享几个真实遇到的案例。案例一某渠道包升级一直失败报INSTALL_FAILED_UPDATE_INCOMPATIBLE。第一反应怀疑签名但检查打包脚本发现是重签了没问题。再细看新旧包居然声明了不同的sharedUserId旧包属于sharedUserIdA新包改成了B。系统认为这属于包身份变化直接拒绝覆盖。解决方式是让渠道方沿用同一个sharedUserId。案例二应用在低端机上安装后闪退logcat里报“didnt find class”。先怀疑混淆和分包但最后查dumpsys package dexopt发现这个包压根没有生成odex安装时DEX优化被跳过了。清除ART缓存重启设备后恢复正常。这类问题常见于OTA升级后遗留的缓存损坏。案例三预装应用第一次开机后不显示。第一反应是扫描失败但看pm list packages里包是存在的再查dumpsys package才发现enabled3DISABLED_UNTIL_USED。这是预装配置里带了android:enabled和pm disable-until-used的典型结果不是安装问题。5.4 logcat快速定位技巧PMS相关的日志标签主要有PackageManager、PackageManagerService、PackageInstaller、DexoptWrapper、installd。定位安装问题我建议第一次就开启这几个标签的过滤adb logcat -s PackageManager PackageManagerService PackageInstaller观察日志里是否出现Package [com.xxx] (123) added或者installPackage等关键字。如果日志停在某个校验阶段迟迟不动再用adb shell dumpsys package installer看会话状态判断是不是卡在文件暂时区或者等待某个广播。给个独家小技巧遇到“安装过程卡死”的问题不要急着杀pm进程先抓一份dumpsys package installer和主线程堆栈看看PackageHandler队列里排了多少任务。我见过不少“卡死”其实是排队任务太多并不是真的死锁。6. 我踩过的一些坑与体会6.1 别在主线程碰PMS普通应用开发里PackageManager的接口虽然有个BinderStub的包装但很多调用走到PMS之后都会做磁盘扫描或全表遍历耗时不可控。getInstalledPackages()、queryIntentActivities()这种接口在包特别多的设备上几百毫秒很正常。在主线程直接调流畅度直接被毁。我自己写系统工具时凡是涉及全量查询的一律放到子线程并且做好结果缓存。PMS内部有大量锁一个慢查询还可能导致其他调用排队牵一发动全身。6.2 安装慢、卡、反复失败未必是APK问题常见的“安装失败”背后可能是/data分区 inode 满了、包管理数据库损坏、SELinux标签不正确、甚至低端机器上 dex2oat 排队过长。遇到安装问题我现在的检查顺序是先看错误码再看dumpsys然后查logcat最后才怀疑APK本身。顺序反过来很容易在应用层折腾半天才发现是系统状态异常。6.3 写系统代码时的一些习惯做PMS相关定制时会踩到锁的坑。PMS内部有很多锁像是包信息锁和设置锁调用顺序如果不一致两个线程互相等待就可能死锁。我的习惯是任何自己写的代码里持有PMS相关锁后绝不再做Binder调用。Binder调用是异步的可能跨进程、可能阻塞在锁里做等于把整个系统大门锁死等着电话铃响。另外改包状态类的操作尽量走系统提供的“事务式”入口比如PackageInstaller会话而不是直接改packages.xml。绕过PMS的约束写文件短期看着能用下次系统重启或做一次清理就原形毕露。结尾做Android系统开发这些年PMS算是我反复“踩进去”又“爬出来”最深的一个模块。它不像业务代码那样有明确的产品逻辑更像一张巨大的状态机把APK的每一点状态变化都记录得清清楚楚。我个人的体会是别想着一下子读懂所有源码先把它“管什么、怎么启动、怎么安装、怎么记录状态”这条主线串起来再带着具体问题去查会顺畅很多。最后再分享一个不知道算不算冷门的技巧当你怀疑应用“装了但没完全装好”时直接对比pm path 包名返回的路径和/data/app下真实的目录是否存在。命令返回正常但目录消失基本就是文件系统状态跟PMS内存表不一致这种问题靠应用日志查不出来只能在系统层定位。PMS就是一栋大楼的档案室很多诡异现象从它这里找答案往往最快。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →