Split APK 解析
本文承接 Manifest 解析入口、四大组件解析、meta-data 解析 和 PackageCacher 缓存。本文只讨论包解析阶段如何把一个目录中的 base APK 和 split APK 组成一个 ParsingPackage,不展开安装会话如何下载 split,也不把运行时 ClassLoader 的实现误当成解析器工作。
Android 17 的 Split 解析是两阶段流程:第一阶段只读取每个 APK 的少量 Manifest 字段,构造不可变的 PackageLite;第二阶段创建资源加载器,解析 base Manifest,再逐个解析 split Manifest。PackageLite 的数组索引是后续所有状态的坐标系,依赖、路径、revision code、是否 feature split 和 class loader 名称都必须保持同一顺序。
1. 两阶段模型
1.1 主调用链
PackageLite 不是最终包对象,它只保存后续解析需要的轻量元数据;真正的组件、权限和 application 字段由 base/split Manifest 阶段填入同一个 ParsingPackage。
1.2 单 APK 与目录
源码文件:frameworks/base/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
public static ParseResult<PackageLite> parsePackageLite(ParseInput input,
File packageFile, int flags) {
if (packageFile.isDirectory()) {
return parseClusterPackageLite(input, packageFile, flags);
} else {
return parseMonolithicPackageLite(input, packageFile, flags);
}
}单 APK 直接包装成没有 split 数组的 PackageLite;目录才需要枚举、校验和排序。这里的目录判断发生在轻量解析入口,PackageParser2 的完整解析阶段不会自己重新猜测包组织形式。
2. Split 坐标
2.1 字段
源码文件:frameworks/base/core/java/android/content/pm/parsing/PackageLite.java
private final @NonNull String mPackageName;
private final @NonNull String mPath;
private final @NonNull String mBaseApkPath;
private final @Nullable String[] mSplitApkPaths;
private final @Nullable String[] mSplitNames;
private final @Nullable String[] mUsesSplitNames;
private final @Nullable String[] mConfigForSplit;
private final @Nullable Set<String> mBaseRequiredSplitTypes;
private final @Nullable Set<String>[] mRequiredSplitTypes;
private final @Nullable Set<String>[] mSplitTypes;
private final int mBaseRevisionCode;
private final @Nullable int[] mSplitRevisionCodes;
private final @Nullable boolean[] mIsFeatureSplits;
private final boolean mIsolatedSplits;
private final boolean mSplitRequired;mSplitNames[i]、mSplitApkPaths[i]、mUsesSplitNames[i]、mConfigForSplit[i]、mSplitRevisionCodes[i] 和对应布尔/集合字段共同描述第 i 个 split。它们不是独立列表,任何排序或过滤都必须对所有数组同步进行。
2.2 Base 属性
public PackageLite(String path, String baseApkPath, ApkLite baseApk,
String[] splitNames, boolean[] isFeatureSplits, String[] usesSplitNames,
String[] configForSplit, String[] splitApkPaths, int[] splitRevisionCodes,
int targetSdk, Set<String>[] requiredSplitTypes, Set<String>[] splitTypes) {
mPath = path;
mBaseApkPath = baseApkPath;
mPackageName = baseApk.getPackageName();
mVersionCode = baseApk.getVersionCode();
mVersionCodeMajor = baseApk.getVersionCodeMajor();
mBaseRevisionCode = baseApk.getRevisionCode();
mIsolatedSplits = baseApk.isIsolatedSplits();
mBaseRequiredSplitTypes = baseApk.getRequiredSplitTypes();
mSplitRequired = (baseApk.isSplitRequired() || hasAnyRequiredSplitTypes());
mSplitNames = splitNames;
mSplitTypes = splitTypes;
mIsFeatureSplits = isFeatureSplits;
mUsesSplitNames = usesSplitNames;
mConfigForSplit = configForSplit;
mSplitApkPaths = splitApkPaths;
mSplitRevisionCodes = splitRevisionCodes;
mTargetSdk = targetSdk;
}包名、版本、签名、target SDK、isolatedSplits 和 base 的 required split types 都从 base ApkLite 继承。split 自身不会成为另一个 package;它是同一 package 的补充输入。
2.3 统一路径
public List<String> getAllApkPaths() {
final ArrayList<String> paths = new ArrayList<>();
paths.add(mBaseApkPath);
if (!ArrayUtils.isEmpty(mSplitApkPaths)) {
Collections.addAll(paths, mSplitApkPaths);
}
return paths;
}资源加载、native library 扫描等消费者可以通过 getAllApkPaths() 按 base-first 顺序获得完整 APK 集合。base 永远位于索引 0,split 数组从索引 0 开始但在依赖加载器内部会整体右移一位。
3. 轻量解析
3.1 枚举与一致性
源码文件:frameworks/base/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
public static ParseResult<PackageLite> parseClusterPackageLite(ParseInput input,
File packageDir, int flags) {
final File[] files = packageDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_NOT_APK,
"No packages found in split");
}
if (files.length == 1 && files[0].isDirectory()) {
return parseClusterPackageLite(input, files[0], flags);
}
String packageName = null;
int versionCode = 0;
final ArrayMap<String, ApkLite> apks = new ArrayMap<>();
for (File file : files) {
if (!isApkFile(file)) {
continue;
}
final ParseResult<ApkLite> result = parseApkLite(input, file, flags);
if (result.isError()) {
return input.error(result);
}
final ApkLite lite = result.getResult();
if (packageName == null) {
packageName = lite.getPackageName();
versionCode = lite.getVersionCode();
} else if (!packageName.equals(lite.getPackageName())
|| versionCode != lite.getVersionCode()) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Inconsistent package or version in " + file);
}
ApkLite previous = apks.put(lite.getSplitName(), lite);
if (previous != null) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Split name " + lite.getSplitName() + " defined more than once");
}
}
final ApkLite baseApk = apks.remove(null);
return composePackageLiteFromApks(input, packageDir, baseApk, apks);
}轻量阶段先过滤非 APK 文件,再要求所有 APK 的 package name 和 version code 一致,并以 split name 为 key 拒绝重复定义。null split name 代表 base;最后必须存在 base,否则组合阶段失败。
3.2 排序与数组
final int size = ArrayUtils.size(splitApks);
String[] splitNames = null;
String[] splitCodePaths = null;
int[] splitRevisionCodes = null;
if (size > 0) {
splitNames = new String[size];
splitCodePaths = new String[size];
splitRevisionCodes = new int[size];
splitNames = splitApks.keySet().toArray(splitNames);
Arrays.sort(splitNames, sSplitNameComparator);
for (int i = 0; i < size; i++) {
final ApkLite apk = splitApks.get(splitNames[i]);
splitCodePaths[i] = apk.getPath();
splitRevisionCodes[i] = apk.getRevisionCode();
}
}排序依据是 split name,而不是目录枚举顺序。所有属性数组都在同一 i 下写入,后续 parseSplitApk(..., splitIndex) 才能用一个整数定位正确 APK 和依赖信息。
3.3 Split 名称
String splitName = parser.getAttributeValue(null, "split");
if (splitName != null) {
if (splitName.length() == 0) {
splitName = null;
} else {
final ParseResult<?> nameResult = FrameworkParsingPackageUtils.validateName(
input, splitName, false, false);
if (nameResult.isError()) {
return input.error(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
"Invalid manifest split: " + nameResult.getErrorMessage());
}
}
}
return input.success(Pair.create(packageName.intern(),
splitName != null ? splitName.intern() : splitName));空字符串 split 被当作 base;非空 split name 必须通过名称校验。这个规则发生在每个 ApkLite 的 Manifest 轻量解析中,因此组合阶段拿到的 key 已经是规范化结果。
4. 资源加载器
4.1 普通模式
源码文件:frameworks/base/core/java/com/android/internal/pm/split/DefaultSplitAssetLoader.java
public AssetManager getBaseAssetManager() throws IllegalArgumentException {
if (mCachedAssetManager != null) {
return mCachedAssetManager;
}
ApkAssets[] apkAssets = new ApkAssets[(mSplitApkPaths != null
? mSplitApkPaths.length : 0) + 1];
int splitIdx = 0;
apkAssets[splitIdx++] = mBaseApkAssets = loadApkAssets(mBaseApkPath, mFlags);
if (!ArrayUtils.isEmpty(mSplitApkPaths)) {
for (String apkPath : mSplitApkPaths) {
apkAssets[splitIdx++] = loadApkAssets(apkPath, mFlags);
}
}
AssetManager assets = new AssetManager();
assets.setConfiguration(/* ... */, Build.VERSION.RESOURCES_SDK_INT_FULL);
assets.setApkAssets(apkAssets, false /*invalidateCaches*/);
mCachedAssetManager = assets;
return mCachedAssetManager;
}
public AssetManager getSplitAssetManager(int splitIdx) throws IllegalArgumentException {
return getBaseAssetManager();
}非 isolated 模式把 base 和全部 split 放进同一个 AssetManager;解析任意 split Manifest 时返回同一个资源管理器。getSplitAssetManager 的参数因此不会改变资源可见范围。
4.2 isolated 模式
源码文件:frameworks/base/core/java/com/android/internal/pm/split/SplitAssetDependencyLoader.java
public SplitAssetDependencyLoader(PackageLite pkg,
SparseArray<int[]> dependencies, @ParseFlags int flags) {
super(dependencies);
// Base is inserted at index 0; split indices are shifted by one.
mSplitPaths = new String[pkg.getSplitApkPaths().length + 1];
mSplitPaths[0] = pkg.getBaseApkPath();
System.arraycopy(pkg.getSplitApkPaths(), 0, mSplitPaths, 1,
pkg.getSplitApkPaths().length);
mFlags = flags;
mCachedSplitApks = new ApkAssets[mSplitPaths.length][];
mCachedAssetManagers = new AssetManager[mSplitPaths.length];
}
protected void constructSplit(int splitIdx, @NonNull int[] configSplitIndices,
int parentSplitIdx) throws IllegalArgumentException {
final ArrayList<ApkAssets> assets = new ArrayList<>();
if (parentSplitIdx >= 0) {
Collections.addAll(assets, mCachedSplitApks[parentSplitIdx]);
}
assets.add(loadApkAssets(mSplitPaths[splitIdx], mFlags));
for (int configSplitIdx : configSplitIndices) {
assets.add(loadApkAssets(mSplitPaths[configSplitIdx], mFlags));
}
mCachedSplitApks[splitIdx] = assets.toArray(new ApkAssets[assets.size()]);
mCachedAssetManagers[splitIdx] = createAssetManagerWithAssets(
mCachedSplitApks[splitIdx]);
}isolated 模式把父 split、当前 feature split 和它的 config split 组合成独立 AssetManager,并缓存构造结果。依赖数组由 SplitDependencyLoader 驱动;如果依赖图非法,ParsingPackageUtils 在创建 loader 时直接返回 bad manifest。
5. 完整解析
5.1 Loader 与 Base
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
final PackageLite lite = liteResult.getResult();
SparseArray<int[]> splitDependencies = null;
final SplitAssetLoader assetLoader;
if (lite.isIsolatedSplits() && !ArrayUtils.isEmpty(lite.getSplitNames())) {
try {
splitDependencies = SplitAssetDependencyLoader
.createDependenciesFromPackage(lite);
assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags);
} catch (SplitAssetDependencyLoader.IllegalDependencyException e) {
return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST, e.getMessage());
}
} else {
assetLoader = new DefaultSplitAssetLoader(lite, flags);
}
final File baseApk = new File(lite.getBaseApkPath());
final ParseResult<ParsingPackage> result = parseBaseApk(input, baseApk,
lite.getPath(), assetLoader, flags, shouldSkipComponents);
if (result.isError()) {
return input.error(result);
}loader 选择必须发生在 base Manifest 完整解析前,因为 base 的资源引用和 split 的 Manifest 都依赖 AssetManager。isolatedSplits=true 且确实存在 split 时才建立依赖树;没有 split 时仍使用普通 loader。
5.2 写入并解析
ParsingPackage pkg = result.getResult();
if (!ArrayUtils.isEmpty(lite.getSplitNames())) {
pkg.asSplit(lite.getSplitNames(), lite.getSplitApkPaths(),
lite.getSplitRevisionCodes(), splitDependencies);
final int num = lite.getSplitNames().length;
for (int i = 0; i < num; i++) {
final AssetManager splitAssets = assetLoader.getSplitAssetManager(i);
final ParseResult<ParsingPackage> split =
parseSplitApk(input, pkg, i, splitAssets, flags);
if (split.isError()) {
return input.error(split);
}
}
}
pkg.set32BitAbiPreferred(lite.isUse32bitAbi());
return input.success(pkg);asSplit() 先把 split 名称、路径、revision code 和依赖树写入 package,并初始化每个 split 的 flags/class-loader 数组。随后循环索引与 PackageLite 相同的 split;任何一个 split 失败都会使整个 package 解析失败,不会返回“只有 base 的半成品”。
5.3 Split Manifest
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
private ParseResult<ParsingPackage> parseSplitApk(ParseInput input,
ParsingPackage pkg, int splitIndex, AssetManager assets, int flags) {
final String apkPath = pkg.getSplitCodePaths()[splitIndex];
final int cookie = assets.findCookieForPath(apkPath);
if (cookie == 0) {
return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Failed adding asset path: " + apkPath);
}
try (XmlResourceParser parser = assets.openXmlResourceParser(cookie,
ANDROID_MANIFEST_FILENAME, false)) {
Resources res = new Resources(assets, mDisplayMetrics, null);
ParseResult<ParsingPackage> parseResult = parseSplitApk(
input, pkg, res, parser, flags, splitIndex);
if (parseResult.isError()) {
return input.error(parseResult.getErrorCode(),
apkPath + " (at " + parser.getPositionDescription() + "): "
+ parseResult.getErrorMessage());
}
return parseResult;
} catch (Exception e) {
return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to read manifest from " + apkPath, e);
}
}split 解析器先通过 AssetManager cookie 找到已经加载的 APK,再打开其 Manifest。错误信息会追加 split 的路径和 XML 位置,便于区分 base 失败还是某一个 split 失败。
6. Split 应用
6.1 顶层 Application
boolean foundApp = false;
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) {
continue;
}
final String tagName = parser.getName();
if (TAG_APPLICATION.equals(tagName)) {
if (foundApp) {
if (RIGID_PARSER) {
result = input.error("<manifest> has more than one <application>");
} else {
Slog.w(TAG, "<manifest> has more than one <application>");
result = input.success(null);
}
} else {
foundApp = true;
result = parseSplitApplication(input, pkg, res, parser, flags, splitIndex);
}
} else {
result = ParsingUtils.unknownTag("<manifest>", pkg, parser, input);
}
if (result.isError()) {
return input.error(result);
}
}split Manifest 的顶层只接受一个 <application>;其它顶层标签进入 unknownTag。没有 application 时通过 deferred error 处理,保持与完整 Manifest 解析的兼容错误模型。
6.2 hasCode 与类加载
TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestApplication);
try {
pkg.setSplitHasCode(splitIndex, sa.getBoolean(
R.styleable.AndroidManifestApplication_hasCode, true));
final String classLoaderName = sa.getString(
R.styleable.AndroidManifestApplication_classLoader);
if (classLoaderName == null || ClassLoaderFactory.isValidClassLoaderName(classLoaderName)) {
pkg.setSplitClassLoaderName(splitIndex, classLoaderName);
} else {
return input.error("Invalid class loader name: " + classLoaderName);
}
} finally {
sa.recycle();
}hasCode 和 classLoader 写入 ParsingPackage 的 split 数组,而不是覆盖 package 级字段。class loader 名称非法会立即失败;合法的 null 表示未显式指定。
6.3 组件默认 split
// If a component did not specify a split, inherit the split it is defined in.
String defaultSplitName = pkg.getSplitNames()[splitIndex];
...
if (isMainComponentTag(tagName)) {
result = parseMainComponent(input, pkg, res, parser, tagName,
defaultSplitName, flags);
} else {
result = parseSplitBaseAppChildTags(input, tagName, pkg, res, parser);
}split 中声明的 Activity、Service、Provider 等主组件,如果没有显式 split 属性,会继承当前 split name。这个默认值用于后续启动组件时选择正确的代码/资源 split;base 解析则传入 null。
7. 依赖与输出
7.1 PackageImpl 状态
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/pkg/PackageImpl.java
private String[] splitClassLoaderNames;
private SparseArray<int[]> splitDependencies;
private int[] splitFlags;
private String[] splitNames;
private int[] splitRevisionCodes;
public PackageImpl asSplit(String[] splitNames, String[] splitCodePaths,
int[] splitRevisionCodes, SparseArray<int[]> splitDependencies) {
this.splitNames = splitNames;
this.splitCodePaths = splitCodePaths;
this.splitRevisionCodes = splitRevisionCodes;
this.splitDependencies = splitDependencies;
int count = splitNames.length;
this.splitFlags = new int[count];
this.splitClassLoaderNames = new String[count];
return this;
}asSplit() 是从轻量对象到完整对象的状态切换点:数组从 PackageLite 转移到 PackageImpl,并为每个 split 创建待填充的 flags/class-loader 槽位。它不解析 Manifest,也不构造 AssetManager。
7.2 PackageInfo 路径
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
info.sourceDir = pkg.getBaseApkPath();
info.publicSourceDir = pkg.getBaseApkPath();
info.splitNames = pkg.getSplitNames();
info.splitSourceDirs = pkg.getSplitCodePaths().length == 0
? null : pkg.getSplitCodePaths();
info.splitPublicSourceDirs = pkg.getSplitCodePaths().length == 0
? null : pkg.getSplitCodePaths();
info.splitDependencies = pkg.getSplitDependencies().size() == 0
? null : pkg.getSplitDependencies();对外 ApplicationInfo/相关信息使用 base path、split names、split code paths 和依赖树。没有 split 时这些数组字段以 null 形式暴露;内部 getter 则可以返回空数组或空 SparseArray。不要把对外 null 当成内部从未初始化。
7.3 AppInfo 字段
appInfo.splitClassLoaderNames = splitClassLoaderNames;
appInfo.splitDependencies = (splitDependencies == null || splitDependencies.size() == 0)
? null : splitDependencies;
appInfo.splitNames = splitNames;
appInfo.setBaseCodePath(mBaseApkPath);
appInfo.setBaseResourcePath(mBaseApkPath);
appInfo.setCodePath(mPath);
appInfo.setResourcePath(mPath);
appInfo.setSplitCodePaths(ArrayUtils.size(splitCodePaths) == 0
? null : splitCodePaths);
appInfo.setSplitResourcePaths(ArrayUtils.size(splitCodePaths) == 0
? null : splitCodePaths);这些字段由 package 对象生成 ApplicationInfo 时一次性复制。解析阶段已经确定 split 的路径和依赖;对外生成阶段只负责按 flags/state 组合信息,不会再次读取 split Manifest。
8. 状态、失败与清理
8.1 状态机
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java、frameworks/base/core/java/com/android/internal/pm/split/DefaultSplitAssetLoader.java、frameworks/base/core/java/com/android/internal/pm/split/SplitAssetDependencyLoader.java
8.2 失败传播
| 阶段 | 典型条件 | 结果 |
|---|---|---|
| 轻量枚举 | 目录为空、缺 APK | INSTALL_PARSE_FAILED_NOT_APK |
| 一致性校验 | package/version 不一致 | bad manifest |
| split 唯一性 | 同名 split 两次出现 | bad manifest |
| 依赖树 | isolated split 依赖非法 | bad manifest,不创建完整包 |
| 资源加载 | APK 不是有效路径或无法打开 | IllegalArgumentException 转解析错误 |
| split Manifest | cookie 为 0、XML 错误、顶层标签非法 | 带 split 路径的位置错误 |
| class loader | 名称非法 | 当前 split 立即失败 |
任一 split 失败都会使整个 parseClusterPackage() 返回 error。源码没有把成功的 base 和前序 split 作为可安装结果返回。
8.3 资源释放
try {
// parse base and all splits
return input.success(pkg);
} catch (IllegalArgumentException e) {
return input.error(e.getCause() instanceof IOException
? INSTALL_FAILED_INVALID_APK : INSTALL_PARSE_FAILED_NOT_APK,
e.getMessage(), e);
} finally {
IoUtils.closeQuietly(assetLoader);
}无论成功还是失败,SplitAssetLoader 都在 finally 中关闭;isolated 模式还会逐个关闭缓存的 AssetManager。解析失败不会泄漏已经加载的 ApkAssets。
9. 测试与诊断
9.1 输入与断言
| 场景 | 断言 | 证明范围 |
|---|---|---|
| 单 base APK | split names/paths 为空或 null | monolithic 包形状 |
| base + 一个 split | mSplitNames[0] 与 path/revision 对齐 | 并行数组 |
| 目录中混入非 APK | 非 APK 被忽略 | 枚举过滤 |
| 两个 package name | 返回 bad manifest | 一致性校验 |
| 两个相同 split name | 返回 bad manifest | 唯一性校验 |
| 缺少 base | 组合失败 | base 必需 |
| isolated split 合法依赖 | split AssetManager 包含父/当前/config | 依赖加载 |
| isolated split 非法依赖 | 返回 bad manifest | 依赖错误 |
| split classLoader 非法 | 解析错误包含 class loader | split application |
| 任一 split XML 错误 | 整个 package error | 原子失败 |
PackageInfoUtils 输出 | split paths/names/dependencies 对齐 | 对外复制 |
测试不能只检查“目录解析成功”。至少还要断言索引对齐、依赖树、split flags/class loader 和失败时资源关闭,否则无法发现“数组错位”或“base 成功但 split 失败仍返回”的错误。
9.2 现场排查顺序
- 先确认输入是单 APK 还是 cluster 目录,并列出目录中的
.apk文件。 - 对每个 APK 检查 Manifest package、version 和 split name。
- 查看
PackageLite的 split names 与 paths 是否按 split name 排序。 - 判断
isolatedSplits,再检查依赖树是否创建成功。 - 检查 base 是否先完成解析,之后失败的是哪个
splitIndex。 - 对 split 组件确认
defaultSplitName、hasCode和 class loader 是否写入对应索引。 - 最后检查
ApplicationInfo的 split paths/dependencies 是否只是 flags 过滤,而不是解析阶段丢失。
10. 源码路线
建议按以下顺序阅读:
ApkLiteParseUtils.parsePackageLite():单 APK 与 cluster 分流。parseClusterPackageLite():目录枚举、一致性和 split 唯一性。composePackageLiteFromApks():排序和并行数组构造。PackageLite:确认 base 继承字段和 split 坐标系。ParsingPackageUtils.parseClusterPackage():loader 选择、base/split 主循环。DefaultSplitAssetLoader:普通模式的单 AssetManager。SplitAssetDependencyLoader:isolated 模式的依赖 AssetManager。parseSplitApk()/parseSplitApplication():split Manifest 的限制和默认 split。PackageImpl.asSplit()与PackageInfoUtils:内部状态转移和对外路径生成。
11. 设计收束
Split APK 解析的核心不是“把多个 APK 拼起来”,而是维护一套稳定坐标:
目录 APK
-> ApkLite(每个文件的轻量 Manifest)
-> PackageLite(按 splitName 排序的并行数组)
-> AssetLoader(普通合并或 isolated 依赖树)
-> base ParsingPackage
-> asSplit(路径、revision、依赖、待填充 flags/classLoader)
-> 逐 split Manifest
-> ParsedPackage / ApplicationInfobase 决定 package 身份和全局属性,split 提供按索引附加的代码、资源和组件。轻量阶段负责“能否组成一个合法 cluster”,完整阶段负责“每个 split 的资源和 Manifest 是否能加入同一个 package”,对外生成阶段才把这些数组暴露成 paths、names 和 dependencies。沿着这三个边界读源码,才能准确解释 split 缺失、依赖错误、资源不可见和组件启动时选错代码路径等问题。
