先看结论与判断条件

  • 盘点对象是某个 AAB 针对明确设备配置派生出的 APK 集,不是 AAB 文件或单个 base APK 的通用清单。
  • 每个集合至少记录 base、功能模块、语言、密度、ABI、包名、versionCode、证书摘要、文件摘要与派生参数,禁止只凭 split 文件名推断归属。
  • 语言完整性既要检查目标语言 split 是否被选择,也要验证默认资源和产品要求的关键文案是否能在安装后的真实资源解析中取得。
  • 密度、ABI 与功能模块会共同改变设备集合;只生成 universal APK 或只抽查一种配置,不能代表按设备交付的全部组合。
  • 本地 bundletool 适合复现 AAB 派生与安装选择,测试轨道和设备回执负责确认实际交付;两类证据不能互相冒充。
  • 资源表结构、安装会话约束和设备测试分别回答格式、集合身份与运行语义,任何单层通过都不能替代完整性结论。

完整性问题必须绑定 AAB、设备配置和安装时点

AAB 派生的 APK 不是固定的一组文件。设备的 ABI、屏幕密度、语言列表、SDK 条件和可选功能会影响最终选择,动态功能的安装状态还可能随使用过程变化。于是,资源 split 完整性不能写成某个目录里一共有多少 APK,而要写成一个三元组:哪个 AAB 候选、哪份设备规格、哪个交付或安装时点。缺少任一项,所谓缺失都可能只是比较了不同目标集合。

盘点要分别回答生成、选择、安装和运行四个问题。生成层确认 AAB 能派生哪些 base、功能与配置 APK;选择层确认给定设备规格应当取得哪些文件;安装层确认同一会话接收并成功提交了哪些 split;运行层确认应用能够从真实 Resources 解析目标语言、密度和功能资源。把 bundletool 生成成功当成运行完整,会遗漏会话、设备和资源回退问题。

调查前先冻结 AAB 摘要、bundletool 版本、签名参数、设备规格文件和 APK set 摘要。每个派生 APK 再记录自身摘要、模块、split 类型、限定符和安装身份。若材料来自商店,应另外保留测试轨道、版本、设备账号环境和实际取得的安装集合。没有真实交付回执时,可以形成本地派生结论,但必须明确它没有覆盖线上选择与下发。

资源 split 完整性的四层问题
层次核心问题主要证据不能替代
生成AAB 能派生哪些 APKAAB 摘要、APK set 与工具记录真实设备会取得全部文件
选择目标设备应选择哪些 split设备规格与选择清单安装会话成功
安装同一会话实际提交哪些文件PackageInstaller 或商店回执资源能够正确解析
运行目标资源是否可取得和回退设备端测试与可见结果其他设备组合也通过
发布证据是否绑定正确候选AAB、APK 与证书摘要业务安全和攻击阻断

先理解 AAB 模块,再建立 APK set 资产表

Android App Bundle format 将应用组织为 base、feature、配置和资产相关模块,设备最终安装的是由这些材料派生出的 APK 集。base 提供应用基础代码与资源,功能模块可能携带自己的代码和资源,配置 APK 可针对语言、屏幕密度或 ABI 拆分。AAB 文件本身不是设备直接安装的最终 APK,因此只验证 AAB ZIP 结构不能回答某台设备得到了哪些资源。

资产表应采用一行一个派生 APK,而不是一行一个 AAB。每行记录 module、splitKind、qualifiers、packageName、versionCode、证书 SHA-256、APK SHA-256、文件长度和来源 APK set。base 行必须唯一;功能模块需要说明是安装时交付还是后续按需取得;配置 split 需要明确它服务哪个模块与限定符。文件名只能作为显示字段,不能成为唯一主键。

同一资源限定符可能在 base 和功能模块各自出现,语言 split 也不一定只对应一个文件。盘点系统应使用模块与 split 类型组合定位,避免看到 zh 或 xxhdpi 字样就判定全局已覆盖。若 APK set 清单无法说明某个配置 APK 的目标模块,状态应为待解析;不能把未知文件自动归入 base,以免掩盖功能模块资源缺口。

APK set 资产表的必要对象
对象关键字段完整性判断常见遗漏
base APK包名、版本、证书、摘要集合中恰好一个只核对文件名
功能 APKmodule、delivery 与摘要目标功能按时点存在忽略按需状态
语言 splitmodule 与 locale目标语言和回退可解释只查 base 语言
密度 splitmodule 与 density匹配目标设备策略只抽查一种密度
ABI splitmodule 与 ABI本地库组合符合设备误用 universal 代表全部
资产模块pack、delivery 与版本按产品时点单独核验混入普通资源结论

设备规格是选择条件,不是测试结果

bundletool 可以从 AAB 生成、检查和部署 APK set,并根据设备规格选择相应 APK。设备规格通常包含受支持 ABI、屏幕密度、SDK 版本和语言等条件。它适合把同一候选映射为可重复的本地选择输入,但规格 JSON 只声明目标环境,不证明真实设备的系统状态、商店策略和安装结果与声明完全相同。规格文件必须与产生它的设备或测试用例绑定。

测试矩阵应从产品支持范围反推,而不是只抓取一台开发机。至少覆盖主要 ABI、密度分档、默认语言、已翻译语言、区域变体、语言列表顺序以及关键 SDK 边界。矩阵可以采用风险分层,但每个格子必须保留预期 split 集与实际集。若某个维度没有覆盖,应在报告中标成未测试,不能由相邻密度或相同语言代码自动推断。

universal APK 适合某些本地检查,却会把多种 ABI 或资源合并进单一文件,无法复现按设备拆分的选择边界。相反,仅针对一个设备规格生成的集合也不能说明其他组合完整。盘点应保留同一 AAB 下的多份 scope,每份 scope 只对应一个明确设备类;聚合报告只统计已验证 scope,不把文件并集当成任一真实设备应该安装的集合。

设备配置矩阵与需要确认的 split
维度示例范围预期核验禁止外推
ABIarm64-v8a、其他支持 ABI对应 Native 配置与模块一种 ABI 代表全部
密度mdpi 到 xxxhdpi 的产品范围密度 split 与资源解析相邻密度必然等价
语言默认、翻译语言与区域语言 split、默认回退、关键文案有文件即翻译完整
SDK最低、主流和最新目标安装选择与运行行为单 API 代表所有版本
功能安装时和按需模块指定时点的功能集合未请求功能必须存在
交付渠道本地、测试轨道与生产各自真实回执本地结果替代商店

语言 split 要同时核对选择、默认资源和产品必需文案

语言完整性不是看到一个 config.zh.apk 就结束。首先要确认该语言 split 属于哪个模块、对应哪个语言或区域限定符,并被目标设备语言列表选择。其次要检查 base 或功能模块中是否存在默认资源,因为缺少特定翻译时 Android 可能按资源规则回退。最后还要根据产品要求核对关键界面文案是否确实提供目标翻译,系统能够回退不代表商业内容可以接受混合语言。

语言列表有顺序,区域限定符也会影响匹配。zh-CN、zh-TW、pt-BR 或其他区域资源不能只按前两个字母粗略合并,除非产品明确接受该回退。盘点清单应保存设备 locale 列表、应用声明支持的语言、派生 split 限定符、安装后当前 locale 和资源解析结果。若只保存 zh、pt 等短标签,后续无法判断区域资源缺失还是预期回退。

功能模块可能包含独立字符串和图片资源,因此 base 的语言覆盖率不能代表整个应用。对安装时功能,应在首次启动路径中一并抽查;对按需功能,应在模块安装后再次记录已安装 split 并运行相同资源用例。若某个语言 split 未被选择但默认资源工作正常,报告应写成目标翻译未交付或未要求,而不是笼统写成资源完整通过。

语言完整性门禁的判定字段
检查项证据通过条件失败表达
设备语言列表完整 locale 顺序与测试 scope 一致设备条件未绑定
语言 splitmodule 与限定符目标文件被正确选择目标语言 split 缺失
默认资源无语言限定资源必需键存在且可解析默认回退缺口
区域资源语言加地区限定符符合产品区域策略区域回退未确认
关键文案资源键与设备端结果目标语言内容可见翻译交付不完整
功能资源模块安装前后集合指定时点资源可解析功能模块语言缺口

密度、ABI 与功能模块必须作为组合检查

屏幕密度 split 影响位图等配置资源的交付,但运行时资源选择还可能使用最接近的限定符或默认资源。盘点应比较 APK set 中的密度限定符、目标设备 density、模块归属和安装后资源解析,而不是要求每台设备安装所有密度文件。若某资源只有高密度版本,能否缩放使用与视觉是否可接受是两个问题;前者由运行时结果确认,后者需要产品验收。

ABI split 主要承载 Native 库,却会与功能模块和资源集合共同决定可安装包。base 使用 arm64-v8a 而某功能模块只出现其他 ABI 时,可能在模块安装或调用时暴露问题。清单需要按模块列出 lib 路径、ABI 和摘要,并核对目标设备可接受的 ABI 选择。本文不把 Native 二进制差分扩写为代码安全结论,只把它作为完整安装集合的一项输入。

组合爆炸不能通过忽略维度解决。可以先建立正交的静态盘点,确认每个模块声明了预期语言、密度和 ABI,再选择高风险组合做真实设备测试。高风险包括主要用户语言、主流密度、含 Native 的按需功能和新 SDK。任何压缩矩阵的策略都应公开记录覆盖边界;没有运行过的组合保持未验证,而不是用静态存在性自动标成通过。

组合完整性中的静态与运行证据
对象静态盘点运行验证结论边界
密度资源限定符、模块与条目设备加载与视觉抽查存在不等于视觉合格
Native ABIlib 路径、ABI 与摘要模块安装和调用存在不等于兼容通过
安装时功能功能 APK 与配置 split首次安装后资源测试base 通过不能替代
按需功能可派生文件与条件请求模块后的集合与资源未请求时缺失不一定错误
默认资源无限定条目回退路径可解析回退不等于翻译完成
组合 scope设备规格与预期集合真实设备回执单 scope 不代表矩阵

安装会话要证明同一身份下的完整集合

Android PackageInstaller 对 split 安装设置了集合约束,base、packageName、versionCode 与签名证书必须保持一致。完整性清单应对每个派生 APK 重复记录这些身份字段并检查一致,不能只在集合头部写一次。这样可以及时发现误混另一个版本、渠道或本地调试签名的 split。身份一致是安装前提,但不证明资源内容满足产品要求。

安装动作需要保存会话级回执。对本地测试,应记录进入同一 session 的文件摘要、提交结果、设备标识、系统版本和安装后实际 split 名称;对商店测试,应记录测试轨道、版本、设备配置、交付时间和实际安装状态。安装失败时要保留平台返回的具体原因,不能为了通过而改用 universal APK,因为那会改变正在验证的交付模式。

bundletool 未显式使用发布签名材料时,可能产生带调试签名的本地测试 APK。这样的集合可以用于资源选择与安装机制验证,但不能登记为发布候选,也不能与商店签名后的文件逐字节比较后声称异常。报告应把签名类别写入 evidenceClass,并明确哪些结论只关于模块和配置选择,哪些才绑定真实发布证书与线上交付。

安装集合的一致身份检查
字段集合要求不一致处置不能推出
base恰好一个且可识别拒绝集合资源翻译完整
packageName所有 split 一致拒绝混装同一源码
versionCode所有 split 一致重新选择同版本文件运行行为一致
证书摘要同一批准身份停线核对签名路径业务代码安全
APK 摘要每个文件唯一绑定拒绝未知替换商店必然下发
session 回执列出实际提交与结果保留失败原因其他设备也通过

本地派生、测试轨道和设备资源测试形成递进证据

Build and test Android App Bundles 说明可以使用 bundletool 和测试轨道验证 App Bundle 的派生与交付。本地 build-apks、针对设备规格的选择以及 install-apks 适合快速重现候选行为,并能在不等待商店处理时发现模块或配置缺口。不过本地生成由本地工具、参数和签名决定,不能完全代表 Play 服务端最终处理与设备实际取得的文件。

Android instrumented tests 适合验证依赖 Android 运行时、组件和系统 API 的语义。资源完整性测试应从当前安装应用读取 split 信息,再对关键资源键切换语言或配置进行解析,覆盖 base 与已安装功能模块。测试回执需要绑定 AAB、派生 APK 集、设备、系统、locale 与用例结果。单一设备通过只能证明该 scope,不能代表完整 API、ABI、密度和厂商矩阵。

上线门禁可以设置三档证据。本地派生档证明给定输入能生成预期集合;设备安装档证明该集合在目标环境完成安装并解析资源;测试轨道档证明当前商店链路对指定设备交付了可核对集合。每一档都记录未覆盖项,不用高档的一次抽查替代其他设备,也不用低档的批量静态检查冒充真实交付。

  • 本地 APK set 绑定 AAB、工具、参数和签名类别
  • 每份设备规格对应唯一预期 split 清单
  • 安装后实际 split 集合与会话回执已保存
  • 关键语言与默认资源通过设备端解析测试
  • 测试轨道结果与本地派生结果分开登记
  • 未覆盖 API、ABI、密度、语言和厂商范围明确列出

用只读清单校验器阻止缺失或混装 split 进入设备测试

下面的 Python 示例读取一份由受信导出流程生成的 APK set JSON 清单。清单包含集合身份、一个 base、若干 split 与目标设备期望值。脚本检查 base 唯一性、包名、versionCode、证书摘要、文件摘要、重复名称、目标语言、密度、ABI 和必需功能模块;任何输入缺失、身份不一致或期望集合不完整都会返回非零状态。它只读清单,不修改 APK,也不处理签名密钥。

示例不直接解析 toc.pb,也不替代 bundletool、aapt2、apksigner 或设备安装。生产流程应由可信工具从当前 AAB 和 APK set 导出 JSON,并把导出器版本、AAB 摘要和每个 APK 摘要一起保存。脚本中的 locale、density、ABI 和 feature 期望必须来自明确测试 scope,而不是为了让门禁通过而从现有文件反推。静态通过只表示清单内部满足这份 scope。

申请 AAB 或 APK 商业加固评估前,可准备 AAB、APK set 与各文件摘要、设备配置矩阵、语言和资源清单、功能交付方式、签名类别、安装会话回执和设备测试结果,再从御盾中央平台提交申请。真实候选与证据齐备前,不宣称商店交付完整、所有设备兼容、资源不可篡改或任何攻击路径已经被阻断。

  • JSON 由当前 AAB 和 APK set 的受信工具导出
  • 集合中恰好一个 base 且所有 split 身份一致
  • 每个 APK 摘要均为真实文件重新计算
  • 语言、密度、ABI 和功能期望来自目标设备 scope
  • 静态通过后仍执行安装会话和资源解析测试
  • 本地调试签名结果不登记为商店发布候选
核对目标设备的 base、语言、密度、ABI 与功能 split
from pathlib import Path
import json
import re
import sys

DIGEST = re.compile(r"^[0-9a-f]{64}$")
KINDS = {"base", "language", "density", "abi", "feature"}

def load_inventory(path_text):
    path = Path(path_text)
    if not path.is_file():
        raise SystemExit(2)
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        raise SystemExit(2)
    if not isinstance(value, dict):
        raise SystemExit(2)
    return value

def require_text(value):
    return isinstance(value, str) and bool(value.strip())

def valid_digest(value):
    return require_text(value) and DIGEST.fullmatch(value.lower()) is not None

def collect_qualifiers(splits, kind):
    return {item["qualifier"] for item in splits if item["kind"] == kind}

if len(sys.argv) != 2:
    raise SystemExit(2)
data = load_inventory(sys.argv[1])
identity = data.get("identity")
splits = data.get("splits")
expected = data.get("expectedForDevice")
if not isinstance(identity, dict) or not isinstance(splits, list) or not isinstance(expected, dict):
    raise SystemExit(2)
for field in ("packageName", "certificateSha256"):
    if not require_text(identity.get(field)):
        raise SystemExit(2)
if not isinstance(identity.get("versionCode"), int) or identity["versionCode"] <= 0:
    raise SystemExit(2)
if not valid_digest(identity["certificateSha256"]):
    raise SystemExit(2)
if not splits:
    raise SystemExit(3)
names = set()
for item in splits:
    required = ("name", "module", "kind", "qualifier", "apkSha256", "packageName", "versionCode", "certificateSha256")
    if not isinstance(item, dict) or any(field not in item for field in required):
        raise SystemExit(3)
    if item["name"] in names or item["kind"] not in KINDS:
        raise SystemExit(3)
    names.add(item["name"])
    if not valid_digest(item["apkSha256"]) or not valid_digest(item["certificateSha256"]):
        raise SystemExit(3)
    for field in ("packageName", "versionCode", "certificateSha256"):
        if item[field] != identity[field]:
            raise SystemExit(4)
base = [item for item in splits if item["kind"] == "base"]
if len(base) != 1 or base[0]["module"] != "base":
    raise SystemExit(4)
checks = {
    "language": set(expected.get("languages", [])),
    "density": {expected.get("density")},
    "abi": {expected.get("abi")},
    "feature": set(expected.get("requiredFeatures", [])),
}
for kind, wanted in checks.items():
    if not all(require_text(value) for value in wanted):
        raise SystemExit(5)
    if not wanted.issubset(collect_qualifiers(splits, kind)):
        raise SystemExit(5)
print(json.dumps({"status": "eligible-for-device-test", "splitCount": len(splits), "packageName": identity["packageName"]}, ensure_ascii=False))

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
AAB 由 base、feature、配置和资产相关模块组成,设备安装的是派生 APK 集。Android App Bundle format 描述 App Bundle 模块结构和派生 APK 交付模型。AAB 结构存在不证明某台设备实际取得了预期 split,也不代表 AAB 可直接安装。
bundletool 和测试轨道可以用于验证 App Bundle 的派生、选择与交付行为。Build and test Android App Bundles 说明本地工具和测试轨道的测试路径。本地生成结果不能完全替代 Play 线上处理和真实设备交付回执。
bundletool 能够生成、检查和部署按设备配置选择的 APK set。bundletool 文档描述 build-apks、设备规格、安装及相关命令能力。未显式提供发布签名时可能使用调试签名,本地文件不能因此冒充发布候选。
split 安装集合需要在 base、包名、versionCode 和签名证书等身份上保持一致。Android PackageInstaller 描述 split 安装会话和包集合相关约束。安装 API 约束不证明商店服务端生成的所有配置组合已被抽样或业务资源完整。
Android 二进制资源表使用带类型和长度的 chunk,并包含 package、type 与 entry 等结构。AOSP ResourceTypes 展示资源表相关 chunk、package、type 和 entry 数据结构。固定历史源码只用于解释格式结构,当前构建仍应以实际 aapt2 产物和工具输出为准。
依赖真实 Android 资源运行时和系统 API 的语义需要设备端测试。Android instrumented tests 说明设备端测试用于验证依赖 Android 运行环境的行为。单一设备通过不能代表完整 API、ABI、密度、语言和厂商矩阵。
语言 split 的存在性、默认资源回退和产品翻译要求必须分别给出结论。工程判断:文件选择、运行时资源解析和商业文案验收回答不同问题。静态清单只能确认预期文件和字段,不能替代安装后资源解析或人工语言验收。
本地派生、设备安装和测试轨道回执应作为递进但不可互换的证据等级。工程判断:三者分别覆盖工具输入、真实运行环境和商店交付链路。任何一档通过都只适用于已绑定的候选、设备 scope 和验证时点。

工程常见问题

检查 base APK 的资源是否完整,能否代表 AAB 交付完整?

不能。功能模块、语言、密度和 ABI 配置可能位于独立 split,目标设备的完整集合必须结合 AAB 候选、设备规格和交付时点核对。

生成 universal APK 并测试通过,是否足够验证资源 split?

不够。universal APK 合并了多种配置,无法复现按设备选择、分包安装和功能模块交付边界。它可作为补充检查,但不能替代设备专属 APK set。

出现目标语言 split,是否说明该语言页面全部翻译完成?

不说明。还要核对 split 的模块归属、设备是否选择它、默认资源是否可回退,并在安装后的真实资源解析中检查产品要求的关键文案。

bundletool 本地安装成功,能否证明 Play 会交付相同文件?

不能完全证明。本地结果绑定本地 bundletool、参数和签名;Play 线上处理与实际下发需要测试轨道或真实设备交付回执单独确认。

所有 split 的包名和证书一致,能否登记资源完整通过?

不能。身份一致是安装集合的前提,还需要核对目标语言、密度、ABI、功能模块、安装会话和运行时资源解析。它也不证明业务代码安全。

申请 AAB 加固评估前需要准备哪些 split 资料?

准备 AAB 与 APK set 摘要、设备配置矩阵、每个 split 的模块和限定符、签名类别、语言与资源清单、安装会话及设备测试回执,再通过御盾中央平台提交申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收