Skip to content

解析错误处理

追踪 Android 17 包解析中的 ParseResult、延迟错误、异常桥接、错误码和资源清理路径。

基于android-17.0.0_r1
AndroidPackageManagerServicePackageParserParseResult源码阅读

解析错误处理 ​

本文是包解析单元的收束篇,承接 Manifest 解析入口、IntentFilter 解析、Split APK 解析 和 共享库依赖解析。前文分别讲了标签如何解析;本文只回答错误如何在这些函数之间传播、何时从“潜在错误”变成真正失败,以及外层 API 如何把返回值重新转换成异常。

Android 17 的现代解析路径使用 ParseInput/ParseResult,而不是让每个内部函数都抛出异常。ParseTypeImpl 同时实现这两个接口,并按线程复用;PackageParser2 在最外层把 error result 转成 PackageParserException。这不是两套互相竞争的解析器,而是“内部返回值、边界异常”的分层设计。

1. 错误状态图 ​

1.1 解析层级 ​

错误码、错误消息和原始异常一起向上移动;父函数不重新解释错误,只通过 input.error(result) 把它转换为自己的泛型类型。成功结果也可以被父函数直接返回,错误与成功都不需要 Java 异常栈展开。

1.2 三种“成功” ​

结果isSuccess()getResult()典型来源
有对象成功true非 null正常组件、包对象
兼容性空成功truenull空 action filter、宽松不支持类型
失败false不应解包缺属性、Manifest 错误、I/O

ParseResult.isSuccess() 不保证 getResult() 非 null。读 Android 17 源码时,必须同时检查状态和对象,否则会把兼容性跳过误判为有效解析对象。

2. 接口契约 ​

2.1 ParseResult ​

源码文件:frameworks/base/core/java/android/content/pm/parsing/result/ParseResult.java

java
public interface ParseResult<ResultType> {
    boolean isSuccess();
    boolean isError();
    ResultType getResult();
    int getErrorCode();
    @Nullable String getErrorMessage();
    @Nullable Exception getException();
}

ParseResult 是输出视图,包含四类信息:状态、成功对象、安装解析错误码、可读消息和底层异常。接口注释还明确指出 ParseInput 与 ParseResult 实际上是同一个线程局部对象,只是通过泛型转换出不同视图。

2.2 ParseInput ​

源码文件:frameworks/base/core/java/android/content/pm/parsing/result/ParseInput.java

java
public interface ParseInput {
    <ResultType> ParseResult<ResultType> success(ResultType result);
    ParseResult<?> deferError(@NonNull String parseError, long deferredError);
    ParseResult<?> enableDeferredError(String packageName, int targetSdkVersion);
    <ResultType> ParseResult<ResultType> skip(@NonNull String parseError);
    <ResultType> ParseResult<ResultType> error(int parseError);
    <ResultType> ParseResult<ResultType> error(@NonNull String parseError);
    <ResultType> ParseResult<ResultType> error(int parseError,
            @Nullable String errorMessage);
    <ResultType> ParseResult<ResultType> error(int parseError,
            @Nullable String errorMessage, @Nullable Exception exception);
    <ResultType> ParseResult<ResultType> error(ParseResult<?> result);
}

接口把错误构造集中到输入对象,解析函数只需返回 ParseResult。error(String) 默认使用 INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;skip() 则使用 INSTALL_PARSE_FAILED_SKIPPED,表示调用方可以忽略该包,而不是包格式一定损坏。

3. ParseTypeImpl ​

3.1 状态字段 ​

源码文件:frameworks/base/core/java/android/content/pm/parsing/result/ParseTypeImpl.java

java
public class ParseTypeImpl implements ParseInput, ParseResult<Object> {
    @NonNull
    private final Callback mCallback;
    private Object mResult;
    private int mErrorCode = PackageManager.INSTALL_SUCCEEDED;
    @Nullable
    private String mErrorMessage;
    @Nullable
    private Exception mException;
    @Nullable
    private ArrayMap<Long, String> mDeferredErrors = null;
    private String mPackageName;
    private int mTargetSdkVersion = -1;
}

一个 ParseTypeImpl 保存当前解析调用的全部状态。INSTALL_SUCCEEDED 是成功哨兵值;只要 error code 不是它,isError() 就为 true。deferred error 使用 ArrayMap<Long, String>,key 是 change ID,value 是第一次出现的错误消息。

3.2 reset 与线程复用 ​

java
public ParseInput reset() {
    mResult = null;
    mErrorCode = PackageManager.INSTALL_SUCCEEDED;
    mErrorMessage = null;
    mException = null;
    if (mDeferredErrors != null) {
        // Reuse allocated storage because one parser may process many APKs.
        mDeferredErrors.erase();
    }
    mTargetSdkVersion = -1;
    return this;
}

PackageParser2 通过 ThreadLocal<ParseTypeImpl> 复用对象。reset 会清掉上一次包的结果、错误和 target SDK,但保留已经分配的 map 容器,避免扫描大量 APK 时反复分配。复用前不 reset 会造成上一个包的错误状态污染下一个包。

3.3 成功与错误 ​

java
@Override
public <ResultType> ParseResult<ResultType> success(ResultType result) {
    if (mErrorCode != PackageManager.INSTALL_SUCCEEDED) {
        Slog.wtf(TAG, "Cannot set to success after set to error, was "
                + mErrorMessage, mException);
    }
    mResult = result;
    //noinspection unchecked
    return (ParseResult<ResultType>) this;
}

@Override
public <ResultType> ParseResult<ResultType> error(int errorCode,
        @Nullable String errorMessage, Exception exception) {
    mErrorCode = errorCode;
    mErrorMessage = errorMessage;
    mException = exception;
    //noinspection unchecked
    return (ParseResult<ResultType>) this;
}

错误状态一旦写入,再调用 success 会记录 wtf;这保证父函数不会把已经失败的子结果覆盖成成功。反过来,连续 success 是允许的,因为解析循环可能在多个 child tag 后反复构造成功视图。

4. DeferredError ​

4.1 ChangeId 定义 ​

源码文件:frameworks/base/core/java/android/content/pm/parsing/result/ParseInput.java

java
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
public static final long MISSING_APP_TAG = 150776642;

@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
public static final long EMPTY_INTENT_ACTION_CATEGORY = 151163173;

@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
public static final long RESOURCES_ARSC_COMPRESSED = 132742131;

@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
public static final long MISSING_EXPORTED_FLAG = 150232615;

这些 ID 对应“缺 application”“空 action/category”“压缩 resources.arsc”“缺少 exported”等兼容规则。@EnabledAfter 只是默认 gating 信息,最终是否启用由 ParseInput.Callback.isChangeEnabled() 决定。

4.2 版本未知时登记 ​

java
@Override
public ParseResult<?> deferError(@NonNull String parseError, long deferredError) {
    if (mTargetSdkVersion != -1) {
        if (mDeferredErrors != null && mDeferredErrors.containsKey(deferredError)) {
            return success(null);
        }
        if (mCallback.isChangeEnabled(deferredError, mPackageName, mTargetSdkVersion)) {
            return error(parseError);
        }
        if (mDeferredErrors == null) {
            mDeferredErrors = new ArrayMap<>();
        }
        mDeferredErrors.put(deferredError, null);
        return success(null);
    }

    if (mDeferredErrors == null) {
        mDeferredErrors = new ArrayMap<>();
    }
    // Keep only the first message for each change ID.
    mDeferredErrors.putIfAbsent(deferredError, parseError);
    return success(null);
}

target SDK 尚未确定时,错误只按 change ID 登记,不立即失败;同一个 change ID 只保留第一条消息。若 target SDK 已知,则当场询问 callback:change 开启就 error,关闭就 success(null)。

4.3 版本确定后触发 ​

java
@Override
public ParseResult<?> enableDeferredError(String packageName, int targetSdkVersion) {
    mPackageName = packageName;
    mTargetSdkVersion = targetSdkVersion;

    int size = CollectionUtils.size(mDeferredErrors);
    for (int index = size - 1; index >= 0; index--) {
        long changeId = mDeferredErrors.keyAt(index);
        String errorMessage = mDeferredErrors.valueAt(index);
        if (mCallback.isChangeEnabled(changeId, mPackageName, mTargetSdkVersion)) {
            return error(errorMessage);
        } else {
            // Keep the key to remember that this change was checked and disabled.
            mDeferredErrors.setValueAt(index, null);
        }
    }
    return success(null);
}

enableDeferredError() 在解析到 uses-sdk 后调用。只要一个 change ID 被启用,就返回第一个触发的 error;全部关闭才返回 success(null)。关闭后的 key 仍保留,后续再次遇到同一 deferred error 不会重复询问兼容服务。

4.4 真实调用点 ​

java
ParseResult<Integer> targetSdkVersionResult = parseTargetSdk(...);
if (targetSdkVersionResult.isError()) {
    return input.error(targetSdkVersionResult);
}
int targetSdkVersion = targetSdkVersionResult.getResult();
ParseResult<?> deferResult = input.enableDeferredError(
        pkg.getPackageName(), targetSdkVersion);
if (deferResult.isError()) {
    return input.error(deferResult);
}

target SDK 解析成功是 deferred error 生效的时机。此处必须先检查 parseTargetSdk,再 enable;如果 target SDK 本身解析失败,就不能用不完整的版本信息判断兼容规则。

5. 错误传播 ​

5.1 错误向上冒泡 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
ParseResult<PackageLite> liteResult =
        ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, liteParseFlags);
if (liteResult.isError()) {
    return input.error(liteResult);
}

ParseResult<ParsingPackage> result = parseBaseApk(
        input, baseApk, lite.getPath(), assetLoader, flags, shouldSkipComponents);
if (result.isError()) {
    return input.error(result);
}

父函数不直接读取子结果的内部字段再重建错误,而是调用 input.error(result)。这会保留原始 error code、message 和 exception,同时满足当前方法的泛型返回类型。

5.2 标签级例子 ​

java
ParseResult<Property> result = ParsingPackageUtils.parseMetaData(
        pkg, component, resources, parser, "<meta-data>", input);
if (result.isError()) {
    return input.error(result);
}
Property property = result.getResult();
if (property != null) {
    component.setMetaData(property.toBundle(component.getMetaData()));
}

meta-data 的调用方同时处理 error 和 success(null);后者在宽松模式下表示“不写入任何值”。IntentFilter、uses-library 和 split 解析也采用同一模式,因此每个 child tag 的调用方都必须明确处理 null 结果。

6. 错误码与边界 ​

6.1 常见错误码 ​

错误码典型来源语义
INSTALL_PARSE_FAILED_NOT_APKAPK/资源无法打开输入不是可解析 APK
INSTALL_PARSE_FAILED_BAD_MANIFESTsplit 不一致、依赖非法Manifest 结构或组合非法
INSTALL_PARSE_FAILED_MANIFEST_MALFORMED缺属性、未知类型Manifest 内容不符合规则
INSTALL_PARSE_FAILED_NO_CERTIFICATES签名收集失败没有满足要求的证书
INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSEDdeferred 资源表规则触发高版本目标禁止压缩资源表
INSTALL_PARSE_FAILED_SKIPPEDinput.skip()调用方应忽略该包

错误码是机器可读分类,错误消息才携带具体标签、路径和 XML 位置。排查时应同时保留两者。

6.2 具体规则与调用方 ​

规则登记位置最终触发点
无 applicationbase/split Manifest 尾部target SDK 已知后
空 action/categoryIntentFilter 子标签target SDK 已知后
缺 android:exportedActivity/Service 组件结束target SDK 已知后
压缩 resources.arscbase APK 资源加载后target SDK 已知后
缺少 name/MIME/loader当前解析函数立即返回 error

7. 异常桥接 ​

7.1 Parser2 边界 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParser2.java

java
ParseInput input = mSharedResult.get().reset();
ParseResult<ParsingPackage> result = mParsingUtils.parsePackage(input, packageFile, flags);
if (result.isError()) {
    throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
            result.getException());
}
ParsedPackage parsed = (ParsedPackage) result.getResult().hideAsParsed();

内部返回值在 PackageParser2.parsePackage() 这一边界重新变成 checked exception。调用方因此可以继续沿旧的 try/catch 结构工作,但错误码和原始异常没有丢失。

7.2 ParserException ​

源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/PackageParserException.java

java
public class PackageParserException extends Exception {
    public final int error;

    public PackageParserException(int error, String detailMessage) {
        super(detailMessage);
        this.error = error;
    }

    public PackageParserException(int error, String detailMessage, Throwable throwable) {
        super(detailMessage, throwable);
        this.error = error;
    }
}

这个类只有一个公开错误码字段和两个构造函数。它不负责分类、日志或恢复;这些职责属于产生错误的 parser 和捕获异常的安装/扫描调用方。

7.3 扫描与安装调用方 ​

java
try {
    return mPackageParser.parsePackage(scanFile, parseFlags, true);
} catch (PackageParserException e) {
    throw new PackageManagerException(e.error, e.getMessage(), e);
}

安装路径也采用相同的桥接方式,只是把异常转换成安装准备阶段的错误对象。

java
try {
    parsedPackage = pp.parsePackage(scanFile, parseFlags, false);
} catch (PackageParserException e) {
    throw new PackageManagerException(e.error, e.getMessage(), e);
}

ParallelPackageParser 和 InstallPackageHelper 都在边界捕获 PackageParserException,再转换为自己的错误类型。扫描与安装可以据此决定记录失败、返回安装码或停止当前任务,而不需要理解 ParseTypeImpl 的内部状态。

8. 失败、恢复与清理 ​

8.1 资源关闭 ​

java
try (XmlResourceParser parser = assets.openXmlResourceParser(
        cookie, ANDROID_MANIFEST_FILENAME, false)) {
    Resources res = new Resources(assets, mDisplayMetrics, null);
    return parseSplitApk(input, pkg, res, parser, flags, splitIndex);
} catch (Exception e) {
    return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
            "Failed to read manifest from " + apkPath, e);
}

Manifest parser 使用 try-with-resources;异常会被转换成带路径的 ParseResult。Split 资源加载器在更外层 finally 中关闭 AssetManager,确保失败的 split 不留下打开的资源句柄。

8.2 缓存与失败恢复 ​

PackageCacher 读取缓存失败时删除坏文件并返回 null,PackageParser2 随后走完整解析;写缓存失败则只记录日志,当前解析仍返回成功包。这说明解析错误和缓存错误的恢复策略不同:前者通常终止当前包,后者允许绕过加速层继续。

8.3 部分状态不回滚 ​

解析器会在发现错误时停止向上返回,但不会在每个 setXxx() 后建立事务回滚。原因是只有最终成功的 ParsedPackage 才会交给扫描/安装提交;失败对象不再作为有效包消费。已经注册到全局 resolver 的旧包,则由替换/卸载流程负责移除,而不是由 ParseInput 负责。

9. 测试与诊断 ​

9.1 输入与断言 ​

输入断言证明范围
缺少 Manifest 必需标签error code 和 message 正确立即失败
target SDK 低于 gating 版本deferred error 最终 success(null)兼容路径
target SDK 高于 gating 版本enableDeferredError 返回 error延迟触发
同一 deferred ID 多次出现只保留第一次消息去重逻辑
子函数 error父函数 error code/message 不变冒泡
success(null)调用方不解包并继续空成功
Parcel/XML/I/O 异常资源关闭,错误带原始 exception清理与因果链
PackageParser2 error转成 PackageParserException异常桥接
input.skip()错误码为 SKIPPED可忽略包

测试要覆盖“错误产生、传播、触发、桥接”四个位置。只测试最终安装码,无法知道错误是在标签解析、target SDK gating 还是外层转换时丢失的。

9.2 诊断顺序 ​

  1. 先记录 getErrorCode()、getErrorMessage() 和 getException(),不要只看日志首行。
  2. 回到产生 error 的标签函数,确认是立即 error 还是 deferred error。
  3. 若是 deferred error,检查 enableDeferredError() 调用时的 package name、target SDK 和 callback。
  4. 沿父函数检查每一层是否执行 input.error(result),以及是否错误地调用了 getResult()。
  5. 到 PackageParser2 查看是否转成 PackageParserException,再看扫描或安装调用方如何转换。
  6. 若涉及 split、APK assets 或 cache,确认 finally/try-with-resources 已执行,并区分“包失败”和“加速层回退”。

10. 源码路线 ​

建议按以下顺序阅读:

  1. ParseResult.java:理解成功、失败和空成功的契约。
  2. ParseInput.java:查看错误构造、skip 和 DeferredError ID。
  3. ParseTypeImpl.java:理解状态字段、reset、error 和 deferred map。
  4. ParsingPackageUtils.enableDeferredError 调用点:确认 target SDK 何时生效。
  5. 任意标签调用方(如 ParsedIntentInfoUtils、ParsedComponentUtils):观察 error 冒泡。
  6. PackageParser2.parsePackage():确认返回值到异常的边界。
  7. ParallelPackageParser、InstallPackageHelper:确认 PMS 如何消费错误码。
  8. split/cache 的 finally 和回退逻辑:理解失败后的资源清理与恢复。

11. 设计收束 ​

Android 17 的包解析错误处理可以压缩为一条链:

text
解析函数
  -> ParseInput 写入 success/error/deferred
  -> ParseTypeImpl 持有线程内状态
  -> 父函数逐层冒泡 ParseResult
  -> target SDK 确定后触发 DeferredError
  -> PackageParser2 转 PackageParserException
  -> 扫描/安装调用方转 PackageManagerException 或安装失败

这条链解释了几个关键现象:错误码和消息必须一起保存;success(null) 不是有效对象;deferred error 不是忽略错误,而是等待兼容性决策;缓存失败可以恢复而 Manifest 失败通常不能;资源关闭属于 parser/loader 的 finally,不属于错误对象本身。掌握这些边界,后续阅读安装事务和失败回滚时,才能准确判断一个失败究竟发生在解析、桥接、依赖校验还是提交阶段。