overlay 包扫描
本文面向已经读过 vendor 与 product 扫描、扫描签名校验 的读者,继续追踪 Runtime Resource Overlay(RRO)如何从 overlay 目录进入 Android 17 的 PackageManager 和 OverlayManager。前文只需知道分区有 overlay 目录以及普通包会经过 ParsedPackage/PackageSetting;本文重点解释 overlay 的轻量发现、配置排序、分区 policy、签名校验、用户状态和 idmap 生效时机。
RRO 的“扫描”与“启用”不是同一个动作:OverlayScanner 读取 manifest 得到 ParsedOverlayInfo,OverlayConfigParser 读取 overlay/config/config.xml 得到启用/可变/优先级配置,PMS 仍然用普通 package scanner 把 APK 注册成 AndroidPackage,随后 OverlayManagerServiceImpl 才为每个用户建立或更新 OverlayInfo、生成 idmap 并通知目标包刷新资源。
读完后,读者应能从 InitAppsHelper.initSystemApps() 找到 overlay 配置初始化入口;能够解释同一个 overlay 为什么同时出现在 ParsedOverlayInfo、ParsedConfiguration、AndroidPackage 和 OverlayInfo 中;能够区分 system property 不匹配、config.xml 不合法、overlay 签名不匹配、target 缺失和 idmap 创建失败;还能够根据 code path、target、priority、mutable 和用户状态定位“扫描到了但没有生效”的问题。
1. 角色边界
1.1 发现与生效
源码文件:
frameworks/base/core/java/com/android/internal/content/om/OverlayScanner.javaframeworks/base/core/java/com/android/internal/content/om/OverlayConfig.javaframeworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
| 阶段 | owner | 主要数据 | 对外效果 |
|---|---|---|---|
| 目录发现 | OverlayScanner | ParsedOverlayInfo | 发现 overlay manifest 信息 |
| 配置解析 | OverlayConfigParser | ParsedConfiguration | enabled、mutable、policy、配置顺序 |
| PMS 包扫描 | InstallPackageHelper | AndroidPackage、PackageSetting | 包进入 PMS 包表和查询结构 |
| 用户态同步 | OverlayManagerServiceImpl | OverlayInfo、OverlayManagerSettings | 每个 user 的 overlay 状态 |
| 资源生效 | IdmapManager/AssetManager | idmap、overlay paths | target package 读取覆盖资源 |
overlay APK 被 OverlayScanner 发现,不代表它已经启用;被 OverlayManagerServiceImpl 初始化,也不代表 idmap 一定创建成功。每一层都有自己的 owner、失败方式和可观察输出。
1.2 两种配置来源
Android 17 同时支持两种 overlay 配置来源:
- system process 使用
PackageProvider,从 PMS 已扫描的AndroidPackage中取得 overlay manifest 字段,再按OverlayConfigParser读取配置文件; - zygote 使用
OverlayScanner直接扫描分区 overlay 目录,建立可供 zygote 使用的静态 overlay 配置,但不包含分区目录外的静态 overlay。
这两个实例的输入不同,不能把 zygote 侧扫描结果当成 PMS 的完整包表。system instance 的 PackageManager 视图才是运行时 OverlayManager 的事实来源。
2. 目录发现
2.1 扫描信息
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayScanner.java
/** Represents information parsed from the manifest of an overlay. */
public static class ParsedOverlayInfo {
public final String packageName;
public final String targetPackageName;
public final int targetSdkVersion;
public final boolean isStatic;
public final int priority;
public final File path;
@Nullable public final File preInstalledApexPath;
public ParsedOverlayInfo(String packageName, String targetPackageName,
int targetSdkVersion, boolean isStatic, int priority, File path,
@Nullable File preInstalledApexPath) {
this.packageName = packageName;
this.targetPackageName = targetPackageName;
this.targetSdkVersion = targetSdkVersion;
this.isStatic = isStatic;
this.priority = priority;
this.path = path;
this.preInstalledApexPath = preInstalledApexPath;
}
@NonNull
public File getOriginalPartitionPath() {
return preInstalledApexPath != null ? preInstalledApexPath : path;
}
}ParsedOverlayInfo 是轻量 manifest 结果,不包含完整组件、权限或资源表。preInstalledApexPath 用于把更新 APEX 中的 overlay 归属回原始分区;普通 overlay 的原始路径就是自身 path。因此分区归属判断不能只看 APK 当前路径。
2.2 递归扫描
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayScanner.java
public void scanDir(File partitionOverlayDir) {
if (!partitionOverlayDir.exists() || !partitionOverlayDir.isDirectory()) {
return;
}
if (!partitionOverlayDir.canRead()) {
Log.w(TAG, "Directory " + partitionOverlayDir + " cannot be read");
return;
}
final File[] files = partitionOverlayDir.listFiles();
if (files == null) {
return;
}
for (int i = 0; i < files.length; i++) {
final File f = files[i];
if (f.isDirectory()) {
scanDir(f);
}
if (!f.isFile() || !f.getPath().endsWith(".apk")) {
continue;
}
final ParsedOverlayInfo info = parseOverlayManifest(f, mExcludedOverlayPackages);
if (info == null) {
continue;
}
mParsedOverlayInfos.put(info.packageName, info);
}
}扫描行为是递归、后写覆盖:目录不存在、不可读、listFiles() 返回 null、非 APK 或 manifest 解析返回 null 都会被跳过;同包名后发现的 ParsedOverlayInfo 替换先前值。源码没有对 File[] 做排序,因此不能把文件系统枚举顺序当成稳定 priority 规则,priority 应由配置/manifest 和后续 OverlayConfig 逻辑解释。
2.3 Manifest 轻解析
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayScanner.java
@VisibleForTesting
public ParsedOverlayInfo parseOverlayManifest(File overlayApk,
List<Pair<String, File>> outExcludedOverlayPackages) {
final ParseTypeImpl input = ParseTypeImpl.forParsingWithoutPlatformCompat();
final ParseResult<ApkLite> ret = ApkLiteParseUtils.parseApkLite(input.reset(),
overlayApk,
FrameworkParsingPackageUtils.PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY);
if (ret.isError()) {
Log.w(TAG, "Got exception loading overlay.", ret.getException());
return null;
}
final ApkLite apkLite = ret.getResult();
if (apkLite.getTargetPackageName() == null) {
// Not an overlay package
return null;
}
final String propName = apkLite.getRequiredSystemPropertyName();
final String propValue = apkLite.getRequiredSystemPropertyValue();
if ((!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue))
&& !FrameworkParsingPackageUtils.checkRequiredSystemProperties(propName, propValue)) {
outExcludedOverlayPackages.add(Pair.create(apkLite.getPackageName(), overlayApk));
return null;
}
return new ParsedOverlayInfo(apkLite.getPackageName(), apkLite.getTargetPackageName(),
apkLite.getTargetSdkVersion(), apkLite.isOverlayIsStatic(),
apkLite.getOverlayPriority(), new File(apkLite.getPath()), null);
}这个入口只调用 ApkLiteParseUtils.parseApkLite(),并使用 PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY 先拿到 required property,再由 checkRequiredSystemProperties() 决定是否排除。没有 targetPackageName 的 APK 被静默视为普通/非 overlay;property 不匹配的 APK 进入排除列表,而不是 mParsedOverlayInfos。
3. 配置解析
3.1 分区 policy
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfigParser.java
public static class OverlayPartition extends SystemPartition {
static final String POLICY_ODM = "odm";
static final String POLICY_OEM = "oem";
static final String POLICY_PRODUCT = "product";
static final String POLICY_PUBLIC = "public";
static final String POLICY_SYSTEM = "system";
static final String POLICY_VENDOR = "vendor";
@NonNull
public final String policy;
private static String policyForPartition(SystemPartition partition) {
switch (partition.type) {
case PackagePartitions.PARTITION_SYSTEM:
case PackagePartitions.PARTITION_SYSTEM_EXT:
return POLICY_SYSTEM;
case PackagePartitions.PARTITION_VENDOR:
return POLICY_VENDOR;
case PackagePartitions.PARTITION_ODM:
return POLICY_ODM;
case PackagePartitions.PARTITION_OEM:
return POLICY_OEM;
case PackagePartitions.PARTITION_PRODUCT:
return POLICY_PRODUCT;
default:
throw new IllegalStateException("Unable to determine policy for "
+ partition.getFolder());
}
}
}policy 是传给 idmap2 的分区安全策略,不等于 overlay priority,也不等于 ApplicationInfo 的分区 private flag。system_ext 使用 system policy;vendor、odm、oem、product 各自有独立 policy。后续 idmap 创建需要同时知道 target、overlay 和该 policy。
3.2 配置项
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfigParser.java
public static class ParsedConfiguration {
@NonNull public final String packageName;
public final boolean enabled;
public final boolean mutable;
@NonNull public final String policy;
@Nullable public final ParsedOverlayInfo parsedInfo;
@Nullable public final ParsedConfigFile parsedConfigFile;
ParsedConfiguration(@NonNull String packageName, boolean enabled, boolean mutable,
@NonNull String policy, @Nullable ParsedOverlayInfo parsedInfo,
@Nullable ParsedConfigFile parsedConfigFile) {
this.packageName = packageName;
this.enabled = enabled;
this.mutable = mutable;
this.policy = policy;
this.parsedInfo = parsedInfo;
this.parsedConfigFile = parsedConfigFile;
}
}配置对象保存 package name、默认 enabled、mutable、partition policy 和来源信息。parsedInfo 可能来自 scanner;如果配置来自 system process 的 PackageManager provider,它可以关联已扫描 AndroidPackage 的 overlay 信息。parsedConfigFile 在 eng/userdebug 还会保存当前 XML 片段,便于诊断配置来源。
3.3 配置文件入口
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfigParser.java
static ArrayList<ParsedConfiguration> getConfigurations(
@NonNull OverlayPartition partition, @Nullable OverlayScanner scanner,
@Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
@NonNull List<String> activeApexes) {
if (scanner != null) {
if (partition.getOverlayFolder() != null) {
scanner.scanDir(partition.getOverlayFolder());
}
for (String apex : activeApexes) {
scanner.scanDir(new File("/apex/" + apex + "/overlay/"));
}
}
if (partition.getOverlayFolder() == null) {
return null;
}
final File configFile = new File(partition.getOverlayFolder(), "config/config.xml");
if (!configFile.exists()) {
return null;
}
final ParsingContext parsingContext = new ParsingContext(partition);
readConfigFile(configFile, scanner, packageManagerOverlayInfos, parsingContext);
return parsingContext.mOrderedConfigurations;
}当 scanner 非空时,配置解析前会扫描分区 overlay 目录和 active APEX 的 /apex/<name>/overlay/。没有 overlay 目录或没有 config/config.xml 时返回 null,表示回退到 manifest 的 static/priority 信息,而不是表示该分区没有 overlay。
3.4 merge 与路径安全
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfigParser.java
private static void parseMerge(@NonNull File configFile, @NonNull XmlPullParser parser,
@Nullable OverlayScanner scanner,
@Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
@NonNull ParsingContext parsingContext) {
final String path;
try {
SysPropWrapper sysPropWrapper = p -> SystemProperties.get(p, "");
path = expandProperty(parser.getAttributeValue(null, "path"), sysPropWrapper);
} catch (IllegalStateException e) {
throw new IllegalStateException("<merge> path expand error in " + configFile, e);
}
if (path == null || path.startsWith("/")) {
throw new IllegalStateException("Merged path must be relative in " + configFile);
}
if (parsingContext.mMergeDepth++ == MAXIMUM_MERGE_DEPTH) {
throw new IllegalStateException("Maximum <merge> depth exceeded in " + configFile);
}
final File configDirectory = new File(parsingContext.mPartition.getOverlayFolder(),
CONFIG_DIRECTORY).getCanonicalFile();
final File includedConfigFile = new File(configDirectory, path).getCanonicalFile();
if (!includedConfigFile.exists()
|| !FileUtils.contains(configDirectory, includedConfigFile)) {
throw new IllegalStateException("Merged file is invalid: " + includedConfigFile);
}
readConfigFile(includedConfigFile, scanner, packageManagerOverlayInfos, parsingContext);
parsingContext.mMergeDepth--;
}<merge> 只允许相对路径、最多递归到 MAXIMUM_MERGE_DEPTH,并用 canonical path + FileUtils.contains() 防止跳出分区的 config 目录。property 展开只允许读取只读 ro. 属性;因此 config.xml 的 merge 既有 XML 语法边界,也有路径和系统属性边界。
3.5 overlay 条目
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfigParser.java
private static void parseOverlay(@NonNull File configFile, @NonNull XmlPullParser parser,
@Nullable OverlayScanner scanner,
@Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
@NonNull ParsingContext parsingContext) {
final String packageName = parser.getAttributeValue(null, "package");
if (packageName == null) {
throw new IllegalStateException("<overlay> without package in " + configFile);
}
ParsedOverlayInfo info = null;
if (scanner != null) {
info = scanner.getParsedInfo(packageName);
if (info == null
&& scanner.isExcludedOverlayPackage(packageName, parsingContext.mPartition)) {
Log.d(TAG, "overlay " + packageName + " is ignored.");
return;
} else if (info == null || !parsingContext.mPartition.containsOverlay(info.path)) {
throw new IllegalStateException("overlay " + packageName
+ " not present in partition " + parsingContext.mPartition.getOverlayFolder());
}
} else if (packageManagerOverlayInfos.get(packageName) == null) {
Log.d(TAG, "overlay " + packageName + " is ignored.");
return;
}
if (parsingContext.mConfiguredOverlays.contains(packageName)) {
throw new IllegalStateException("overlay " + packageName
+ " configured multiple times in a single partition");
}
boolean isEnabled = DEFAULT_ENABLED_STATE;
final String enabled = parser.getAttributeValue(null, "enabled");
if (enabled != null) {
isEnabled = !"false".equals(enabled);
}
boolean isMutable = DEFAULT_MUTABILITY;
final String mutable = parser.getAttributeValue(null, "mutable");
if (mutable != null) {
isMutable = !"false".equals(mutable);
if (!isMutable && parsingContext.mFoundMutableOverlay) {
throw new IllegalStateException("immutable overlays must precede mutable overlays");
}
}
if (isMutable) {
parsingContext.mFoundMutableOverlay = true;
}
parsingContext.mConfiguredOverlays.add(packageName);
parsingContext.mOrderedConfigurations.add(new ParsedConfiguration(
packageName, isEnabled, isMutable, parsingContext.mPartition.policy,
info, new ParsedConfigFile(configFile.getPath(), parser.getLineNumber(), null)));
}配置条目有几个容易忽略的约束:
- scanner 模式下,配置项必须能在该分区 scanner 结果中找到,且
info.path必须位于该分区 overlay 目录;property 排除项例外地被忽略。 - 同一个分区内不能重复配置同一 package。
- immutable overlay 必须排在 mutable overlay 之前;未列出的 overlay 默认 mutable 且 disabled。
- 配置顺序本身就是优先级顺序,后面的 overlay 对同一 target 资源拥有更高配置优先级。
4. PMS 启动
4.1 system instance
源码文件:
frameworks/base/services/core/java/com/android/server/pm/InitAppsHelper.javaframeworks/base/core/java/com/android/internal/content/om/OverlayConfig.java
public OverlayConfig initSystemApps(PackageParser2 packageParser,
WatchedArrayMap<String, PackageSetting> packageSettings,
int[] userIds, long startTime) {
final List<ApexManager.ScanResult> apexScanResults = scanApexPackagesTraced(packageParser);
mApexManager.notifyScanResult(apexScanResults);
scanSystemDirs(packageParser, mExecutorService);
final ArrayMap<String, File> apkInApexPreInstalledPaths = new ArrayMap<>();
for (ApexManager.ActiveApexInfo apexInfo : mApexManager.getActiveApexInfos()) {
final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName(
apexInfo.apexModuleName);
for (String packageName : mApexManager.getApksInApex(apexPackageName)) {
apkInApexPreInstalledPaths.put(packageName, apexInfo.preInstalledApexPath);
}
}
final OverlayConfig overlayConfig = OverlayConfig.initializeSystemInstance(
consumer -> mPm.forEachPackageState(mPm.snapshotComputer(), packageState -> {
var pkg = packageState.getPkg();
if (pkg != null) {
consumer.accept(pkg, packageState.isSystem(),
apkInApexPreInstalledPaths.get(pkg.getPackageName()));
}
}));
return overlayConfig;
}OverlayConfig 的 system instance 在 system APK 扫描完成后初始化。PackageManager 通过 forEachPackageState() 提供已注册包;APEX 内 APK 额外提供 pre-installed APEX path,使 OverlayConfig 能把当前 APEX 包归属到原始分区。这里的顺序保证 OverlayManager 看到的 package truth 已经来自 PMS 扫描,而不是只来自 overlay 目录轻解析。
4.2 PackageProvider
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfig.java
public interface PackageProvider {
/** Performs the given action for each package. */
void forEachPackage(TriConsumer<Package, Boolean, File> p);
interface Package {
String getBaseApkPath();
int getOverlayPriority();
String getOverlayTarget();
String getPackageName();
int getTargetSdkVersion();
boolean isOverlayIsStatic();
}
}
public static OverlayConfig initializeSystemInstance(PackageProvider packageProvider) {
Trace.traceBegin(Trace.TRACE_TAG_RRO, "OverlayConfig#initializeSystemInstance");
try {
sInstance = new OverlayConfig(null, null, packageProvider);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RRO);
}
return sInstance;
}PackageProvider 暴露的是 overlay 所需的最小包视图:base APK path、priority、target、package name、target SDK、static 属性。它不把完整 PackageSetting 直接交给 OverlayConfig;OverlayConfig 负责配置排序和查询,OverlayManagerServiceImpl 之后再从 package state 取得完整 AndroidPackage。
4.3 配置汇总
源码文件:frameworks/base/core/java/com/android/internal/content/om/OverlayConfig.java
for (int i = 0, n = partitions.size(); i < n; i++) {
final OverlayPartition partition = partitions.get(i);
final OverlayScanner scanner = (scannerFactory == null) ? null : scannerFactory.get();
final ArrayList<ParsedConfiguration> partitionOverlays =
OverlayConfigParser.getConfigurations(partition, scanner,
packageManagerOverlayInfos,
activeApexesPerPartition.getOrDefault(partition.type,
Collections.emptyList()));
if (partitionOverlays != null) {
overlays.addAll(partitionOverlays);
continue;
}
final ArrayList<ParsedOverlayInfo> partitionOverlayInfos =
scannerFactory != null
? new ArrayList<>(scanner.getAllParsedInfos())
: new ArrayList<>(packageManagerOverlayInfos.values());
final ArrayList<ParsedConfiguration> partitionConfigs = new ArrayList<>();
for (int j = 0, m = partitionOverlayInfos.size(); j < m; j++) {
final ParsedOverlayInfo p = partitionOverlayInfos.get(j);
if (p.isStatic) {
partitionConfigs.add(new ParsedConfiguration(p.packageName,
true /* enabled */, false /* mutable */, partition.policy, p, null));
}
}
partitionConfigs.sort(sStaticOverlayComparator);
overlays.addAll(partitionConfigs);
}
for (int i = 0, n = overlays.size(); i < n; i++) {
final ParsedConfiguration config = overlays.get(i);
// Later partition definitions replace earlier definitions with the same package name.
mConfigurations.put(config.packageName, new Configuration(config, i));
}有 config.xml 时,解析出的配置顺序直接加入总列表;没有 config.xml 时,只把静态 overlay 转成 immutable + enabled 配置,并按 target、manifest priority、path 排序。总表写入 map 时,后分区同名定义覆盖前分区定义;configIndex 保存最终配置位置并作为 priority 查询结果。
5. 运行时同步
5.1 用户初始化
源码文件:frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@NonNull
ArraySet<UserPackage> updateOverlaysForUser(final int newUserId) {
if (DEBUG) {
Slog.d(TAG, "updateOverlaysForUser newUserId=" + newUserId);
}
final ArraySet<UserPackage> updatedTargets = new ArraySet<>();
final ArrayMap<String, PackageState> userPackages = mPackageManager.initializeForUser(
newUserId);
CollectionUtils.addAll(updatedTargets, removeOverlaysForUser(
(info) -> !userPackages.containsKey(info.packageName), newUserId));
final ArraySet<String> overlaidByOthers = new ArraySet<>();
for (PackageState packageState : userPackages.values()) {
var pkg = packageState.getAndroidPackage();
final String overlayTarget = pkg == null ? null : pkg.getOverlayTarget();
if (!TextUtils.isEmpty(overlayTarget)) {
overlaidByOthers.add(overlayTarget);
}
}用户切换/首次初始化时,OverlayManager 先从 PackageManager 取得该用户可见包,删除 settings 中已经不存在的 overlay,再收集被其他 overlay 覆盖的 target。返回的 updatedTargets 是需要重新计算 overlay paths 的目标集合,不是 overlay 包列表。
5.2 OverlayInfo
源码文件:frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
private OverlayInfo initOverlayForPackage(@NonNull AndroidPackage pkg, final int userId,
Set<UserPackage> updatedTargets)
throws OverlayManagerSettings.BadKeyException {
final OverlayIdentifier overlay = new OverlayIdentifier(pkg.getPackageName());
final int priority = getPackageConfiguredPriority(pkg);
OverlayInfo currentInfo = mSettings.getNullableOverlayInfo(overlay, userId);
if (mustReinitializeOverlay(pkg, currentInfo)) {
if (currentInfo != null) {
updatedTargets.add(UserPackage.of(userId, currentInfo.targetPackageName));
}
currentInfo = mSettings.init(overlay, userId, pkg.getOverlayTarget(),
pkg.getOverlayTargetOverlayableName(), pkg.getSplits().get(0).getPath(),
isPackageConfiguredMutable(pkg), isPackageConfiguredEnabled(pkg),
getPackageConfiguredPriority(pkg), pkg.getOverlayCategory(), false);
} else if (priority != currentInfo.priority) {
// Changing priority does not reinitialize all settings.
mSettings.setPriority(overlay, userId, priority);
updatedTargets.add(UserPackage.of(userId, currentInfo.targetPackageName));
}
return currentInfo;
}OverlayManager 以 package name + user 构造 OverlayIdentifier,从 OverlayConfig 取得 priority/mutable/enabled,再与持久化的 OverlayInfo 比较。target、target overlayable、mutable 状态或 immutable enabled 状态改变时必须重新初始化;仅 priority 改变时只更新 priority 并要求 target 刷新。
5.3 重初始化条件
源码文件:frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
private boolean mustReinitializeOverlay(@NonNull final AndroidPackage theTruth,
@Nullable final OverlayInfo oldSettings) {
if (oldSettings == null) {
return true;
}
if (!Objects.equals(theTruth.getOverlayTarget(), oldSettings.targetPackageName)) {
return true;
}
if (!Objects.equals(theTruth.getOverlayTargetOverlayableName(),
oldSettings.targetOverlayableName)) {
return true;
}
if (oldSettings.isFabricated) {
return true;
}
boolean isMutable = isPackageConfiguredMutable(theTruth);
if (isMutable != oldSettings.isMutable) {
return true;
}
if (!isMutable && isPackageConfiguredEnabled(theTruth) != oldSettings.isEnabled()) {
return true;
}
return false;
}OverlayManagerServiceImpl 把 PackageManager 视为 truth:旧 settings 缺失、target/overlayable 改变、旧记录是 fabricated overlay、mutable 改变,或 immutable 的 enabled 默认值改变,都会丢弃旧 overlay settings 重新初始化。mutable overlay 后续用户手动切换过 enabled 后,配置文件默认值变化不会简单覆盖用户状态。
5.4 idmap 与刷新
源码文件:frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@NonNull
private Set<UserPackage> updatePackageOverlays(@NonNull AndroidPackage pkg,
final int userId, final int flags) throws OperationFailedException {
if (pkg.getOverlayTarget() == null) {
// This package does not have overlays declared in its manifest.
return Collections.emptySet();
}
Set<UserPackage> updatedTargets = new ArraySet<>();
try {
OverlayInfo currentInfo = initOverlayForPackage(pkg, userId, updatedTargets);
if (updateState(currentInfo, userId, flags, currentInfo.constraints)) {
updatedTargets = CollectionUtils.add(updatedTargets,
UserPackage.of(userId, currentInfo.targetPackageName));
}
} catch (OverlayManagerSettings.BadKeyException e) {
throw new OperationFailedException("failed to update settings", e);
}
return updatedTargets;
}这个方法只处理“该 package 自身是 overlay”的状态;目标包的 overlay paths 更新由 updateOverlaysForTarget() 另行完成。idmap 创建/校验失败会通过 OperationFailedException 返回给上层,不能把 OverlayInfo 已写入 settings 当作资源已经生效。
6. PMS 策略
6.1 target 与分区
前文已展示 InstallPackageHelper.assertOverlayIsValid() 的完整分支;在本文语境下需要抓住三个 owner:
ParsedPackage的overlayTarget/targetOverlayableName来自 Manifest;ScanPackageUtils.applyPolicy()把 system、privileged、vendor/product 等 scan flags 写入包对象;InstallPackageHelper在addForInitLI()前执行 overlay target、分区和签名策略检查。
因此 OverlayConfig 的 policy 只供 idmap2 使用,不能替代 PMS 对 overlay package 的 target/reference 签名检查;OverlayManager 的 per-user enabled 状态也不能绕过 PMS 的 package validity。
6.2 预装与非预装
Android 17 的 overlay validity 分支区分 system overlay 与非预装 overlay:
- system/vendor/odm 等预装 overlay 的 target SDK 低于要求时,当前版本可能记录 warning;
- 非预装 overlay target SDK 低于 Q 且未使用 platform certificate 时,会抛
INTERNAL_ERROR_OVERLAY_LOW_TARGET_SDK; - overlay target、reference package 和 overlay config signature 的检查仍由 PMS 完成,不能由 OverlayConfig 的 enabled/mutable 配置替代。
6.3 资源生效时机
system boot 中,PMS 先扫描 overlay APK,再完成 OverlayConfig.initializeSystemInstance();OverlayManagerService 在系统服务初始化/用户切换时把 package state 同步到 OverlayManagerSettings,随后 idmap 和 target overlay paths 才可能更新。一个 overlay 在 dumpsys package 中存在,只能证明 PMS 注册成功;要证明资源覆盖,需要同时观察 OverlayInfo 状态、idmap 和 target 的 overlay paths。
7. 测试与诊断
7.1 scanner 测试范围
源码文件:frameworks/base/core/tests/coretests/src/com/android/internal/content/om/OverlayScannerTest.java
测试应围绕 scanDir() 和 parseOverlayManifest() 的真实边界组织:
- 空目录、不存在目录、不可读目录和
listFiles()==null的跳过路径; - 普通 APK 没有 target package 时不进入 overlay map;
- required system property 匹配时返回
ParsedOverlayInfo,不匹配时进入 excluded list; - 同包名多次扫描时后一个 info 覆盖前一个 info;
preInstalledApexPath存在时getOriginalPartitionPath()返回原始分区路径。
这些测试证明轻量发现层,不证明 overlay 已由 PMS 注册或 idmap 已创建。
7.2 config 测试范围
源码文件:frameworks/base/core/tests/coretests/src/com/android/internal/content/om/OverlayConfigTest.java
配置测试应断言:
partition_order.xml缺失、重复 partition、数量不匹配或名称缺失时回退默认顺序;<merge>的绝对路径、越界 canonical path、不存在文件和最大深度超限会失败;- 同一 partition 重复
<overlay>、immutable 排在 mutable 之后会失败; - 配置项的 enabled/mutable/priority/policy 和配置来源行号被正确保存;
- 未配置 overlay 的默认状态是 disabled + mutable,静态 overlay 的 fallback 是 enabled + immutable。
7.3 运行时诊断
adb shell dumpsys overlay
adb shell dumpsys package <overlay-package>
adb shell logcat -s OverlayManagerService OverlayConfig PackageManager按以下顺序对照:
- overlay APK 的 code path 是否位于预期分区
overlay目录或 active APEX; targetPackageName、targetOverlayableName、static、priority、target SDK 是否来自当前 Manifest;OverlayConfig是否为该 package 提供 enabled/mutable/priority/policy;OverlayInfo的 user、state、isMutable、isEnabled、base code path 和 target 是否正确;- idmap 是否存在、是否缺失或创建失败;
- target package 的 overlay paths 是否刷新。
8. 源码路线
定位“overlay 扫描到了但没有生效”时,可沿以下顺序阅读:
PackagePartitions/ScanPartition:确认该分区是否真的有 overlay 目录。OverlayScanner.scanDir():确认 APK 被发现、递归路径和同名覆盖。OverlayScanner.parseOverlayManifest():确认 target package 和 required property。OverlayConfigParser.getConfigurations()/parseOverlay():确认 config.xml、merge、enabled/mutable、顺序和分区 policy。InitAppsHelper.initSystemApps():确认 PMS package scan、APEX path 和 systemOverlayConfig初始化顺序。InstallPackageHelper.assertOverlayIsValid():确认 target、签名、SDK 和预装/非预装分支。OverlayManagerServiceImpl.updateOverlaysForUser():确认用户包集合、旧 settings 删除和 OverlayInfo 初始化。IdmapManager与 target update 方法:确认资源映射和 overlay paths 是否实际生效。
一个实用练习是:给定一个 /product/overlay/foo.apk,Manifest target 为 android,config.xml 将它设为 immutable + enabled,但设备属性不匹配。请判断它会在哪个对象中被记录、是否进入 mConfigurations、PMS 是否仍扫描该 APK、OverlayManager 是否为 user 创建 OverlayInfo,以及最终是否会生成 idmap。这个练习能检验你是否把“发现、配置、PMS 注册、用户状态和资源生效”分开。
9. 设计收束
Android 17 的 overlay 包扫描由多个 owner 共同完成:
OverlayScanner递归发现 overlay APK,只做ApkLite级 manifest 提取、property 排除和原始 APEX 路径保存;OverlayConfigParser读取分区 config.xml,验证 merge 路径、分区归属、重复项和 immutable/mutable 顺序,并生成 policy/priority 配置;- PMS 仍通过普通 package scan 注册
AndroidPackage,并在assertOverlayIsValid()中执行 target、SDK、分区和签名策略; OverlayConfig.initializeSystemInstance()在 system package scan 后汇总 PackageManager 的 overlay 视图;OverlayManagerServiceImpl按用户把 package truth 同步到 OverlayInfo/settings,必要时重初始化、更新 priority、创建 idmap 并刷新 target;- 目录发现成功、配置存在、PMS 注册成功、OverlayInfo 启用和 idmap 生效是五个不同状态,诊断时必须逐层确认。
后续专题会进入静态共享库扫描,分析 ScanResult 中的 SDK/static/dynamic shared library 信息如何参与版本、签名和依赖注册。
