先看结论与判断条件
- 验收抽样依据实际存在的差异维度选取代表集,未进入矩阵的渠道需通过对应 variant 的验收抽样方可宣称有效。
- 签名变体抽样需覆盖使用不同签名证书的 build type 或 product flavor,并验证上传密钥与 Play App Signing 配置一致性。
- 资源差异抽样针对每个 product flavor 内的 source set 差异,检查加固前后资源表完整性而非假设压缩异常。
- SDK 组合覆盖矩阵基于 Google Play SDK Index 可用信息,对私有 SDK 或未列出组合需额外记录未覆盖边界。
- ABI 检查可参考 play track device catalog,本地验证需辅以 Play 线上测试以确保原生库交付完整。
- 用户覆盖抽样依据 country targeting 和 userFraction 设置,明确 halted 状态不自动回滚已安装客户端。
- 代码脚本根据渠道差异清单生成覆盖矩阵,标记未覆盖组合并在关键维度缺失时返回非零状态强制修正。
- 验收报告必须包含覆盖节点数、通过节点数及未覆盖边界的具体描述,不得将局部通过推广至全量渠道。
渠道差异清单构建与验收盲区识别
先把渠道之间真正不同的地方列出来。构建类型与产品风味可能改变签名,源码集覆盖会改变资源,渠道还可能替换 SDK、裁剪 ABI 或使用不同发布轨道。若把这些维度直接做全排列,许多组合根本不会出现在生产中;抽样应围绕实际渠道组合,而不是追求一张看起来很大的矩阵。
清单数据从渠道管理系统和构建脚本导出,记录产品风味、构建类型、applicationId 后缀、签名配置和资源目录。使用 Google Play 的渠道再补充发布轨道、目标国家和灰度比例。这里首次保留英文 API 字段,后文直接使用中文含义;动态下发的差异仍需从运行记录补充。
静态清单看不到远端配置触发的 SDK 行为、按需下发模块和部分私有签名流程。把这些项单独标为“需要运行验证”,并记录本轮是否拿到了对应包和回执。这样,报告能明确区分已检查内容与尚未取证部分。
| 维度 | 可能导致加固失效的场景 | 静态提取方式 | 工具覆盖边界 |
|---|---|---|---|
| 签名密钥与证书 | 加固壳签名校验失败、Play 保护机制拒识 | 解析 signingConfig 与 apksigner 验证 | 文档不能确认实际包使用正确生产证书 |
| 资源 source set | 加固后资源 ID 偏移、界面错位、多语言资源遗漏 | 分析 src/<flavor>/res 与 build 中间产物 | 合并规则复杂,手动抽查无法穷尽 |
| SDK 组合 | 加固 hook 与 SDK 通信冲突、类重复、初始化失败 | 依赖清单、Google Play SDK Index | SDK Index 不覆盖私有 SDK,也不替代二进制清单 |
| ABI 裁剪 | 原生库被错误剥离、armv7/arm64 兼容丢失 | AAB 派生 APK 中使用 bundletool 检查 lib | 本地派生不能完全替代 Play 线上交付回执 |
- 渠道差异清单是否包含所有 product flavor 与 build type 组合?
- 是否记录了每个渠道的 Play track、country targeting 和 userFraction?
- 是否将动态 SDK 加载、按需模块和离线投放列入未覆盖边界?
签名变体分层与签名校验抽样
签名是加固验收中优先级最高的验证维度,因为一旦签名出现异常,整个包在设备上的安装和更新都会直接失败。区域渠道包可能因为使用不同的 release signingConfig、构建类型或地域策略而产生多套签名。抽样必须确保每一类唯一的签名证书至少有一个代表包纳入验收范围。对于同时使用上传密钥和 Play App Signing 的渠道,还需要单独验证上传密钥与发布证书的映射关系,防止密钥混淆导致的分发中断。
具体的抽样步骤包括:从渠道差异清单中提取所有独一无二的签名别名、密钥库路径或证书指纹;对每个条目选取一个最近的发布包;使用 apksigner verify --print-certs 导出证书摘要;与构建预期或 Google Play Console 中的应用签名证书相比对。这一步骤旨在确认构建产物的签名身份与预期一致,避免因配置错误导致的安全校验失败。
如果渠道通过 Play App Signing 发布,还需通过 Play Developer API 获取当前生产证书指纹,验证本地包的上传签名有效且由 Google 代替重签名。但受限于官方文档《Sign your Android app》的说明,上述检查不能确认为实际线上包使用了正确的生产证书,该分歧必须记录入未覆盖边界。这意味着本地验证只能保证上传链路的正确性,无法完全替代云端的重签名确认环节。
特别要注意 debug 签名残留的问题。有时渠道因配置失误将 debug 证书带入灰度或生产 track,加固后壳校验如果拒绝 debug 证书会导致大面积安装失败。签名抽样因此也必须覆盖 debug build type 或任何使用默认 debug 证书的 flavor。若发现渠道使用自签名却未在内部证书清单注册,应终止后续流程并要求重新签名,所有非对称签名关系都在矩阵中作为独立节点记录。
| 渠道标识 | buildType | 签名类型 | apksigner 输出指纹 | Play 生产指纹 | 是否一致 |
|---|---|---|---|---|---|
| channel_sea_release | release | upload+RSA | A1:B2:C3... | A1:B2:C3... | 待验证 |
| channel_india_release | release | upload+ECDSA | D4:E5:F6... | D4:E5:F6... | 待验证 |
| channel_latam_debug | debug | debug | C7:D8:E9... | N/A | 不适用,debug 包不得发布 |
| channel_eu_release | release | Play App Signing | U1:V2:W3... | X4:Y5:Z6... | 用户安装证书为 Play 颁发,验证上传密钥 |
资源差异与编译变体分层抽样
资源差异源于 product flavor 的 source set、build type 中的资源替代或渠道特定的 resValue 覆盖。加固过程可能修改 resources.arsc 内的字符串池、资源名称映射或 png 压缩方式。如果加固工具错误地重写了资源表,可能导致界面显示错乱或多语言资源丢失,该类缺陷往往只在特定 flavor 中暴露。因此,验收抽样的操作目的是确认加固前后资源表的稳定性,而非假设必然发生压缩异常。
验收的核心在于界定资源差异的独立性:将渠道清单中每个 flavor 的 res 目录组合与 base 源集进行 diff,挑出存在非空资源重写的组合。对每一个这样的组合,至少选取一个 APK 使用 aapt2 dump resources 导出资源表,并检查加固前后相同 flavor 的资源 ID 是否稳定、字符串是否完整、是否符合压缩格式的预期值。若 flavor 仅覆盖少量字符串,可以只抽样该类资源相关的字符串表和解析代码段,以提高效率。
需要特别关注多 density、多 locale 的资源分割。当渠道特异设置 locale 资源时,加固可能误删或错误合并 fallback 资源。依据 Android build variants 对 source set 的描述,不同 buildType 与 flavor 的资源按优先级合并,合并规则复杂到难以人工完全覆盖。因此抽样后必须声明基于抽样的检查不能穷尽所有资源组合,未抽取的 flavor 资源变更不视为通过,需在报告中明确这一限制。
若渠道包以 Android App Bundle 形式发布,资源抽样还需覆盖 AAB 拆分的资源模块。可以使用 bundletool build-apks 生成当前抽样 flavor 的设备特定 APK set,再对其中的 base 和 split 分别解包比对资源,确认加固没有损坏按需分发的资源表或造成资源索引越界。这一过程确保了即使在分包交付场景下,核心资源依然保持完整且可被正确加载。
- 是否识别出所有非空资源覆盖的 source set 组合?
- 加固前后 aapt2 dump 出的资源数量是否一致?
- 多语言和多密度资源配置在 AAB 拆分后是否完整?
SDK 组合与第三方依赖覆盖矩阵
地区渠道常会替换支付、分析或广告 SDK。先比较每个渠道的实际依赖版本,再选出唯一组合做启动、登录、支付等对应路径回归。某个组合出现 ClassNotFound、反射失败或 Native 加载异常时,再依据日志判断是否与加固变化相关。
验收抽样应以 SDK 组合为单元,抽取在渠道清单中出现过的每种非空 SDK 集合作为矩阵中的一行。首先从每个渠道的 libs 目录、Gradle 依赖或最终的 DEX/APK 包里提取出所有非重复的第三方包名和版本,形成渠道-SDK 清单。接着可以在 Google Play SDK Index 中查找这些 SDK 的安全建议、权限需求和已知行为。但是 SDK Index 不覆盖所有私有或开源 SDK,也不替代二进制清单,对于不在 Index 中的 SDK,验收矩阵中需增加“未在 SDK Index 中评估”标签,并记为未覆盖边界。
验收矩阵构建后,为每一行选取一个带有该 SDK 集的 APK/AAB 执行动态测试。测试内容应包含 SDK 初始化是否成功、是否出现加固导致的 ClassNotFound、NoSuchMethod 或证书钉死错误。同时检查 Manifest 中的权限合并是否被加固额外引入新权限。由于加固后的 SDK 行为变化可能只在特定 AB 测试分支发生,矩阵内的通过结果不能推广至其他仅在运行时动态加载的 SDK 组件,必须严格限定验证范围。
代码实现上,可以编写脚本自动化解析每个渠道包的 assets/dex 列表或使用 dexdump 查找 SDK 特征类,并与一份基于工程判断的可信 SDK 名录比对,输出覆盖矩阵和未覆盖 SDK 列表。如果发现渠道清单上标记了必须通过验收的 SDK 而在抽样中因为包缺失无法验证,脚本应以非零退出。这确保了所有关键依赖都经过了显式的存在性检查和行为验证。
| 渠道组合 | SDK 集 | SDK Index 评估 | 核心功能抽检方法 | 覆盖状态 |
|---|---|---|---|---|
| SEA | AdMob, Firebase | 已评审,权限可靠 | 检查广告加载和 Firebase 远程配置初始化 | 通过 |
| India | Paytm SDK, CleverTap | Paytm 不在 SDK Index | 检查支付 SDK 启动与回调,依赖项目证据 | 索引外未覆盖 |
| LATAM | MercadoPago SDK, AppsFlyer | AppsFlyer 在索引,MercadoPago 不在 | AppsFlyer 归因验证通过,MercadoPago 仅基础 API 调用 | 部分未覆盖 |
| EU | Ironsource, Firebase | 均已评审 | 插屏广告与崩溃上报均正常 | 通过 |
ABI 裁剪与设备交付验证抽样
当渠道包以 Android App Bundle 形式分发时,用户设备仅接收与其 ABI 匹配的 split APK。加固工具在保护原生库时可能错误地剥离或压缩了特定架构的.so 文件,导致 ABI 绑定的崩溃。抽样验收需要显式覆盖渠道启用的所有 ABI,不能仅依赖单一架构的 APK 测试,必须确保目标架构的原生库在加固后依然完整可用。
利用 bundletool 可以从代表渠道的 AAB 文件中生成针对特定设备配置的全套 APK。验收应选取渠道差异清单中关于 ABI 配置的所有唯一值,为每一个 ABI 生成一个 APK set,并用 aapt dump badging 或反查 lib 目录确认 native library 存在、版本正确,且无加固防护导致的加载路径重定位错误。然而 Build and test Android App Bundles 文档所述的限制指出,本地生成不能完全替代 Play 线上交付回执,因此补充抽检应包括至少一个渠道的实机交付。
在设备矩阵的选择上,可以参考 Firebase Test Lab 的自动测试矩阵思路:依据设备目录选择市场上保有量较高的型号与 API level,覆盖低内存和高密度屏幕设备。但结合该文档约束,云真机矩阵仍需依据真实用户与最低支持范围选择,渠道抽样矩阵需要在 ABI 之外结合最低 API 级别和内存等级。如果某些渠道针对低端设备单独裁剪了库,那么对应设备不可缺失,否则无法验证兼容性。
未覆盖边界包括:渠道可能针对特定运营商或设备制造商进一步裁切原生库,或者使用基于资源的按条件分发,这些裁剪决策常常不体现在构建配置中,只能通过实际分发日志核实。对于这类未抽取的精细 ABI 剪裁,验收结论必须声明未覆盖。这意味着验收报告需明确指出哪些架构组合未经过实机验证,以供决策者评估风险。
商店分发配置验收:track、国家定向与灰度
渠道包在 Google Play 上的分发配置决定了哪些用户在何时收到加固过的版本。验收抽样必须确保这些配置与渠道差异清单一致,且加固没有影响与分发 track、国家定向或 userFraction 相关的任何发布机制。这一层面的验证主要聚焦于 Play Console 配置层面的正确性,而非客户端运行时的表现。
根据 Google Play Developer API 中的 tracks 接口,一个 release 记录了 versionCodes、status、userFraction 和 countryTargeting。验收时需要为每个渠道抽取其对应的一个 track release 记录,比对该 release 包含的 versionCodes 是否与加固后的渠道包一致,countryTargeting 是否匹配渠道目标市场,userFraction 是否符合灰度计划。如果清单中指定了多个 country targeting 组合,每个组合都应至少有一个 track 记录被纳入抽样,以确保地域限制的准确性。
还需要验证在灰度过程中,加固包在指定 userFraction 下的可安装性与更新路径。通过受限的内部测试用户,模拟升级、降级和全新安装流程,检查 Play 商店的替换签名是否正确,安装后是否有 signature mismatch 的拒绝。但是在执行这一验证时,必须注意 halted 状态的语义边界:halting 一个 release 仅停止向新用户分发该版本,不会影响已经安装该版本的用户,也不能被表述为客户端自动回滚或召回。因此验收回退逻辑只能测试 halted 后新用户的分发行为。
如果渠道使用了 staged rollout,抽样点应包括在 userFraction 变更时,前后的版本组合是否正确演化。例如,从 10% 灰度提升到 50% 时,versionCode 是否保持一致且签名延续,这些都可以通过 API 记录和有限测试覆盖。所有基于分发配置的验证都依赖 Google Play 的实际行为,测试脚本无法实现完全自动化,因此未模拟的条件必须记录,供审计使用。
| 渠道 | track | countryTargeting | userFraction | status | 抽样包 versionCode | 验证结果 |
|---|---|---|---|---|---|---|
| SEA | production | SG,TH,ID | 100% | completed | 2101 | 覆盖 |
| India | production | IN | 50% | inProgress | 2102 | 覆盖 |
| LATAM | beta | BR,MX,AR | 10% | inProgress | 2103 | 覆盖 |
| EU | alpha | DE,FR,IT | 100% | halted | 2104 | 仅验证新安装拒绝 |
用户覆盖与回退条件验证
用户覆盖验证将渠道抽样的粒度细化到终端场景,旨在确认加固后的包在实际用户环境中的稳定性不会因缺少设备或系统版本覆盖而导致具体可观测的故障类型,如 native crash、ANR 或 silent failure。除了常规的设备型号和 OS version 维度,对区域渠道包还应强调运营商网络条件和语言环境的代表性,以反映真实用户的使用场景。
在分层抽样中,用户覆盖维度可以从 play 商店提供的设备安装数据中提取代表性设备列表,按 ABI、OS、屏幕和内存组合成设备矩阵。验收抽样不需要覆盖所有排列,而应优先选择安装基数大的配置。根据 Firebase Test Lab 的使用经验,一个设备矩阵中任一设备执行失败会影响整矩阵的判断,因此覆盖矩阵的宽窄应在风险和资源间平衡。同时,该文档边界已经指出:云真机矩阵仍需依据真实用户与最低支持范围选择,说明抽样设计不可直接照搬测试服务推荐矩阵。
回退条件验证必须仔细区分:验收过程中发现加固包出现严重缺陷时,运营团队是否可以通过暂停 track 来阻止进一步分发。此时可以利用 Play Developer API 将 release 状态改为 halted,并通过抽样确认新用户无法安装该版本。但 halted 不影响已安装用户,这是关键的行为约束,在产品说明和验收结论中都应着重指出,不能给业务方造成可远程修复所有客户端的误解。
已分发用户的修复不在本抽样流程范围内。验收报告应清晰区分“分发暂停”和“客户端回退”,并建议对已安装用户的补救方案单独评估。例如,潜在修复方案包括发布新的高版本加固包强制升级,或利用应用内开关关闭已损坏功能,这些举措超出加固验收范畴,需由运营团队另行决策和执行。
验收抽样执行流程与未覆盖边界记录
执行时先导出最新渠道清单。每个渠道记录签名配置、资源组合、SDK 列表、ABI 设置和分发参数,并附上数据来源与生成时间。脚本只处理这份实际清单,不自行猜测缺失字段。
接着运行抽样脚本。它为签名、资源、SDK、ABI 和分发配置建立唯一值集合,对照实际渠道组合输出覆盖节点与缺口。关键字段缺失或引用未登记的签名、轨道时返回非零状态,提醒维护者修正输入。
测试人员按覆盖节点执行验证并回填结果。工具不支持、SDK 资料缺失、分发回执未取得或动态资源未触发的节点,都写成未覆盖并说明原因。报告列出覆盖、通过和未覆盖数量,不把空白节点算作通过。
本节代码块读取 JSON 清单,整理各维度并把实际渠道映射到覆盖矩阵。输入缺少关键字段时返回非零状态。脚本不访问商店 API,也不判断包体是否已经兼容;它只负责暴露选样输入是否完整。
- 渠道差异清单是否以结构化 JSON 格式维护并纳入版本管理?
- 矩阵生成脚本是否在每次验收时运行并记录未覆盖组合?
- 未覆盖边界是否在验收报告中逐条说明原因和潜在风险?
- 验收签署前是否已审查所有关键维度的抽样通过节点?
import json
import sys
from itertools import product
def load_channel_list(path):
with open(path) as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("清单必须是渠道对象数组")
return data
def validate_channels(channels):
required = ["channel_id", "signature", "resource_flavor", "sdks", "abi_set", "track", "country"]
for ch in channels:
for r in required:
if r not in ch:
print(f"{ch.get('channel_id','?')} 缺少 {r}", file=sys.stderr)
sys.exit(3)
return channels
def build_matrix(channels):
sigs = sorted({c["signature"] for c in channels})
flavors = sorted({c["resource_flavor"] for c in channels})
sdk_sets = sorted({tuple(sorted(c["sdks"])) for c in channels})
abis = sorted({tuple(sorted(c["abi_set"])) for c in channels})
tracks = sorted({c["track"] for c in channels})
return {
"dimensions": {
"signature": sigs,
"resource_flavor": flavors,
"sdk_set": [list(s) for s in sdk_sets],
"abi_set": [list(a) for a in abis],
"track": tracks
}
}
def check_coverage(channels, matrix):
actual = set()
for c in channels:
key = (c["signature"], c["resource_flavor"],
tuple(sorted(c["sdks"])), tuple(sorted(c["abi_set"])), c["track"])
actual.add(key)
dims = matrix["dimensions"]
full_space = list(product(
dims["signature"], dims["resource_flavor"],
[tuple(s) for s in dims["sdk_set"]],
[tuple(a) for a in dims["abi_set"]], dims["track"]
))
uncovered = [combo for combo in full_space if combo not in actual]
covered = [combo for combo in full_space if combo in actual]
print("覆盖节点数:", len(covered))
for node in covered:
print("覆盖:", node)
print("未覆盖节点数:", len(uncovered))
for node in uncovered:
print("未覆盖:", node)
return uncovered
def main():
if len(sys.argv) != 2:
print("用法:python coverage.py <渠道清单.json>", file=sys.stderr)
sys.exit(2)
channels = load_channel_list(sys.argv[1])
channels = validate_channels(channels)
matrix = build_matrix(channels)
uncovered = check_coverage(channels, matrix)
if len(uncovered) > 0:
print(f"存在 {len(uncovered)} 个未覆盖维度组合,验收拒绝。", file=sys.stderr)
sys.exit(1)
else:
print("全部维度组合均已覆盖。")
sys.exit(0)
if __name__ == "__main__":
main()事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 不同 build variant 即使共享代码仓库,也可能拥有完全不同的 SDK 集合、资源合并结果和签名证书,因此必须针对每个 variant 定义独立验收抽样。 | Android build variants 文档 | 该文档仅描述构建系统行为,不保证实际构建产物与描述一致,也不覆盖发布后动态加载的组件。 |
| 应用签名需区分应用签名密钥、上传密钥与 Play App Signing 责任,签名验证必须检查这些对应关系。 | Sign your Android app 文档 | 该文档不提供自动确认线上 APK 使用了正确生产证书的方法。 |
| bundletool 可用于在本地基于 AAB 重现派生 APK,从而验证资源、ABI 等维度的完整性与加固影响。 | Build and test Android App Bundles 文档 | 本地生成不能完全替代 Play 线上交付回执,无法复现所有设备定向逻辑。 |
| Firebase Test Lab 的设备矩阵中任一执行失败将影响矩阵整体判断,因此验收覆盖矩阵设计需谨慎选择代表性设备。 | Firebase Test Lab Android matrices 文档 | 云真机矩阵仍需依据真实用户与最低支持范围选择,不可直接采用推荐矩阵。 |
| Google Play SDK Index 提供 SDK 权限、可靠性与数据处理指引,可用于验收抽样中 SDK 组合的预评估。 | Google Play SDK Index 文档 | SDK Index 不覆盖所有私有或开源 SDK,也不替代二进制清单,无法覆盖安全漏洞。 |
| Google Play track release 的 countryTargeting 和 userFraction 字段允许验收团队核对分发配置是否与渠道差异清单一致。 | Google Play Developer API tracks 文档 | 获取的 release 信息可能因缓存或权限延迟,且 halted 状态不影响已安装用户。 |
| 上传密钥与 Play 应用签名密钥的分离要求渠道抽样时必须验证两者映射的正确性,以避免签名错误。 | Sign your Android app 文档 | 无法通过 API 直接验证 Play 应用签名密钥的私钥合规性,只能依赖二次核查。 |
| 使用 bundletool 检查 AAB 派生 APK 的原生库是验证 ABI 覆盖的直接方法,但需辅以实际 Play 下载测试。 | Build and test Android App Bundles 文档 | 本地生成的 APK set 可能不包含所有条件分发模块。 |
工程常见问题
验收抽样为何不能全面测试所有渠道包,而必须分层?
很多维度的全排列在实际渠道中并不存在。先以真实构建组合为底表,再选择能覆盖每种签名、资源、SDK 和 ABI 差异的包,可以把时间用在有效节点上。未抽到的组合明确写成未验证,不随代表包自动通过。
如何判断一个签名变体是否需要进入抽样?
看最终证书指纹和发布链路,而不只看 Gradle 配置名称。证书不同、由商店重签或走独立企业分发的包都应单列节点。调试签名若意外出现在可发布清单中,应作为配置异常处理,不把它当正常生产样本。
当渠道依赖的 SDK 不在 Google Play SDK Index 中时,验收结论应如何处理?
先标记为“公开索引无资料”,再从依赖锁文件、供应商文档和实际包体确认版本。功能回归与数据行为要用项目自己的测试证据补齐。SDK Index 没有收录并不等于不安全,也不等于已经通过。
已有内部测试群的设备能否替代 Firebase Test Lab 的矩阵?
可以使用内部设备,只要选择依据来自渠道的真实用户分布和最低支持范围。记录机型、系统、ABI 与网络条件,失败时补充相邻配置。无论内部设备还是云真机,都要在报告中列出没覆盖的环境。
如果灰度发布在验收中途被 halted,后续验收结论如何写?
分别写清暂停后的新分发结果和已安装用户状态。halted 可停止继续分发,但不会自动让设备退回旧版本。已经安装的用户如何修复属于另一项发布决策,不能合并成“已经回滚”。
验收矩阵生成脚本失败(非零退出)意味着什么?
先看错误指出的是缺字段、未登记值还是没有实际渠道覆盖。修正清单或补充样本后再运行。脚本失败只说明选样输入不完整,不代表某个渠道包功能失败,也不判断加固效果。