ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android 11 PMS启动流程全拆解:从SystemServer到systemReady

Android 11 PMS启动流程全拆解:从SystemServer到systemReady

做Android系统开发这几年,我越来越觉得PackageManagerService(PMS)是一个被严重低估的研究对象。很多人都知道它管着应用安装、卸载、权限,可真遇到开机时间异常、应用装不上、权限白名单失效这类问题,追进PMS源码里往往就不知道从哪下手了。PMS的启动流程是整个包管理体系的骨架,Android 11这一版的启动逻辑已经相当成熟,主线非常清楚:系统服务启动阶段被创建,构造函数里完成扫描与初始化,最后在systemReady里做全局收尾。这篇文章就沿着这条主线,把Android 11 PMS启动的总体流程完整拆一遍,适合正在做系统定制、排查开机类问题,或者想在AMS和PMS之间建立全局视野的开发者阅读。

先说明一下,这是系列第一篇,目标是搭起整套时间线框架。很多细节像单个APK怎么解析、权限表怎么迁移,这篇不去无限展开,后面写第二篇的时候再往骨头缝里钻。

1. 一次启动被拆成三张“时间表”:PMS启动的三个阶段划分

读系统服务源码最忌讳的事,就是一头扎进某个方法里出不来。PMS的“启动”绝不只是new一个对象那么简单,它其实横跨了SystemServer启动链条里好长一段。我习惯先把整个启动过程切成三段:准备、构造、就绪。

阶段调用入口核心工作完成标志
准备SystemServer.startBootstrapServices()拉起Installer等前置服务进入PMS.main()
构造PMS.main() → 构造函数Settings恢复、目录扫描、权限初始化“package”服务注册进ServiceManager
就绪PMS.systemReady()补发挂起事件、权限最终更新mSystemReady置为true

这个划分不是拍脑袋想的,它和SystemServer里三个截然不同的调用点一一对应。遇到任何和PMS启动相关的问题,第一件事是先判断问题落在哪个阶段,再决定从哪个入口去查。

1.1 为什么PMS的启动不像Activity那样按生命周期走

Activity有onCreate、onStart这些生命周期,是因为它要应对各种用户交互场景。系统服务不是这种玩法,PMS的启动顺序完全由SystemServer按服务间的依赖关系手工排定。说得直白一点:PMS在整个系统里处于极早期,因为后面几乎每个服务都要来问“这个包存不存在”“这个权限授予过没有”,但它自己又依赖Installer先把底层文件系统准备到位,所以它被放在startBootstrapServices里。

拿装修打个比方:PMS相当于整栋楼的物业。物业要等水电(Installer)通了才能上岗;物业上岗后,各户业主(其他系统服务)才能开始正常入住。如果水电没通就硬让物业去办事,后面全是扯淡。

1.2 三个阶段的入口坐标

有了这张图,翻源码就轻快多了。准备阶段的入口在SystemServer.java的startBootstrapServices(),这一段本身没多少业务逻辑,主要是把Installer等服务拉起来,然后调用PMS的静态入口方法。

构造阶段的入口是PackageManagerService.main(),里面new出一个PMS实例,真正的重活全在构造方法里。这里要特别提醒一点,PMS不是在SystemServiceManager.startService()里面被创建出来的,它没有走通用服务模板,原因后面讲。

就绪阶段的入口是systemReady(),SystemServer等到AMS、WMS这些核心服务都准备得差不多了,才会在后期回调它,让PMS做对外联动。

1.3 带着“完成标志”去读源码

源码里有很多标志位,读的时候一定要盯住。比如构造结束的标志是ServiceManager.addService("package", m)被调用,就绪结束的标志是mSystemReady = true。遇到启动卡顿、崩溃、ANR,先看卡在哪个标志之前还是之后,排查范围瞬间就缩小了。

这一点说起来简单,但我见过不少人排了很久的启动问题,最后发现连“服务注册没注册上”都没确认过。所以我把“完成标志”这个概念放在最前面,它是整个PMS启动分析的第一块基石。

2. 起点:SystemServer.startBootstrapServices里的关键准备

2.1 Installer:PMS的“手脚”,必须先就位

SystemServer在创建PMS之前,会先启动一个叫Installer的Java服务。它本身不干重活,真正的体力活都在native层的installd里面,两者通过Binder通信。PMS后面创建用户目录、安装卸载APK、做dex优化,靠的全是Installer去驱动native层。

如果Installer没就绪,PMS构造时随便调用一个安装相关的接口都可能失败,整个system_server都起不来。所以Installer的启动顺序必须在PMS之前,这是硬依赖,没有商量余地。在Android 11里,这一步是通过mSystemServiceManager.startService(Installer.class)完成的。

2.2 DeviceIdentifiersPolicyService:一个容易被忽略的前置服务

仔细看startBootstrapServices(),Installer之后通常还会拉起一个DeviceIdentifiersPolicyService。这个服务平时存在感很低,但它提供的是设备标识符相关的策略查询能力。Android 11开始对设备标识符的访问控制收紧,PMS后续处理某些包信息和权限判断时,要有一个统一的策略依据。

对这个服务没概念没关系,你只要知道它属于PMS启动的“前置依赖”之一就行。很多人在梳理PMS启动时把这段直接跳过去,真到排查设备标识符相关问题时会发现少一块拼图。

2.3 PMS.main():静态入口与特殊的注册方式

接下来就是重点了。SystemServer里的核心调用路径大致是这个样子:

// SystemServer.java 核心调用路径(节选) private void startBootstrapServices() { // installd 的 Java 封装服务 Installer installer = mSystemServiceManager.startService(Installer.class); mInstaller = installer; // 设备标识符策略服务 mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class); // 创建 PMS 并把 Binder 服务注册进 ServiceManager mPackageManagerService = PackageManagerService.main(mSystemContext, installer, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore); mFirstBoot = mPackageManagerService.isFirstBoot(); }

PMS并没有走SystemServiceManager.startService()这个通用模板,而是直接用静态方法PMS.main()创建。原因有两个:一是PMS构造需要factoryMode、onlyCore这类额外参数,通用模板传不进来;二是PMS的构造过程太长,单独用一个入口管理更清晰。

main()内部最核心的就是两件事:

// PackageManagerService.main() 的核心调用路径(节选) public static PackageManagerService main(...) { PackageManagerService m = new PackageManagerService(...); ServiceManager.addService("package", m); return m; }

ServiceManager.addService("package", m)这一步是把PMS的Binder实例注册到系统全局的servicemanager里。注册完之后,其他进程通过getSystemService("package")拿到的就是它。很多人误以为“PMS创建”就是“PMS可用”,实际上加了这个注册动作才算真正对外开放。

2.4 第一时间捕获mFirstBoot

startBootstrapServices()里还有一个容易被忽略的动作:在PMS.main()返回后,SystemServer立刻调用isFirstBoot(),把结果存起来。

为什么这么急?因为后面启动其他服务时,有大量逻辑要区分“全新设备首次开机”和“正常重启”。首次开机要创建系统用户、安装预置应用、给系统应用授予默认权限;非首次开机则是从packages.xml里恢复历史状态。这个判断必须在PMS刚构造完的那一刻就固定下来,等所有流程跑完再回头问,黄花菜都凉了。

3. 从new到可用:PMS构造方法里那几个绕不开的初始化

PMS构造方法的体量相当可观,逐行读很容易头晕。把它拆成几大块之后,思路会清晰很多:Context与日志、injector与权限子系统、Settings与packages.xml、SystemConfig、目录扫描、收尾写盘。

3.1 依赖收拢到mInjector

Android 11的PMS构造函数一开始就会构建一个PackageManagerServiceInjector,把Context、Installer、Settings等一大把依赖装进同一个容器里。这样做最直接的好处是单元测试时能替换真实依赖,系统定制时也多了一个注入点。

另外一个关键组件是PermissionManagerService。Android 11里权限相关逻辑已经从PMS里独立出去,构造函数中通过PermissionManagerService.create()创建权限服务,PMS持有它的引用,后面扫描包的时候要把权限声明、默认授权全部交给它处理。如果还拿着老版本源码的思路去找权限相关字段,八成会扑空。

3.2 Settings与packages.xml:先恢复历史状态再开始扫描

构造PMS时会先创建Settings对象,它对立的是/data/system目录下的packages.xml、packages.list等文件。随后调用readLPw()把已有状态读进内存。

首次开机时这些文件根本不存在,系统会据此判断是首次启动,PMS在扫描完成后把内存里的状态整体写盘。非首次开机则会把上次安装过的包、授予过的权限、用户偏好全部恢复回来,这样扫描时就能直接对比新旧差异,而不是从零重建整个世界。

我一度觉得这段代码平平无奇,直到有一次开发机上packages.xml被误删,开机后所有应用全部变成“新装”状态,才真正意识到这个恢复步骤对日常体验有多重要。

3.3 SystemConfig:系统级权限配置的一次性快照

PMS构造时还会调用SystemConfig.getInstance()。这个实例是进程级单例,负责把/system/etc/permission、/vendor/etc/permission等目录下的XML全部解析一遍,得到系统权限、共享库清单、未加密应用白名单等基础配置。

这些配置是所有权限判断的底层依据。如果改过这些XML,一定要保证格式正确,一个标签写错,解释到一半停住,后面包管理的行为可能整体错乱。这个坑我在改系统权限白名单时踩过,后来养成了改完先单独解析验证的习惯。

3.4 扫描前的目录判定:framework、app、vendor app、data app

在真正扫描之前,PMS会先把要扫的目录都准备好。核心路径大致是这样:

// PMS 构造方法中目录路径的初始化(节选) mSystemFrameworkDir = new File(Environment.getRootDirectory(), "framework"); mSystemAppDir = new File(Environment.getRootDirectory(), "app"); mProductAppDir = new File(Environment.getProductDirectory(), "app"); mVendorAppDir = new File(Environment.getVendorDirectory(), "app"); mAppInstallDir = new File(Environment.getDataDirectory(), "app");

这几行看着简单,其实是整个包管理视野的起点:系统包、产品分区包、厂商包、用户安装包,来源全部在这里。不同分区因为只读属性不同,扫描时的处理策略也不一样。后面讨论扫描顺序,依赖的就是这张目录清单。

3.5 scanDirTracedLI:启动流程里的重头戏

构造方法最终会把上面这些目录交给scanDirTracedLI()。每个目录下的APK会被ParallelPackageParser并行解析AndroidManifest.xml,解析结果进入scanPackageTracedLI()做真正的包登记:解析四大组件、权限声明、签名、应用信息,然后写入内存中的包集合。

这个过程是阻塞的,而且发生在system_server主线程上。换句话说,APK总量直接决定开机耗时。这也是为什么厂商做开机提速时,几乎都要在这个环节做文章:要么并行解析,要么延迟部分包的扫描,要么强化缓存命中。

3.6 收尾:清理无主状态并写回packages.xml

扫描全部结束后,PMS还会做几件收尾事:清理已经不存在但还残留状态的包、根据扫描结果重新计算默认权限、把变化写回packages.xml。有些版本在结尾还会主动调用一次GC。

这几步看似琐碎,但对后续启动速度影响很大。一个稳定的packages.xml能让下次开机走增量恢复,而不是全量重建。启动慢的问题一直都在扫描阶段反复出现,往往就是写盘恢复这条链路不够健壮。

4. 扫描过程拆解:framework/vendor/data目录为什么按这个顺序来

4.1 扫描顺序背后的依赖关系

PMS扫描目录不是随便扫,顺序背后是依赖关系。framework目录必须最先扫,因为里面有framework-res.apk和系统框架相关的资源、权限定义。后续其他APK解析时可能要引用framework里的资源ID,扫描顺序一旦颠倒,就会遇到“资源找不到”这类诡异问题。

framework之后是system和product目录里的系统应用,再到vendor目录的厂商应用,最后才是data/app下的用户安装应用。整体思路很朴素:先基础,后上层。

顺序目录内容特点
1/system/frameworkframework-res、核心框架包只读,启动期必扫
2/system/app、/product/app系统应用、产品分区应用只读,改动依赖OTA
3/vendor/app厂商预置应用只读,厂商维度
4/data/app用户安装应用可写,变化频繁

这个顺序在总体流程里属于“常量”,除非做深度定制,否则不应该去动它。如果改了扫描顺序,开机后轻则应用消失,重则系统UI起不来。

4.2 解析失败不会让system_server崩溃

扫描每个APK时,PMS不是“零容错”的严格模式。恰恰相反,单个包解析失败会被吞掉,记一条日志然后继续扫下一个。一个损坏的APK最多造成某个应用不可见,不会把整个开机流程打断。这个机制保证了设备的最低下限可用性。

知道这个机制后,逆向排查反而简单:如果某次开机后有应用消失,去logcat里搜对应包名的解析报错,基本能锁定是APK文件本身的问题。一个损坏的APK、一个签名对不上的包,都会在这一步被悄悄跳过。

4.3 动态与静态:为什么有些包会被“记住”有些不会

/data/app下的包每次开机前都可能被用户装删,所以它的扫描结果必须和packages.xml里的历史记录做一次“求差”:新增的包要写入,被删掉的包要清掉状态。

/system这些只读目录就稳定得多,只要文件没变,扫描结果基本一致。这也是为什么OTA升级后、或者手动改动system分区后,经常能明显感觉到开机变久——大量包的内容和哈希都变了,PMS只能重新解析。

4.4 并行解析与锁竞争的取舍

Android 11的ParallelPackageParser会开启多个解析线程,把AndroidManifest.xml的解析分摊到多核。但解析完成后,登记进内存结构这一步仍然在synchronized (mPackages)互斥块里做,因为mPackages、权限管理这些全局结构不能同时被多线程写。

所以严格说,PMS扫描阶段是“解析并行、登记串行”。APK数量多时并行确实有帮助,但如果包特别复杂,登记阶段的锁竞争依然可能成为瓶颈。厂商做开机耗时优化,瞄准的通常就是这两个环节:要么降低解析时长,要么压缩锁内操作。

5. 权限、多用户与持久化:启动流程里的三条暗线

5.1 PermissionManagerService:权限从PMS里拆出来之后

Android 11的PMS构造时会创建PermissionManagerService,并向它提供系统配置、Settings引用。应用安装时的权限授予、默认权限处理、权限迁移,后续都由这个服务承载。

启动过程中比较关键的是默认权限的初始化:系统应用在首次开机时会被授予一批默认权限,比如某些预置应用需要位置、存储权限,这些授权记录会写进Settings,下次启动时读取恢复。如果不理解这条线,排权限问题时会一直以为PMS在管,实际上权限的抓手已经换地方了。

5.2 packages.xml的读取与多用户状态恢复

Settings.readLPw()恢复的不只是包列表,还有按用户区分的状态:User 0(机主)、User 10+(工作资料等)各自装了什么、禁用了什么、授予了什么权限。Android 11里多用户体系已经很常见,PMS启动时必须把每个用户的状态一起恢复到内存,否则切换用户后看到的桌面、应用列表就会异常。

这块内容常被忽略,因为它藏在Settings里,不直接出现在PMS的扫描主线上。但实际工作中碰到“某个用户下应用消失”这类问题,回溯到这里往往就有答案。

5.3 数据损坏的连锁反应

packages.xml一旦损坏,轻则解析失败被忽略,系统用默认状态启动;重则大量应用数据关联丢失,表现为开机后应用数据被清、权限要重新询问。开发阶段调试系统,动/data/system下的文件前一定要先备份。我自己习惯把packages.xml和packages.list一起拷出来,恢复时两个一起放回去,只恢复其中一个反而可能造成不一致。

对于那些已经对外发布的设备,这类问题基本只能恢复出厂或者走维修通道。所以PMS这层持久化数据的重要程度,不比APK本身低。

5.4 onlyCore参数与“最小系统”思维

PMS.main()接收的onlyCore参数经常被一笔带过,但它非常有用。在工厂测试模式或者只需要启动核心功能的场景下,传true会让PMS跳过大量非核心目录的扫描,以最快速度把系统拉起来。

理解这个参数后,再看SystemServer里那几个布尔量的传递,就会明白PMS为什么要专门接收它们而不是自己判断。它不是设计冗余,而是给上层场景预留的开关。

6. systemReady:PMS启动最后的“合闸”,以及它刻意不管的事

6.1 systemReady的调用时机

PMS构造完成,并不代表可以立即对外服务。SystemServer还要继续启动AMS、WMS这些核心服务,等它们都就绪后,才会在startOtherServices的收尾阶段回调PMS.systemReady()。

这里的逻辑有点反直觉。按理说包管理应该越早可用越好,但PMS在就绪阶段要做跨服务联动,比如通知其他服务“包管理可用了”“有应用变更了”。如果对方还没ready,消息发了也没人接,等于白发。

6.2 为什么扫描不放到systemReady里

反过来想一下:如果PMS把所有重活都放到systemReady,开机时序会非常难看。桌面要等PMS扫完所有APK才能画图标,但PMS又要等AMS、WMS先ready,等于把关键路径无限拉长。

所以Android的设计思路是:把“扫描、解析、登记”这些最重的计算放到构造函数里,让PMS在注册进servicemanager的一刻数据已经基本齐全;systemReady只做轻量的状态修正和对外通知。这个“计算提前、联动延后”的思路,是整套PMS启动设计里最值得抄作业的地方。

6.3 systemReady里常见动作

就绪阶段主要做这么几件事:置位mSystemReady;调用PermissionManagerService.systemReady()补齐权限相关的最终状态;处理构造期间积攒的挂起消息,比如某个应用安装完成的广播,当时系统还没ready不能发,现在统一补发;触发一些后置的dex优化任务。

这个“挂起消息队列”机制很有用。它保证了PMS在完全可用之前不丢事件,等ready之后再一次性补发。真遇到广播延迟,先检查是不是被挂到了这个队列后面。

6.4 可观察信号:什么时候算PMS真的起来了

从开发者视角,有几个很直接的验证信号:

  • adb shell service check package能正常返回
  • adb shell dumpsys package能输出包列表
  • pm list packages不阻塞、不报错
  • 桌面和系统UI能正常展示应用图标

日志角度,构造开始时有“Creating package manager service”打点,systemReady阶段也有对应日志。把这些锚点串起来,PMS启动的“时间线”就非常清楚了。

6.5 启动失败场景:PMS hang住是什么样子

如果system_server主线程卡在PMS构造或systemReady,会触发系统的watchdog机制。现象是开机动画一直转、dumpsys无响应,最终system_server被重启。根因大概率落在扫描环节:APK太多、某个APK解析异常慢、数据目录损坏。

排查时先抓system_server的Anr trace,看线程栈停在哪个方法。停在scanDirTracedLI说明是扫描问题,停在readLPw说明是持久化数据恢复问题,停在PermissionManagerService.systemReady说明是权限初始化问题。定位到方法,方向就有了。

7. 从日志到现场:调试PMS启动的几条实用经验

7.1 用logcat锚点定位阶段

日常调试,我第一件事就是开日志过滤:

adb logcat -s PackageManager PackageManagerService SystemServer

实际操作时会看到,构造阶段的“Creating package manager service”出现后,跟着一大段扫描日志;等到systemReady相关的日志出现,就代表PMS进入了就绪阶段。如果中间长时间没有进展,就有理由怀疑卡在扫描。

再补一条经验:用时间戳粗算。logcat每条日志都带时间,记下两个锚点之间的时间差,和正常设备对比,差异太大就是线索。

7.2 开机慢的经典定责与对比实验

拿到“开机慢”这种问题,先分清楚慢在哪个阶段。最有效的办法是做对比实验:把最近新增的APK临时移除,对比开机时间。我之前调试过一台测试设备,正常开机十几秒,那次硬是拖到一分钟,logcat里不停刷包解析错误,最后定位到/data/app里放了几个损坏的APK,清理掉之后开机立刻恢复。

做这种实验的时候注意:先备份原文件,别在正式环境乱动。系统调试本身就是在风险边缘试探,备份意识一定要有。

7.3 packages.xml损坏后怎么处理

开发机上遇到packages.xml损坏,能恢复备份就恢复备份。没备份的话,可以把损坏文件改名,让PMS重新生成,但代价很大:所有应用会被当成新装,权限、图标排序全丢。所以“先备份”这三个字,怎么强调都不过分。

如果是线上设备遇到类似问题,基本只能恢复出厂设置。这也侧面说明,PMS这层数据一旦坏了,影响面是整个用户的数字生活。

7.4 用dumpsys验证启动结果

PMS起来之后,建议马上验证:

adb shell dumpsys package | head -n 50

先看包总数是否合理,再确认关键包(launcher、systemui)都在。如果某个包没扫到,单独查:

adb shell pm path com.android.systemui

如果路径能正常输出,说明扫描完成且包已登记;如果报错,就需要回去查解析日志。这套验证方法不仅能用来排查问题,改过PMS启动逻辑的人也可以拿它做回归确认。

这篇主要是建立“时间线意识”。源码分析最怕被一个函数带偏,忘了自己处在整个启动过程的哪个位置。我实际读PMS代码时,会刻意读完一个阶段就停下来,去日志里找锚点对时间线,对上了再往下钻。下一篇打算顺着scanDirTracedLI走进单个APK的扫描链路,看PackageParser读完AndroidManifest.xml之后,PMS到底怎么把包、权限、组件这些信息登记进内存。如果你在调试启动问题时也踩过什么有意思的坑,欢迎在评论区聊聊。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进