基于Android10.0,分析PackageManagerService启动
一、PakageManagerService概述
1.1 作用
PakageManagerService(简称PKMS),是Android核心服务之一,管理着所有package相关的工作。
如:安装、卸载应用
查询、增加、删除permission相关信息
查询Application相关信息
查询已安装应用
清除用户数据,缓存
1.2 PKMS类关系
PKMS是通过Binder进行通信。IPackageManager.aidl通过工具生成Binder的服务端IPackageManager.Stub和
客户端IPackageManager.Stub.Proxy,编译后生成的路径在 out/target/common 目录下。
Binder服务端:PackageManagerService继承于IPackageManager.Stub
Binder客户端:ApplicationPackageManager(简称APM)的成员变量mPM继承于IPackageManager.Stub.Proxy;
APM继承于PakageManager对象
1.3 PKMS类成员
PKMS类共有二万多行,这里就简单介绍一下其中重要的类和成员变量。
重要成员支持类
PackageParser
这个类主要用于解析APK,解析AndroidManifest.xml文件得到package的所有信息,PackageParser.Package用于存储得到的信息。
Settings
这个类表示它服务处理设置和读取包的各种状态,是动态的,比如shareUser,permission,signature,userid等相关信息。安装包就是从安装的package中抽取信息更新Settings中的内容,特别是shareUser和origPackge。为了加速启动,Setting内容会写入data/system/pakcages.xml、packages-backup.xml和packages.list,下次启动时会直接载入。
Installer
这个类是协助安装的过程,操作基本上是在cpp里面,真正的工作是由installd执行,installd通过Native Binder实现。
类成员
//代表系统已经安装的package
final ArrayMap<String, PackageParser.Package> mPackages
//被升级过的应用列表
final private ArrayMap<String, File> mExpectingBetter
//保存PackageManager动态设置信息
final Settings mSettings;
1.4 PKMS启动流程
SystemServer启动过程中涉及的PKMS如下:
主要在下面的两个方法中
1 | private voi run() { |
startBootstrapServices方法相关PKMS的操作
1 | /** |
上面最重要的是PKMS.mian()操作,主要是创建PKMS服务,并注册到ServiceManager中。
startOtherServices方法中相关PKMS的操作
1 | /** |
在SystemServer启动过程中,PKMS最主要的操作如下:
- PKMS.main()
- PKMS.updatePackagesIfNeeded()
- PKMS.systemReady()
二、PKMS.main
1 | public static PackageManagerService main(Context context, Installer installer, |
main方法主要功能是创建PKMS对象,创建PKMS对象的执行时间比较长,在其构造函数做了很多的“重力活”,这也是Android启动慢的原因之一。构造函数的主要功能是扫描Android系统中目标文件夹中的APK,从而建立合适的数据结构来管理诸如:Package信息,四大组件、权限等各种信息。PKMS工作流程相对简单,复杂的是其中用于保存各种信息的数据结构以及它们之间的关系。PKMS构造函数主要分为五个阶段,每个阶段都会输出日志到EventLog,除了阶段1的开始部分代码,后面的代码都同时持有同步锁mPackage、mInstallLock。
1 | public PackageManagerService(Context context, Installer installer, |
2.1 PMS_START
主要的工作如下:
- 创建Settings对象;
- 将8类shareUserId到mSettings;
- 初始化SystemConfig;
- 创建名为“PackageManager”的handler线程
mHandlerThread
; - 创建UserManagerService、PermissionManagerService服务;
- 通过解析目录中的xmL文件构造共享mSharedLibraries;
1 | LockGuard.installLock(mPackages, LockGuard.INDEX_PACKAGES); |
2.1.1 new Settings
Setting构造函数主要工作是创建系统文件夹和一些包管理的文件。
packages.xml、packages-backup.xml是一组,用于描述系统所安装的Package信息,其中packages-backup.xml是packages.xml的备份
packages.list用于描述系统中存在的所有非系统自带的apk信息以及UID大于10000的apk。当APK有变化时,PKMS就会更新该文件。
1 | Settings(File dataDir, PermissionSettings permission, Object lock) { |
2.1.2 addSharedUserLPw
该方法将shareUserId name和一个int类型的UID对应起来。UID的定义在Process.java中。
1 | SharedUserSetting addSharedUserLPw(String name, int uid, int pkgFlags, int pkgPrivateFlags) { |
UID、GID
1 | /** |
Setting模块的AndroidManifest.xml里面,如下所示:
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
在xml里面android:sharedUserId属性设置为”android.uid.system”。sharedUserId这个属性主要有两个作用:
1.两个或者多个声明了同一种sharedUserId的应用可以共享彼此的数据
2.通过声明特定sharedUserId,该应用所在进程将赋予指定的UID。如Setting声明了system的uid,则就可以共享system用户所对应的权限。
除了了xml了声明sharedUserId外,应用编译的时候还必须使用对应的证书进行签名。如Setting需要platform的签名。
PID、UID的区别
PID是进程的身份识别,程序一旦运行,就会给应用分配唯一的PID。一个应用可能包含多个进程,每个进程有唯一的一个PID。进程终止后PID被系统回收,再次打开应用,会分配一个PID(新进程的PID一般比之前的号大)。
调用adb shell ps可以查看系统运行的进程。
UID是用户ID。UID在linux中就是用户ID,表明哪个用户运行了这个程序,主要用于权限的管理。而Android为单用户系统,这时候UID被赋予了新的使命,数据共享,为了实现数据共享,android为每个应用都分配了不同的uid,不像传统的linux,每个用户相同就为之分配相同的UID。
GID时用户组ID。对于普通的应用程序来说GID等于UID,由于每个应用程序的UID和GID不相同,所以不管是native还是java层都能够达到保护私有数据的作用。
adb shell cat /proc/PID号/status
2.1.3 SystemConfig.getInstance
SystemConfig构造函数中主要通过readPermissions函数将对应目录下的xml文件中定义的各个节点读取出来保存到SystemConfig成员变量中。在终端的/system/etc/permissions目录下可以看到很多xml配置文件。
1 | SystemConfig() { |
如下是终端的/system/etc/permissions目录的xml配置文件:
1 | HWSTF:/system/etc/permissions $ ls -all |
这些配置文件都是编译时从framework指定位置拷贝过来的(framework/native/data/etc),下面是platform.xml里面的部分
1 | <permissions> |
readPermissions方法内部调用readPermissionsFromXml方法来解析xml里面的各个节点,其中xml涉及到的标签内容有permission、assign-permission、library、feature等,这些标签的内容解析出来保存到SystemConfig的对应数据结构的全局变量中,以便管理查询。
feature用来描述设备是否支持硬件特性;
library用于指定系统库,当应用程序运行时,系统会为进程加载一些必须的库;
assign-permission将system中描述的permission与uid关联;
permission将permission和gid关联。
2.1.4 readLPw
读取在Settings初始化时的文件,并保存
1 | boolean readLPw(@NonNull List<UserInfo> users) { |
2.2 PMS_SYSTEM_SCAN_START
主要的工作是扫描系统apk。
1 | 扫描目录 |
1 | long startTime = SystemClock.uptimeMillis(); |
2.2.1 PackageParser.Package
Package数据,表示从磁盘上的apk文件解析出来的package,一个包由一个基础的apk和多个拆分的apk构成。
1 | /** |
2.2.2 scanDirTracedLI
这个是扫描目录的函数,主要是解析apk中xml的数据,并把相关信息保存到PackageParser.Package中。
1 | private void scanDirTracedLI(File scanDir, final int parseFlags, int scanFlags, long currentTime) { |
里面跳转的方法比较多,其主要流程如下:
2.3 PMS_DATA_SCAN_START
主要的工作是扫描data分区的apk,利用上一步获取的系统应用信息执行相关操作,最后更新动态库路径。
1 | //非加密状态 |
2.4 PMS_SCAN_END
主要的工作如下:
- 更新所有权限;
- 为系统应用准备空间和数据
- 将上述信息写回/data/system/packages.xml;
1 | EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END, |
2.5 PMS_READY
主要工作是创建PackageInstallerService服务。
1 | EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_READY,SystemClock.uptimeMillis()); |
三、PKMS操作
3.1 getPackageManager
在ContextImpl中
/frameworks/base/core/java/android/app/ContextImpl.java
1 | @Override |
/frameworks/base/core/java/android/app/ActivityThread.java
1 | @UnsupportedAppUsage |
3.2 updatePackagesIfNeeded
这里主要是对package进行Dexopt
1 | @Override |
PackageManagerServiceUtils.getPackagesForDexopt的方法,主要的功能是根据app的重要性,对dexopt的顺序进行排序,重要的app将会首先会执行Dexopt操作,以防止设备空间不足。排序的原则是根据app的使用时间长短。
1 | // Sort apps by importance for dexopt ordering. Important apps are given |
performDexOptUpgrade方法是对packages执行dexopt操作。首先是创建package的配置文件,配置文件生成后再执行performDexOptTraced的操作,里面最后执行的操作是mInstaller.dexopt方法。
1 | /** |
3.3 performFstrimIfNeeded
这个操作主要是用来清理磁盘碎片,条件是离上一次的清理的时间间隔3天。sm.runMaintenance方法调用的是StorageManagerService中的runMaintenance方法。
1 | private static final long DEFAULT_MANDATORY_FSTRIM_INTERVAL = 3 * DateUtils.DAY_IN_MILLIS; |
1 | // Binder entry point for kicking off an immediate fstrim |
3.4 systemReady
系统初始化完成并且PKMS也初始化完成,PKMS需要进行的操作,如通知其他服务执行systemReady操作。
1 | @Override |
3.5 waitForAppDataPrepared
该方法主要是等待mPrepareAppDataFuture多线程的任务都处理完成。
1 | public void waitForAppDataPrepared() { |
四、PKMS总结
主要介绍了PKMS从SystemServer开始启动的相关流程,并详细介绍了流程相关的重要方法。PKMS初始化按照其代码的日志区分可以分为五个阶段,但从其功能上来说,可以分为三个阶段:
第一阶段:主要是扫描解析XML文件,并将其中的信息保存到特定的数据结构中,为下一阶段工作提供重要的参考信息;
第二阶段:扫描各个目录下的APK,可以看到手机上apk,扫描的工作量就越大,系统启动速度也就越慢; 第三阶段:主要是更新相关信息并启动PackageInstallerService服务。
PKMS构造函数工作任务非常繁重,特别是在扫描apk的时候。如果需要优化该流程以加快启动速度,则这个是一个好的方向。如延时扫描不重要的apk,或者保存Package信息到文件,然后启动时从文件中恢复这些信息以减少APK文件读取并解析xml的工作量。但是一直没有比较满意的方案,原因很多,比如apk之间有着比较多微妙的关系,因此到底延时扫描哪些apk,尚不能确定。还有一种方案是,PKMS可以启动多个线程扫描不同的目录,在android10.0中可以看到在扫描的时候已经启动多线程来扫描。对于PKMS的优化,Google基本上能想到基本上已经优化了,我们也可以思考。
后记
对于这些流程中有一些操作并没有详细的描述,由于篇幅有限将单独介绍。
Android dex,odex,oat,vdex,art文件结构
参考资料
https://blog.csdn.net/thh159/article/details/88289642
https://www.jianshu.com/p/cbf8e73f41ed
https://blog.csdn.net/innost/article/details/47253179
https://blog.csdn.net/xiaosongluo/article/details/52014585
https://blog.csdn.net/u013553529/article/details/61962439
附录
源码路径
1 | frameworks/base/core/java/android/content/pm/PackageManager.java |
LI、LIF、LPw、LPr的含义
PackageManagerService中方法名中LI、LIF、LPw、LPr的含义
PackageManagerService内部用两个锁,mInstallLock和mPackages。LI、LIF、LPw、LPr中的L,指的是Lock,而后米娜跟的是I和P两个锁,I表示mInstallLock同步锁,P表示mPackages同步锁。LPw、LPr中的w表示writing,r表示reading。LIF中的F表示Frozen.
mInstallLockt同步锁是指安装app的时候,对安装的处理要用synchronized保护起来,用于保护所有对installd的访问,installd通常包含对应用数据的繁重操作。
mPackages同步锁用来保护内存中已经解析的包信息以及其他相关状态。因为争抢mPackage锁的请求很多,只能短时间持有这个锁。
1 | //这种情况是允许的,因为mPackages处理完成之后,其他对mPackages操作的请求可以处理,不需要等待太久 |
@GuardedBy注解,用于标记哪些变量要用同步锁保护起来。
PackageManagerService中的注解
1 | /** |
A/B(无缝)系统更新
A/B 系统更新(也称为无缝更新)的目标是确保在无线下载 (OTA) 更新期间在磁盘上保留一个可正常启动和使用的系统。采用这种方式可以降低更新之后设备无法启动的可能性,这意味着用户需要将设备送到维修和保修中心进行更换和刷机的情况将会减少。其他某些商业级操作系统(例如 ChromeOS)也成功使用了 A/B 更新机制。
要详细了解 A/B 系统更新,请参见分区选择(槽位)一节。
A/B 系统更新可带来以下好处**:
- OTA 更新可以在系统运行期间进行,而不会打断用户。用户可以在 OTA 期间继续使用其设备。在更新期间,唯一的一次宕机发生在设备重新启动到更新后的磁盘分区时。
- 更新后,重新启动所用的时间不会超过常规重新启动所用的时间。
- 如果 OTA 无法应用(例如,因为刷机失败),用户将不会受到影响。用户将继续运行旧的操作系统,并且客户端可以重新尝试进行更新。
- 如果 OTA 更新已应用但无法启动,设备将重新启动回旧分区,并且仍然可以使用。客户端可以重新尝试进行更新。
- 任何错误(例如 I/O 错误)都只会影响未使用的分区组,并且用户可以进行重试。由于 I/O 负载被特意控制在较低水平,以免影响用户体验,因此发生此类错误的可能性也会降低。
- 更新包可以流式传输到 A/B 设备,因此在安装之前不需要先下载更新包。流式更新意味着用户没有必要在
/data
或/cache
上留出足够的可用空间来存储更新包。 - 缓存分区不再用于存储 OTA 更新包,因此无需确保缓存分区的大小要足以应对日后的更新。
- dm-verity 可保证设备将使用未损坏的启动映像。如果设备因 OTA 错误或 dm-verity 问题而无法启动,则可以重新启动到旧映像。(Android 验证启动不需要 A/B 更新。)
关于 A/B 系统更新
进行 A/B 更新时,客户端和系统都需要进行更改。不过,OTA 更新包服务器应该不需要进行更改:更新包仍通过 HTTPS 提供。对于使用 Google OTA 基础架构的设备,系统更改全部是在 AOSP 中进行,并且客户端代码由 Google Play 服务提供。不使用 Google OTA 基础架构的原始设备制造商 (OEM) 将能够重复使用 AOSP 系统代码,但需要自行提供客户端。
如果 OEM 自行提供客户端,客户端需要:
- 确定何时进行更新。由于 A/B 更新是在后台进行,因此不再需要由用户启动。为了避免干扰用户,建议将更新安排在设备处于闲时维护模式(如夜间)并已连接到 WLAN 网络时进行。不过,客户端可以使用您希望使用的任何启发法。
- 向 OTA 更新包服务器进行核查,确定是否有可用的更新。这应与您现有的客户端代码大体相同,不过您需要表明相应设备支持 A/B 更新。(Google 的客户端还包含立即检查按钮,以便用户检查是否有最新更新。)
- 调用
update_engine
(使用 HTTPS 网址),以获取更新包(假设有可用的更新包)。update_engine
将在流式传输更新包的同时,在当前未使用的分区上更新原始数据块。 - 根据
update_engine
结果代码向您的服务器报告安装是成功了还是失败了。如果更新已成功应用,update_engine
将会告知引导加载程序在下次重新启动时启动到新的操作系统。如果新的操作系统无法启动,引导加载程序将会回退到旧的操作系统,因此无需在客户端执行任何操作。如果更新失败,客户端将需要根据详细的错误代码确定何时(以及是否)重试。例如,优秀的客户端能够识别出是一部分(“diff”)OTA 更新包失败,并改为尝试完整的 OTA 更新包。
客户端可能会:
- 显示通知,以提醒用户重新启动系统。如果您想要实施鼓励用户定期更新的政策,则可以将该通知添加到客户端。如果客户端不提示用户,用户将会在下次重新启动系统时收到更新。(Google 的客户端会有延迟,该延迟可按每次更新进行配置。)
- 显示通知,以告知用户他们是启动到了新的操作系统版本,还是应启动到新的操作系统版本,但却回退到了旧的操作系统版本。(Google 的客户端通常不会显示此类通知。)
在系统方面,A/B 系统更新会影响以下各项:
- 分区选择(槽位)、
update_engine
守护进程,以及引导加载程序交互(如下所述) - 编译过程和 OTA 更新包生成(如实现 A/B 更新中所述)
注意:只有对于新设备,才建议通过 OTA 实现 A/B 系统更新。
分区选择(槽位)
A/B 系统更新使用两组称为槽位(通常是槽位 A 和槽位 B)的分区。系统从“当前”槽位运行,但在正常操作期间,运行中的系统不会访问未使用的槽位中的分区。这种方法通过将未使用的槽位保留为后备槽位,来防范更新出现问题:如果在更新期间或更新刚刚完成后出现错误,系统可以回滚到原来的槽位并继续正常运行。为了实现这一目标,当前槽位使用的任何分区(包括只有一个副本的分区)都不应在 OTA 更新期间进行更新。
每个槽位都有一个“可启动”属性,该属性用于表明相应槽位存储的系统正确无误,设备可从相应槽位启动。系统运行时,当前槽位处于可启动状态,但另一个槽位则可能包含旧版本(仍然正确)的系统、包含更新版本的系统,或包含无效的数据。无论当前槽位是哪一个,都有一个槽位是活动槽位(引导加载程序在下次启动时将使用的槽位,也称为首选槽位)。
此外,每个槽位还都有一个由用户空间设置的“成功”属性,仅当相应槽位处于可启动状态时,该属性才具有相关性。被标记为成功的槽位应该能够自行启动、运行和更新。未被标记为成功的可启动槽位(多次尝试使用它启动之后)应由引导加载程序标记为不可启动,其中包括将活动槽位更改为另一个可启动的槽位(通常是更改为在尝试启动到新的活动槽位之前正在运行的槽位)。关于相应接口的具体详细信息在 boot_control.h
中进行了定义。
更新引擎守护进程
A/B 系统更新过程会使用名为 update_engine
的后台守护进程来使系统做好准备,以启动到更新后的新版本。该守护进程可以执行以下操作:
- 按照 OTA 更新包的指示,从当前槽位 A/B 分区读取数据,然后将所有数据写入到未使用槽位 A/B 分区。
- 在预定义的工作流程中调用
boot_control
接口。 - 按照 OTA 更新包的指示,在将数据写入到所有未使用槽位分区之后,从新分区运行安装后程序。(有关详细信息,请参阅安装后)。
由于 update_engine
守护进程本身不会参与到启动流程中,因此该守护进程在更新期间可执行的操作受限于当前槽位中的 SELinux 政策和功能(在系统启动到新版本之前,此类政策和功能无法更新)。为了维持一个稳定可靠的系统,更新流程不应修改分区表、当前槽位中各个分区的内容,以及无法通过恢复出厂设置擦除的非 A/B 分区的内容。
更新引擎源代码
update_engine
源代码位于 system/update_engine
中。A/B OTA dexopt 文件分开放到了 installd
和一个程序包管理器中:
frameworks/native/cmds/installd/
ota* 包括安装后脚本、用于 chroot 的二进制文件、负责调用 dex2oat 的已安装克隆、OTA 后 move-artifacts 脚本,以及 move 脚本的 rc 文件。frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(加上OtaDexoptShellCommand
)是负责为应用准备 dex2oat 命令的程序包管理器。
如需实际示例,请参阅 /device/google/marlin/device-common.mk
。
更新引擎日志
对于 Android 8.x 及更低版本,可在 logcat
及错误报告中找到 update_engine
日志。要使 update_engine
日志可在文件系统中使用,请将以下更改添加到您的细分版本中:
这些更改会将最新的 update_engine
日志的副本保存到 /data/misc/update_engine_log/update_engine.YEAR-TIME
。除当前日志以外,最近的五个日志也会保存在 /data/misc/update_engine_log/
下。拥有日志组 ID 的用户将能够访问相应的文件系统日志。
引导加载程序交互
boot_control
HAL 供 update_engine
(可能还有其他守护进程)用于指示引导加载程序从何处启动。常见的示例情况及其相关状态包括:
- 正常情况:系统正在从其当前槽位(槽位 A 或槽位 B)运行。到目前为止尚未应用任何更新。系统的当前槽位是可启动且被标记为成功的活动槽位。
- 正在更新:系统正在从槽位 B 运行,因此,槽位 B 是可启动且被标记为成功的活动槽位。由于槽位 A 中的内容正在更新,但是尚未完成,因此槽位 A 被标记为不可启动。在此状态下,应继续从槽位 B 重新启动。
- 已应用更新,正在等待重新启动:系统正在从槽位 B 运行,槽位 B 可启动且被标记为成功,但槽位 A 之前被标记为活动槽位(因此现在被标记为可启动)。槽位 A 尚未被标记为成功,引导加载程序应尝试从槽位 A 启动若干次。
- 系统已重新启动到新的更新:系统正在首次从槽位 A 运行,槽位 B 仍可启动且被标记为成功,而槽位 A 仅可启动,且仍是活动槽位,但未被标记为成功。在进行一些检查之后,用户空间守护进程
update_verifier
应将槽位 A 标记为成功。
流式更新支持
用户设备并非在 /data
上总是有足够的空间来下载更新包。由于 OEM 和用户都不想浪费 /cache
分区上的空间,因此有些用户会因为设备上没有空间来存储更新包而不进行更新。为了解决这个问题,Android 8.0 中添加了对流式 A/B 更新(下载数据块后直接将数据块写入 B 分区,而无需将数据块存储在 /data
上)的支持。流式 A/B 更新几乎不需要临时存储空间,并且只需要能够存储大约 100KiB 元数据的存储空间即可。
要在 Android 7.1 中实现流式更新,请选择以下补丁程序:
无论是使用 Google 移动服务 (GMS),还是使用任何其他更新客户端,都需要安装这些补丁程序,才能在 Android 7.1 中支持流式传输 A/B 更新包。
A/B 更新过程
当有 OTA 更新包(在代码中称为有效负载)可供下载时,更新流程便开始了。设备中的政策可以根据电池电量、用户活动、充电状态或其他政策来延迟下载和应用有效负载。此外,由于更新是在后台运行,因此用户可能并不知道正在进行更新。所有这些都意味着,更新流程可能随时会由于政策、意外重新启动或用户操作而中断。
OTA 更新包本身所含的元数据可能会指示可进行流式更新,在这种情况下,相应更新包也可采用非流式安装方式。服务器可以利用这些元数据告诉客户端正在进行流式更新,以便客户端正确地将 OTA 移交给 update_engine
。如果设备制造商具有自己的服务器和客户端,便可以通过确保以下两项来实现流式更新:确保服务器能够识别出更新是流式更新(或假定所有更新都是流式更新),并确保客户端能够正确调用 update_engine
来进行流式更新。制造商可以根据更新包是流式更新变体这一事实向客户端发送一个标记,以便在进行流式更新时触发向框架端的移交工作。
有可用的有效负载后,更新流程将遵循如下步骤:
步骤 | 操作 |
---|---|
1 | 通过 markBootSuccessful() 将当前槽位(或“源槽位”)标记为成功(如果尚未标记)。 |
2 | 调用函数 setSlotAsUnbootable() ,将未使用的槽位(或“目标槽位”)标记为不可启动。当前槽位始终会在更新开始时被标记为成功,以防止引导加载程序回退到未使用的槽位(该槽位中很快将会有无效数据)。如果系统已做好准备,可以开始应用更新,那么即使其他主要组件出现损坏(例如界面陷入崩溃循环),当前槽位也会被标记为成功,因为可以通过推送新软件来解决这些问题。 更新有效负载是不透明的 Blob,其中包含更新到新版本的指示。更新有效负载由以下部分组成:元数据。元数据在更新有效负载中所占的比重相对较小,其中包含一系列用于在目标槽位上生成和验证新版本的操作。例如,某项操作可能会解压缩特定 Blob 并将其写入到目标分区中的特定块,或者从源分区读取数据、应用二进制补丁程序,然后写入到目标分区中的特定块。额外数据。与操作相关的额外数据在更新有效负载中占据了大部分比重,其中包含这些示例中的已压缩 Blob 或二进制补丁程序。 |
3 | 下载有效负载元数据。 |
4 | 对于元数据中定义的每项操作,都将按顺序发生以下行为:将相关数据(如果有)下载到内存中、应用操作,然后释放关联的内存。 |
5 | 对照预期的哈希重新读取并验证所有分区。 |
6 | 运行安装后步骤(如果有)。如果在执行任何步骤期间出现错误,则更新失败,系统可能会通过其他有效负载重新尝试更新。如果上述所有步骤均已成功完成,则更新成功,系统会执行最后一个步骤。 |
7 | 调用 setActiveBootSlot() ,将未使用的槽位标记为活动槽位。将未使用的槽位标记为活动槽位并不意味着它将完成启动。如果引导加载程序(或系统本身)未读取到“成功”状态,则可以将活动槽位切换回来。 |
8 | 安装后步骤(如下所述)包括从“新更新”版本中运行仍在旧版本中运行的程序。如果此步骤已在 OTA 更新包中定义,则为强制性步骤,且程序必须返回并显示退出代码 0 ,否则更新会失败。 |
9 | 在系统足够深入地成功启动到新槽位并完成重新启动后检查之后,系统会调用 markBootSuccessful() ,将现在的当前槽位(原“目标槽位”)标记为成功。 |
注意:第 3 步和第 4 步占用了大部分更新时间,因为这两个步骤涉及写入和下载大量数据,并且可能会因政策或重新启动等原因而中断。
安装后
对于定义了安装后步骤的每个分区,update_engine
都会将新分区装载到特定位置,并执行与装载的分区相关的 OTA 中指定的程序。例如,如果安装后程序被定义为相应系统分区中的 usr/bin/postinstall
,则系统会将未使用槽位中的这个分区装载到一个固定位置(例如 /postinstall_mount
),然后执行 /postinstall_mount/usr/bin/postinstall
命令。
为确保成功执行安装后步骤,旧内核必须能够:
- 装载新的文件系统格式。文件系统类型不能更改(除非旧内核中支持这么做),包括使用的压缩算法(如果使用 SquashFS 等经过压缩的文件系统)等详细信息。
- 理解新分区的安装后程序格式。如果使用可执行且可链接格式 (ELF) 的二进制文件,则该文件应该与旧内核兼容(例如,如果架构从 32 位细分版本改为使用 64 位细分版本,则 64 位的新程序应该可以在旧的 32 位内核上运行)。除非加载程序 (
ld
) 收到使用其他路径或编译静态二进制文件的指令,否则将会从旧系统映像而非新系统映像加载各种库。
例如,您可以使用 shell 脚本作为安装后程序(由旧系统中顶部包含 #!
标记的 shell 二进制文件解析),然后从新环境设置库路径,以便执行更复杂的二进制安装后程序。或者,您可以从专用的较小分区执行安装后步骤,以便主系统分区中的文件系统格式可以得到更新,同时不会产生向后兼容问题或引发 stepping-stone 更新;这样一来,用户便可以从出厂映像直接更新到最新版本。
新的安装后程序将受旧系统中定义的 SELinux 政策限制。因此,安装后步骤适用于在指定设备上执行设计所要求的任务或其他需要尽可能完成的任务(例如,更新支持 A/B 更新的固件或引导加载程序、为新版本准备数据库副本,等等)。安装后步骤不适用于重新启动之前的一次性错误修复(此类修复需要无法预见的权限)。
所选的安装后程序在 postinstall
SELinux 环境中运行。新装载的分区中的所有文件都将带有 postinstall_file
标记,无论在重新启动到新系统后它们的属性如何,都是如此。在新系统中对 SELinux 属性进行的更改不会影响安装后步骤。如果安装后程序需要额外的权限,则必须将这些权限添加到安装后环境中。
重新启动后
重新启动后,update_verifier
会触发利用 dm-verity 进行完整性检查。系统会先启动该检查,然后再启动 zygote,以避免 Java 服务进行任何无法撤消且会导致无法进行安全回滚的更改。在此过程中,如果验证启动功能或 dm-verity 检测到任何损坏,引导加载程序和内核还可能会触发重新启动。检查完成后,update_verifier
会将启动标记为成功。
update_verifier
只会读取 /data/ota_package/care_map.txt
(在使用 AOSP 代码时,该文件会包含在 A/B OTA 更新包中)中列出的数据块。Java 系统更新客户端(例如 GmsCore)会在重新启动设备前提取 care_map.txt
并设置访问权限,在系统成功启动到新版本后会删除所提取的文件。