先看结论与判断条件
- 把最终 AAB 而非源码资源目录作为验收起点,记录模块、资源配置与构建身份,避免检查对象和实际交付对象不一致。
- 为目标 locale、ABI、密度和 API 生成或取得设备 APK 集,核对每个配置获得的 base、语言和资源 split,而不是只看通用包。
- 关键资源清单要覆盖启动、登录、支付、错误、无障碍和合规页面,并明确目标翻译、默认回退以及资源类型。
- 静态资源存在只证明派生集合包含对应条目,仍需在设备上切换语言、重建进程并从真实入口验证查找结果和布局。
- 本地 bundletool 适合重现设备选择,云真机适合扩展 locale 与设备矩阵;两者都不能替代未执行条件或真实商店回执。
- 本文只验收语言与资源 split,不据此证明 AAB 最终身份安全、资源表未被篡改或所有设备已经兼容。
资源缺失要沿交付链定位,不从界面猜原因
加固后出现按钮文字回退、图片空白、主题异常或 Resources.NotFoundException,可能来自不同阶段:源码缺少目标配置、构建时资源被过滤、AAB 模块没有携带条目、设备派生集合未选择语言 split、安装集合残缺、运行时配置没有刷新,或代码使用了不稳定的动态名称查找。界面现象相似,但修复位置不同,因此第一步是固定候选身份和设备配置,再寻找最早不一致。
Android App Bundle format 说明 AAB 由 base、feature、配置和资产相关模块组成,设备安装的是从这些输入派生的 APK 集,而不是直接安装 AAB 文件。语言、密度和 ABI 等配置可能落入不同 split。同一 AAB 在两台设备上得到的文件集合可以不同,验收不能只打开 AAB 看见资源名称就宣布设备一定能取得,也不能用一台默认中文设备代表所有 locale。
一条可复核链应连接最终 AAB 摘要、模块与资源配置、device spec、派生 APK set、实际选中文件、资源表输出、设备已安装 split、运行时 locale、关键资源查找和页面结果。任何一环缺失,结论只写到已有证据范围。本文不讨论 AAB 最终身份固定或资源表修改防护,仅回答加固候选怎样避免语言与资源 split 缺失。
| 现象 | 优先检查层 | 直接证据 | 不应先做 |
|---|---|---|---|
| 目标语言显示默认文案 | 语言资源与设备选择 | 资源条目、locale 和 split | 复制字符串到代码 |
| 图片或字体空白 | 资源类型与配置 split | 派生 APK 和设备日志 | 关闭全部资源优化 |
| 主题属性找不到 | base 与 feature 资源契约 | 模块资源表和真实入口 | 扩大所有 keep 规则 |
| 切换语言后未刷新 | 运行时配置与生命周期 | 进程、Activity 和 locale 时间线 | 只重开单个页面 |
| 仅部分设备失败 | device spec 与安装集合 | ABI、密度、API 和 split 列表 | 归因厂商兼容 |
| 加固前后结果不同 | 同输入候选差异 | AAB、派生集与运行回执 | 同时改资源和业务代码 |
关键资源清单要从业务入口反推
不是所有资源具有相同发布风险。清单应从启动、登录、授权、支付、订阅、错误处理、帮助、隐私与无障碍等入口反推,记录资源稳定名称、类型、所属模块、默认配置、必须翻译的 locale、允许回退的 locale、调用页面和失败后果。只统计 strings.xml 行数会漏掉复数、数组、图片、字体、主题属性、导航、原始文件和 feature 专属资源。
默认资源和目标语言资源要分别核对。默认配置承担回退,但回退是否可接受是产品判断:法律条款、价格说明和风险提示通常不能把未翻译默默当成通过;通用图标或非文本装饰可能允许共用。工程门禁需要为每个关键资源标注 required-translation、fallback-allowed 或 locale-independent,并由内容和业务负责人确认,而不是看到运行时没有崩溃就结束。
资源身份使用稳定名称与类型,不能只保存编译后的整数 ID。整数值可能受构建与链接结果影响,动态查找还可能把拼写错误变成运行时缺失。清单把源名称连接到最终资源表条目,再由 instrumented test 通过应用 API 查找并渲染。若项目确实依赖 getIdentifier、反射或远端资源名,单独列为高风险契约并覆盖未知名称和回退,而不是扩大成所有资源都动态查找。
| 字段 | 作用 | 示例判断 | 缺失动作 |
|---|---|---|---|
| resourceName | 连接源码与资源表 | string/payment_error | 无法稳定定位则阻断 |
| ownerModule | 确定 base 或 feature 所有权 | base、checkout_feature | 模块不明则补来源 |
| requiredLocales | 声明必须翻译范围 | 基于实际市场与合规 | 不得自动全量通过 |
| fallbackPolicy | 说明默认资源能否接受 | 允许、禁止或待审 | 未知按禁止处理 |
| entryPath | 提供真实运行入口 | 登录失败、支付确认 | 只有资源名不足 |
| failureImpact | 决定门禁优先级 | 误导价格或阻断启动 | 由业务负责人补充 |
用 device spec 生成代表性语言与配置集合
Build and test Android App Bundles 描述了使用 bundletool 构建 APK set、生成或提供 device spec,并把适配集合安装到设备的方法。代表性 spec 至少包含目标 API、支持 ABI、屏幕密度和 locale。测试设计应来自真实支持范围和用户分布,同时加入默认语言、从右到左语言、区域变体以及不存在完整翻译的回退样本;不能仅生成 zh-CN 与 en-US 后称为国际化完成。
device spec 是派生输入,不是设备运行回执。它可以重现 bundletool 如何选择 APK,但不能证明某个厂商系统、应用语言设置、下载服务或安装现场与 JSON 完全一致。每份 spec 需要摘要和可读标签,生成的 apks 文件也要保存摘要;解出的 APK 列表记录文件名、split 名、模块、签名与资源检查结果。若同名 spec 被修改,必须生成新身份。
本地生成要明确签名等级。bundletool 能使用提供的签名参数生成 APK set;若测试没有绑定真实发布签名,产物只能作为结构与本地加载证据,不能登记成生产候选。生产采用 Play App Signing 时,还要在测试轨道核对实际交付集合和应用签名责任。本文不把本地 set 与线上交付混为一类,也不从一次生成成功推断全部 locale 均已交付。
| 维度 | 最小代表 | 可能影响 | 记录方式 |
|---|---|---|---|
| API | 最低、主流和最新目标 | 资源与配置行为 | sdkVersion 与设备系统 |
| ABI | 应用实际支持的每类 ABI | Native 与配置 split | supportedAbis 和现场 ABI |
| 密度 | 低、中、高及主流设备 | 图片和布局资源 | screenDensity 与型号 |
| locale | 默认、主语言、RTL、区域变体 | 语言 split 与回退 | supportedLocales 和运行值 |
| 模块状态 | base 与已安装 feature | 资源所有权与加载 | 安装 split 列表 |
| 安装来源 | 本地、测试轨道 | 签名与交付路径 | 证据等级分开 |
资源表检查回答存在性,设备测试回答可用性
AOSP ResourceTypes 的历史源码展示了 Android 二进制资源表由带类型与长度的 chunk、package、type 和 entry 等结构组成。它适合解释为什么资源不是普通文件名列表,以及错误的偏移、长度或引用会影响解析。该固定历史版本不能代表当前构建工具的所有实现细节,实际候选仍应以项目当前 aapt2 和 bundletool 产物为准,不能用阅读结构定义替代解析结果。
静态检查分两层。第一层确认目标 device spec 解出的 APK 集包含预期模块和语言配置;第二层使用当前 aapt2 等官方构建工具输出每个 APK 的资源表,合并检查关键资源名称、类型和配置。关键条目在某个 split 中存在,只证明这份派生集合静态可见;资源引用是否能从 base 或 feature 正确解析、配置切换后是否刷新,仍需要设备运行。
运行检查应通过应用的真实 Context 和页面入口取得资源。对每个目标 locale,记录设置前后的 Configuration、应用进程是否重建、Activity 是否重建、当前安装 split、资源实际字符串或可渲染状态以及截图。字符串比较要避免把日期、数量和服务端内容误当成静态资源;图片比较优先断言非空、尺寸与语义标识,不用脆弱的像素完全一致替代人工可读性检查。
| 检查 | 输入 | 能发现 | 不能证明 |
|---|---|---|---|
| AAB 模块检查 | 最终 AAB | 模块与配置声明缺失 | 设备选择正确 |
| APK set 列表 | AAB 和 device spec | 派生文件集合差异 | 设备已完整安装 |
| aapt2 资源输出 | 选中 APK | 关键名称与类型不存在 | 页面一定能解析 |
| 设备 split 列表 | 已安装应用 | 现场集合与预期不同 | 资源内容正确 |
| instrumented 查找 | 设备 Context 与 locale | 运行时查找失败或回退 | 所有页面布局可读 |
| 截图与人工复核 | 真实页面和业务数据 | 截断、重叠与方向问题 | 未执行设备兼容 |
语言切换必须覆盖进程、页面与 feature 边界
只在系统设置里切换语言再看当前页面,容易保留旧 Context、缓存文案或已经创建的 View。回归应记录切换动作后 Activity 与进程的实际生命周期,至少覆盖当前页面重建、返回栈中的旧页面、冷启动、后台恢复和通知或深链入口。测试结果以应用实际 Configuration 和资源值为准,不假设一次界面刷新已经更新所有组件。
base 与 feature 的资源契约需要双向验证。base 若引用 feature 只在安装后存在的资源,未安装状态必须有合法门控;feature 若复用 base 的主题与字符串,安装后要在目标 locale 和密度下解析。动态模块下载回归属于另一个意图,本页只在模块已经按计划安装的前提下核对资源集合和查找。测试报告列出模块安装状态,避免把资源缺失和模块未交付混成同一错误。
从右到左布局、区域变体和格式化字符串不能只靠资源存在性判断。RTL 需要查看方向、图标语义、返回箭头、数字与混排;区域变体需要验证选择与回退符合产品规则;带占位符的字符串要用代表性长文本、数量和错误参数执行。未翻译条目回退到默认语言可能是预期,也可能违反合规要求,最终结论必须引用清单中的 fallbackPolicy。
- 切换前后记录应用实际 Configuration
- 覆盖 Activity 重建、冷启动与后台恢复
- base 与 feature 资源分别从真实入口查找
- RTL 检查方向、混排、图标与交互语义
- 区域变体和默认回退按资源清单判定
- 格式化字符串使用长文本与错误参数回归
用脚本固定派生集合和关键资源门禁
自动门禁应接收明确的 bundletool、aapt2、最终 AAB 和关键资源清单,不从工作目录猜文件。脚本为多个代表 locale 构建设备 spec,使用同一 AAB 生成相应 APK set,解出设备选择的 APK,再合并 aapt2 资源输出检查关键名称。每个失败都返回非零状态并保留输出目录,便于审阅实际 spec、APK 和资源转储。
下面 Bash 示例使用通用设备参数和外部关键资源清单,不包含签名密钥、账号、内网地址或客户资源名。它验证输入存在、为四个代表语言生成 device spec、执行 build-apks 与 extract-apks、要求解出至少一个 APK,并逐条检查资源表输出。项目应把 ABI、密度、API 和 locale 替换为经过批准的真实矩阵;示例未提供发布签名,因此生成物只能用作结构回归。
静态脚本没有打开页面,也没有证明运行时 locale 生效。门禁通过后仍要把同一 APK set 安装到对应设备,运行 instrumented test 并记录资源值与截图。若资源缺失,先保留失败 spec 和输出,不立即向 base 复制所有翻译;复制可能掩盖 split 选择或模块所有权错误,还会改变回退政策。修复后生成新候选和新摘要,不能覆盖旧证据。
- 输入文件均由显式参数提供并验证存在
- device spec 与 APK set 保留在独立证据目录
- 资源检查合并当前设备选中的全部 APK
- 关键资源清单忽略空行与公开注释
- 任一 locale 缺失均返回非零退出状态
- 脚本通过后继续执行设备运行与页面验证
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 4 ]]; then
echo "usage: check_resource_splits.sh bundletool.jar aapt2 app.aab critical-resources.txt" >&2
exit 64
fi
bundletool_jar=$1
aapt2_bin=$2
aab_file=$3
resource_list=$4
if [[ ! -f "$1" || ! -f "$2" || ! -f "$3" || ! -f "$4" ]]; then
echo "one or more required input files are missing" >&2
exit 66
fi
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/resource-split-check.XXXXXX")
locales=("zh-CN" "en-US" "ar" "pt-BR")
failures=0
for locale in "${locales[@]}"; do
case_dir="$work_dir/$locale"
mkdir -p "$case_dir/extracted"
spec_file="$case_dir/device-spec.json"
apks_file="$case_dir/candidate.apks"
dump_file="$case_dir/resources.txt"
printf '{"supportedAbis":["arm64-v8a"],"supportedLocales":["%s"],"screenDensity":420,"sdkVersion":35}\n' "$locale" > "$spec_file"
java -jar "$bundletool_jar" build-apks \
--bundle="$aab_file" \
--output="$apks_file" \
--device-spec="$spec_file" \
--overwrite
java -jar "$bundletool_jar" extract-apks \
--apks="$apks_file" \
--device-spec="$spec_file" \
--output-dir="$case_dir/extracted"
mapfile -t apk_files < <(find "$case_dir/extracted" -type f -name '*.apk' -print | sort)
if [[ ${#apk_files[@]} -eq 0 ]]; then
echo "$locale: no APK was selected" >&2
failures=$((failures + 1))
continue
fi
: > "$dump_file"
for apk_file in "${apk_files[@]}"; do
"$aapt2_bin" dump resources "$apk_file" >> "$dump_file"
done
while IFS= read -r resource_name; do
[[ -z "$resource_name" || "$resource_name" == "#"* ]] && continue
if ! grep -Fq "$resource_name" "$dump_file"; then
echo "$locale: missing $resource_name" >&2
failures=$((failures + 1))
fi
done < "$resource_list"
done
if [[ $failures -ne 0 ]]; then
echo "resource split gate failed with $failures finding(s); evidence: $work_dir" >&2
exit 2
fi
echo "resource split gate passed; evidence: $work_dir"设备矩阵要把 locale 当成一等维度
Firebase Test Lab Android matrices 文档把设备型号、系统版本、方向和 locale 作为矩阵维度,并说明矩阵由多次执行组成。云真机适合扩大设备与语言组合,但选择仍应来自真实支持范围。矩阵中某次执行失败意味着对应组合没有通过,不能用多数成功掩盖;服务不可用、基础设施错误和应用失败也要分开记录。
Android instrumented tests 运行在设备或模拟器上,可以访问 Context、Resources、Configuration、Activity 和系统 API。测试应读取关键资源并从真实页面验证,而不是只对源码 XML 做单元断言。每次执行记录候选摘要、安装 split、locale、方向、API、ABI、密度、进程重建方式、资源值摘要和截图。涉及用户数据的页面使用测试账号与公开安全样本。
矩阵规模应按风险分层。所有支持 locale 先做静态派生与关键资源门禁;高流量、RTL、法规要求、复杂字体和长文本语言进入更广设备矩阵;低风险配置至少覆盖一台代表设备和默认回退。这个分层是工程计划,不是兼容结论。未执行组合在报告中保持未测试,不把 planned、queued 或云平台接受任务写成通过。
| 维度 | 执行值 | 核心断言 | 结果边界 |
|---|---|---|---|
| locale | 实际应用 Configuration | 目标翻译或允许回退 | 不代表其他区域变体 |
| 方向 | LTR 或 RTL | 布局、图标和混排可用 | 截图仍需人工复核 |
| 设备 | 型号、API、ABI 和密度 | 派生集合与资源加载 | 不外推未测厂商 |
| 模块 | base 与已装 feature | 所有者资源均可查找 | 未交付模块另测 |
| 生命周期 | 重建、冷启、恢复 | 配置不被旧缓存覆盖 | 只覆盖列出入口 |
| 证据状态 | 通过、失败、未测或基建错误 | 不混淆执行结果 | 任务创建不是通过 |
按最早差异修复并绑定发布证据
故障定位按交付链顺序进行。AAB 中缺少资源先查源码所有权、模块与打包配置;AAB 有而 device spec 派生集没有,检查目标配置和拆分选择;派生 APK 有而设备未安装,检查安装来源与现场集合;设备有而 aapt2 或运行时找不到,检查资源表、引用和模块 Context;值存在但页面异常,再查生命周期、布局、字体和业务数据。每步只改变一个可解释变量。
常见临时动作会掩盖根因:把全部语言放回 base、关闭所有拆分、复制 feature 资源到 base、硬编码文案、用默认资源覆盖错误,或清空数据后只测冷启动。这些动作可以用于验证假设,但不能直接成为长期修复。正式修复要说明资源所有权、交付与回退政策,重新生成 AAB、APK set 和设备回执,并确认没有把安装体积、模块边界或合规内容变化留在未知状态。
放行记录应限定为指定摘要 AAB 在列出的 device spec、APK set、安装来源、设备、locale、模块状态和业务入口中,关键资源存在并按政策查找或回退。不能写成所有语言完整、所有设备兼容或资源表绝对安全。准备资源 split 回归评估时,可整理最终 AAB、资源清单、目标 locale、device spec、aapt2 输出、实际 split 和设备矩阵,再通过御盾中央平台提交申请。
| 检查点 | 通过条件 | 失败指向 | 下一证据 |
|---|---|---|---|
| 最终 AAB | 模块携带目标资源 | 源码、模块或构建配置 | 模块与资源清单 |
| device spec 派生 | 选中预期 split | 设备配置或交付选择 | APK set 与 toc |
| aapt2 输出 | 关键名称和类型存在 | 资源链接或过滤 | 各 APK 资源转储 |
| 设备安装集合 | 现场 split 与预期一致 | 安装来源或残留状态 | 包管理器现场记录 |
| 运行时查找 | 目标值或允许回退 | Context、配置或引用 | instrumented 日志 |
| 页面结果 | 布局、字体和语义可用 | UI 或业务数据 | 截图与人工复核 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| AAB 由 base、feature、配置与资产相关模块组成,设备安装的是派生 APK 集。 | Android App Bundle format 描述 bundle 的模块与设备交付结构。 | AAB 结构不能证明某台设备实际取得了完整语言和资源 split。 |
| bundletool 与测试轨道可用于构建和验证 AAB 派生的设备交付行为。 | Build and test Android App Bundles 描述 APK set、device spec、设备安装与测试轨道。 | 本地生成结果不能完全替代 Play 线上交付和真实账号设备回执。 |
| bundletool 可以从 AAB 构建、检查并部署按设备配置选择的 APK set。 | bundletool 文档描述相关命令、device spec 与签名参数。 | 未绑定真实发布签名的本地产物不能登记为生产发布候选。 |
| Android 二进制资源表包含带类型和长度的 chunk、package、type 与 entry 等结构。 | AOSP ResourceTypes 展示固定历史版本的资源表结构定义。 | 历史源码只用于解释结构,当前候选仍以项目使用的 aapt2 输出和设备解析为准。 |
| Android 云真机矩阵可以按型号、系统、方向和 locale 组合多次执行。 | Firebase Test Lab Android matrices 描述设备矩阵配置与执行结果。 | 云矩阵仍需按真实支持范围选择,任务提交或多数执行成功不代表全部通过。 |
| 依赖 Android Resources、Configuration 和组件生命周期的行为适合设备端测试。 | Android instrumented tests 描述在设备或模拟器上访问 Android 框架的测试。 | 单一设备和 locale 通过不能代表全部 API、ABI、密度、厂商与语言。 |
| 关键资源应同时进行派生集合静态检查和真实入口运行检查。 | 工程判断:静态存在性与运行时 Context、配置刷新和页面可用性属于不同失败边界。 | 具体关键资源、目标 locale 与允许回退必须由项目业务和合规要求确认。 |
| AAB、device spec、APK set、资源转储和设备回执必须绑定同一候选链。 | 工程判断:只有可追溯身份才能排除重构建、重签、配置变化和文件替换造成的证据漂移。 | 证据链完整仍不能外推未执行设备或证明资源表不存在任何安全风险。 |
工程常见问题
源码中有某个翻译,为什么设备上仍可能缺失?
资源还要经过模块打包、AAB 构建、设备配置选择、split 安装和运行时查找。任一环不一致都可能让源码存在但设备不可用。
检查 AAB 里的资源名称是否足够?
不够。还要为代表 device spec 生成实际 APK 集,检查选中 split,并在设备目标 locale 下重建进程、查找资源和打开真实页面。
默认语言能够回退,是否可以忽略所有缺少翻译?
不能统一忽略。价格、合规、风险提示等资源可能禁止回退;每个关键资源应有明确 requiredLocales 和 fallbackPolicy。
bundletool 本地测试通过能否代表 Play 交付?
不能完全代表。本地工具适合重现设备选择与结构,还要区分签名,并在适用的测试轨道与真实设备上核对交付集合。
语言回归为什么必须杀进程或重建页面?
旧 Context、缓存文案和已创建界面可能保留切换前配置。需要验证重建、冷启动和后台恢复后的实际 Configuration 与资源结果。
申请语言与资源 split 回归评估要准备什么?
准备最终 AAB、关键资源和回退政策、目标 locale、device spec、APK set、aapt2 输出、实际安装 split 与设备矩阵,再通过御盾中央平台提交申请。