Skip to content

overlay 包扫描

追踪 Android 17 的 RRO 发现、config.xml、分区 policy、OverlayInfo、idmap 和用户态刷新边界。

基于android-17.0.0_r1
AndroidPackageManagerService包扫描RROOverlayConfigOverlayManagerService源码阅读

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.java
  • frameworks/base/core/java/com/android/internal/content/om/OverlayConfig.java
  • frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
阶段owner主要数据对外效果
目录发现OverlayScannerParsedOverlayInfo发现 overlay manifest 信息
配置解析OverlayConfigParserParsedConfigurationenabled、mutable、policy、配置顺序
PMS 包扫描InstallPackageHelperAndroidPackage、PackageSetting包进入 PMS 包表和查询结构
用户态同步OverlayManagerServiceImplOverlayInfo、OverlayManagerSettings每个 user 的 overlay 状态
资源生效IdmapManager/AssetManageridmap、overlay pathstarget 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

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

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

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

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

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

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

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

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.java
  • frameworks/base/core/java/com/android/internal/content/om/OverlayConfig.java
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

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

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

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

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

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

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 运行时诊断 ​

text
adb shell dumpsys overlay
adb shell dumpsys package <overlay-package>
adb shell logcat -s OverlayManagerService OverlayConfig PackageManager

按以下顺序对照:

  1. overlay APK 的 code path 是否位于预期分区 overlay 目录或 active APEX;
  2. targetPackageName、targetOverlayableName、static、priority、target SDK 是否来自当前 Manifest;
  3. OverlayConfig 是否为该 package 提供 enabled/mutable/priority/policy;
  4. OverlayInfo 的 user、state、isMutable、isEnabled、base code path 和 target 是否正确;
  5. idmap 是否存在、是否缺失或创建失败;
  6. target package 的 overlay paths 是否刷新。

8. 源码路线 ​

定位“overlay 扫描到了但没有生效”时,可沿以下顺序阅读:

  1. PackagePartitions/ScanPartition:确认该分区是否真的有 overlay 目录。
  2. OverlayScanner.scanDir():确认 APK 被发现、递归路径和同名覆盖。
  3. OverlayScanner.parseOverlayManifest():确认 target package 和 required property。
  4. OverlayConfigParser.getConfigurations()/parseOverlay():确认 config.xml、merge、enabled/mutable、顺序和分区 policy。
  5. InitAppsHelper.initSystemApps():确认 PMS package scan、APEX path 和 system OverlayConfig 初始化顺序。
  6. InstallPackageHelper.assertOverlayIsValid():确认 target、签名、SDK 和预装/非预装分支。
  7. OverlayManagerServiceImpl.updateOverlaysForUser():确认用户包集合、旧 settings 删除和 OverlayInfo 初始化。
  8. 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 信息如何参与版本、签名和依赖注册。