先看结论与判断条件
- AAB 上传体积、通用 APK、设备派生 APK 集、应用商店传输大小和安装后占用是不同指标,不能混成一个包体数字。
- 比较必须绑定未加固基线与加固候选的 SHA-256、variant、versionCode、签名阶段、R8 规则、依赖锁和模块集合。
- AAB 会按 ABI、语言、密度、SDK 与动态功能条件派生不同 APK,代表设备矩阵比单一 universal APK 更接近用户交付。
- bundletool 可生成并检查设备 APK set,但未提供发布签名时产生的诊断对象不能冒充商业发布候选或线上下载回执。
- 安装占用还受 Native 库提取、运行时编译、资源、数据库和缓存影响,应在固定设备状态下区分首次安装与业务数据增长。
- 体积差异只能说明交付成本变化,不能推导下载转化、用户流失、启动性能或加固保护效果,相关结论需要独立真实数据。
先统一五种体积口径,避免同名指标互相替代
团队常把“包体”同时用来指 AAB 文件、通用 APK、应用商店下载量和安装占用。它们来自不同阶段,也回答不同问题。AAB 文件大小影响上传与归档;设备派生 APK 集接近某类设备拿到的代码和资源;传输大小还涉及压缩与服务端处理;安装占用则包含解包、Native 库和运行时数据。
软件加固可能新增 DEX、Native 载荷、配置、资源或元数据,也可能改变原有内容的压缩特征。某个 AAB 增量不一定等比例进入每台设备,因为 ABI、语言、密度和动态模块会筛选交付内容。反过来,AAB 文件变化很小也不能保证安装占用不变,设备端提取和运行时生成文件仍可能增加。
测量报告应为每个数字标注 metricName、candidateSha256、toolVersion、deviceSpec、moduleSet、compressionBasis 和 collectedAt。若只有一个无口径数字,无法判断比较对象是否一致。工程判断是至少保留上传、派生、传输近似和安装四层记录,并把动态模块单列。
| 指标 | 测量对象 | 主要用途 | 不能代替 |
|---|---|---|---|
| AAB 文件字节 | 上传与归档文件 | 比较构建产物 | 设备下载大小 |
| 通用 APK 字节 | 包含多配置的单 APK | 侧载与粗略诊断 | Play 设备派生结果 |
| 设备 APK set | 特定设备配置的 APK 集 | 比较代表设备交付 | 线上真实传输回执 |
| 传输口径 | 服务端处理后的下载数据 | 评估网络成本 | 安装后占用 |
| 安装占用 | 设备文件与运行时产物 | 评估存储影响 | 初始下载大小 |
先锁定候选身份,再谈加固前后差值
有效对照要求未加固基线与加固候选来自同一源码提交、依赖锁、资源、build variant、targetSdk、R8 规则和模块配置。若加固版本同时升级 SDK、图片资源或第三方依赖,观察到的字节差无法只归因于加固。基线也应由同一受控流水线生成,而不是从历史下载目录挑一个近似版本。
两份产物至少记录 artifactSha256、versionCode、applicationId、variantId、签名阶段、构建工具版本和生成时间。签名数据本身会改变文件字节,因此发布签名对象与诊断签名对象要分开比较。重新签名、重新构建或重新加固都产生新候选,旧尺寸回执不能沿用到摘要不同的文件。
模块集合必须一致或显式解释。动态 feature、新增 asset pack、ABI 拆分和语言资源都会改变派生结果。比较脚本可以发现文件级差异,却不能判断变化是否合理。负责人应把新增条目连接到保护配置、业务模块或构建变更,并将无法解释的增量标记为阻塞。
| 字段 | 为什么需要 | 错配表现 | 处理 |
|---|---|---|---|
| 源码与依赖 | 排除业务代码增量 | 加固包同时升级 SDK | 重建可比基线 |
| variant 与模块 | 保持交付集合一致 | feature 或 ABI 集不同 | 拆分报告 |
| R8 规则 | 控制缩减和优化差异 | 基线未开启 release 优化 | 统一构建配置 |
| 签名阶段 | 签名影响文件字节 | 调试签名对比发布签名 | 同阶段比较 |
| 文件摘要 | 锁定精确对象 | 文件名相同但内容变化 | 拒绝旧回执 |
AAB 模块结构决定哪些加固增量会到达设备
Android App Bundle format 描述 AAB 由 base、feature、配置和资产模块组成,分发系统会为设备生成适配的 APK。base 中的公共加固载荷通常进入所有安装;某个动态 feature 内的增量只会影响获得该模块的设备;ABI 专属 Native 库则按设备架构选择。完整 AAB 不是设备直接安装对象。
因此报告应按 moduleName、entryType、ABI 和 deliveryMode 拆解。若保护工具把所有 ABI 的运行库放进 base,AAB 文件可能显著增加,但单设备只获得匹配 ABI 的部分;若一份公共 DEX 载荷进入 base,它更可能影响每类设备。仅查看压缩包总字节无法解释这些差别。
动态交付还改变时间维度。安装时模块、按需模块和条件模块不会在同一时刻下载,首装下载与后续功能下载应分别记录。工程判断是为每个代表设备建立 initialModuleSet 与 optionalModuleSet,并让加固前后使用相同交付条件,防止把功能模块差异算成保护增量。
| 位置 | 可能交付范围 | 测量重点 | 误判风险 |
|---|---|---|---|
| base DEX | 所有基础安装 | 每设备公共增量 | 只看总 AAB 稀释影响 |
| base Native | 按 ABI 选择 | 各架构库字节 | 把全部 ABI 相加给单设备 |
| install-time feature | 满足条件的首装 | 条件和设备规格 | 忽略首装模块差异 |
| on-demand feature | 用户请求后下载 | 后续下载字节 | 计入所有用户首装 |
| asset pack | 按交付模式获取 | 资产模块和压缩口径 | 与代码加固增量混合 |
用 bundletool 为代表设备生成可复现 APK set
Build and test Android App Bundles 说明可使用 bundletool 从 AAB 生成 APK set,并按设备规格测试交付组合。device spec 应记录支持的 ABI、语言、屏幕密度和 SDK 等选择条件。对照实验必须让基线与加固候选使用同一 bundletool 版本和同一规格文件,否则工具或选择条件变化会污染差值。
bundletool 文档覆盖 build-apks、extract-apks、install-apks 和 get-size 等工作流。测量时可以先生成各代表设备的 `.apks` 归档,再统计其中 APK 条目和模块。若命令未显式提供发布签名,工具可能使用调试签名;这种输出适合受控诊断,不是商业发布对象,也不能证明线上商店会交付相同文件。
本地派生提供可复现近似值,仍不等于真实应用商店下载。服务端版本、签名、压缩、设备定位和动态模块状态可能不同。需要精确线上结论时,应保留测试轨道或发布控制台的同候选回执,并明确统计口径。没有这些证据时,报告只能写本地派生字节。
| 记录项 | 示例含义 | 一致性要求 | 失败条件 |
|---|---|---|---|
| bundletool 摘要 | 固定工具 JAR | 两候选同版本 | 工具版本漂移 |
| device spec 摘要 | ABI、语言、密度、SDK | 两候选同规格 | 规格文件不同 |
| AAB 摘要 | 基线或加固输入 | 对应受控候选 | 文件身份不明 |
| APK set 摘要 | 派生输出归档 | 每次生成可追溯 | 输出被覆盖 |
| 签名说明 | 诊断或发布阶段 | 同阶段比较 | 调试对象冒充发布 |
压缩字节、归档字节与真实下载不能混算
APK 与 `.apks` 都是归档格式,条目的未压缩大小、归档内压缩大小和外层文件大小不同。新增的高熵 Native 载荷与可压缩文本可能有相同原始字节,却产生不同传输增量。对比时必须使用同一算法和工具,报告 rawBytes、archiveBytes 与口径名称,不能只写“增加了多少”。
设备 APK set 内还可能包含 split APK、独立 APK 和元数据。外层 `.apks` 归档大小包含打包开销,不应直接称为应用商店下载大小;把各 APK 条目的文件字节相加更接近设备所需文件集合,但仍未覆盖分发端压缩、补丁下载与服务端处理。工程判断是称其为派生集合字节。
版本更新与首次安装也要分开。已有用户可能获得差分更新,实际传输受旧版本、分发策略和文件变化影响;新用户接收完整所需集合。若没有商店侧同版本对照,不应根据本地 APK 差值预测更新流量,更不能编造下载转化或用户流失结论。
| 字段 | 计算方式 | 适用结论 | 禁止命名 |
|---|---|---|---|
| raw entry bytes | 条目未压缩大小之和 | 内容规模变化 | 用户下载量 |
| APK file bytes | 派生 APK 文件大小之和 | 设备集合文件变化 | Play 精确下载 |
| .apks archive bytes | 外层归档文件大小 | 本地生成物变化 | 安装后占用 |
| 控制台下载估算 | 分发平台统计口径 | 同平台同条件比较 | 所有渠道通用值 |
| 更新传输字节 | 旧新版本与分发策略共同决定 | 特定升级路径 | 首次安装大小 |
安装后占用要分开代码、Native、编译产物和业务数据
安装占用不是下载文件解压后的简单总和。设备可能保留 APK、split、提取后的 Native 库、运行时编译产物、profile、数据库、缓存和下载资源。Android 版本、安装器、ABI 与应用配置都会影响结果。加固评估应在固定设备和干净状态下记录安装完成后的初始占用。
测试步骤需要定义清楚:卸载或清理到约定状态,安装精确候选,等待相同的包管理与编译阶段,保持相同网络和登录条件,再读取代码、数据与缓存分类。若应用首次启动会下载模型或资源,应分别记录安装未启动、首次启动完成和业务数据生成后的状态。
同一设备上的基线与加固候选还要控制签名和 applicationId。无法共存时应串行安装并恢复相同快照;可以共存时,包名差异也可能改变配置和服务端行为。安装占用结果只证明该设备状态下的文件规模,不代表所有系统和厂商实现。
| 层次 | 包含内容 | 采集时点 | 解释边界 |
|---|---|---|---|
| 安装代码 | APK、split 与相关文件 | 安装稳定后 | 受安装器和系统影响 |
| Native 提取 | 架构匹配的 SO | 安装或首次加载后 | 取决于打包和系统策略 |
| 运行时编译 | 优化与 profile 产物 | 约定等待阶段后 | 不同系统可能不同 |
| 应用数据 | 数据库、配置和账号状态 | 初始化完成后 | 不是纯加固增量 |
| 缓存与下载 | 临时文件、模型和资源 | 业务路径后 | 应与首装指标分开 |
设备矩阵和线上信号只用于校准,不用于编造影响
Test Lab available devices 说明可用设备目录、稳定性和容量会变化。代表设备选择应记录实际型号、系统、ABI、语言和屏幕配置,而不是只写高端机或低端机。云设备可帮助覆盖组合,但可用并不意味着覆盖目标业务的所有厂商功能和分发渠道。
Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号,可帮助判断哪些设备类别值得优先测量。它不是体积测量工具,也不能从包体变化直接推导质量指标。Play 样本还受安装来源、用户同意和统计口径影响,非 Play 渠道需要独立数据。
NIST SP 800-218 SSDF 提倡保留来源、构建、验证和变更证据,并管理供应链风险。体积验收可以把基线与候选、工具、设备规格、派生输出、测量脚本和批准结果放入发布证据包。SSDF 不定义软件加固应增加多少字节,也不证明某种保护效果。
- 代表设备来自真实目标分布或明确假设
- 记录型号、系统、ABI、语言和密度
- 云设备可用性由当次回执确认
- 线上质量信号与体积指标分开
- 非 Play 渠道不套用 Play 样本
- 发布证据绑定候选和工具摘要
- 没有真实数据不预测转化或流失
用可复现脚本生成设备 APK set 并拆解字节
下面的 Python 示例接收 bundletool JAR、基线 AAB、加固 AAB、device spec 和空输出目录。它为两份候选分别执行 build-apks,再统计 AAB 文件、`.apks` 归档以及归档内 APK 条目的文件字节和压缩字节。命令使用参数数组,不执行 shell 拼接,也不接触密钥。
脚本要求输入文件存在、输出目录尚未存在,任一 bundletool 调用失败就返回非零状态。结果只描述同一工具和设备规格下的诊断派生对象。由于没有传入商业签名材料,输出不能作为发布候选;归档内字节也不能命名为应用商店真实下载大小。
准备软件加固体积评估时,可整理可比基线与加固候选、文件摘要、AAB 模块、代表 device spec、APK set、安装占用和分发平台回执,再通过御盾中央平台提交申请。体积门禁通过只说明指标在批准边界内,不证明下载转化、启动性能或保护强度。
- 基线与加固 AAB 来自同一可比构建
- bundletool 与 device spec 完全一致
- 输出目录不覆盖既有证据
- AAB、APK set 和条目字节分别命名
- 模块字节用于解释增量来源
- 诊断签名对象不冒充发布候选
- 本地派生结果不冒充商店下载回执
from pathlib import Path
import hashlib
import json
import subprocess
import sys
import zipfile
if len(sys.argv) != 6:
raise SystemExit(2)
bundletool_jar = Path(sys.argv[1])
baseline_aab = Path(sys.argv[2])
hardened_aab = Path(sys.argv[3])
device_spec = Path(sys.argv[4])
output_dir = Path(sys.argv[5])
inputs = [bundletool_jar, baseline_aab, hardened_aab, device_spec]
if any(not path.is_file() for path in inputs) or output_dir.exists():
raise SystemExit(2)
output_dir.mkdir(parents=True)
def build_and_measure(label, aab_path):
apks_path = output_dir / f"{label}.apks"
command = ["java", "-jar", str(bundletool_jar), "build-apks", f"--bundle={aab_path}", f"--output={apks_path}", f"--device-spec={device_spec}", "--mode=default"]
completed = subprocess.run(command, check=False, capture_output=True, text=True)
if completed.returncode != 0 or not apks_path.is_file():
raise SystemExit(3)
apk_file_bytes = 0
apk_compressed_bytes = 0
modules = {}
with zipfile.ZipFile(apks_path) as archive:
for entry in archive.infolist():
if entry.is_dir() or not entry.filename.endswith(".apk"):
continue
apk_file_bytes += entry.file_size
apk_compressed_bytes += entry.compress_size
filename = Path(entry.filename).name
module = filename.split("-", 1)[0].split(".", 1)[0]
modules[module] = modules.get(module, 0) + entry.file_size
if apk_file_bytes == 0:
raise SystemExit(3)
aab_digest = hashlib.sha256()
with aab_path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
aab_digest.update(chunk)
return {"aabSha256": aab_digest.hexdigest(), "aabBytes": aab_path.stat().st_size, "apksArchiveBytes": apks_path.stat().st_size, "apkEntryFileBytes": apk_file_bytes, "apkEntryCompressedBytes": apk_compressed_bytes, "moduleFileBytes": dict(sorted(modules.items()))}
baseline = build_and_measure("baseline", baseline_aab)
hardened = build_and_measure("hardened", hardened_aab)
delta = {key: hardened[key] - baseline[key] for key in ["aabBytes", "apksArchiveBytes", "apkEntryFileBytes", "apkEntryCompressedBytes"]}
report = {"metricBoundary": "local diagnostic APK set, not store download", "deviceSpec": str(device_spec), "baseline": baseline, "hardened": hardened, "deltaBytes": delta}
print(json.dumps(report, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| AAB 由 base、feature、配置和资产模块组成,设备安装的是按配置派生的 APK 集。 | Android App Bundle format 描述 Bundle 模块与设备派生交付结构。 | 完整 AAB 不是设备直接安装对象,AAB 文件大小不能直接称为设备下载大小。 |
| bundletool 与测试轨道可以生成和验证 AAB 派生 APK 的设备交付组合。 | Build and test Android App Bundles 说明本地和测试轨道的 Bundle 测试流程。 | 本地派生结果不能完全替代 Play 线上交付和真实传输回执。 |
| bundletool 可以从 AAB 生成、检查和部署按设备配置选择的 APK set。 | bundletool 文档描述 build-apks、extract-apks、install-apks 和 get-size 等命令。 | 未显式提供发布签名时产生的调试签名对象不能作为商业发布候选。 |
| 云测试设备目录、稳定性和容量会变化,设备计划应保留实际型号与系统回执。 | Test Lab available devices 描述可用 Android 设备、型号和容量状态。 | 设备可用不代表覆盖目标业务的全部厂商特性、渠道和用户分布。 |
| Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号。 | Android vitals 描述 Play 质量指标及其监测范围。 | 这些信号不是体积测量值,且样本受安装来源、用户同意和统计口径限制。 |
| 安全发布应保留来源、构建、验证和变更证据,并管理供应链风险。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践。 | SSDF 不定义软件加固产品功能、允许体积增量或具体保护效果。 |
| 加固体积必须拆成上传、设备派生、传输和安装占用等独立口径。 | 工程判断:不同阶段的文件集合、压缩方式和设备处理不同,单一数字无法解释用户成本。 | 拆分口径提高可比性,仍需应用商店和真实设备回执才能形成线上结论。 |
| 任何体积差异都应绑定同源码基线、构建配置、模块集合和候选摘要。 | 工程判断:业务、依赖、R8、签名或模块变化会与加固增量混杂。 | 可比候选只能隔离主要变量,不能单独证明下载转化、启动性能或保护强度。 |
工程常见问题
直接比较加固前后的 AAB 文件大小可以吗?
可以作为上传产物指标,但不能当作设备下载大小。还要按相同 device spec 比较派生 APK 集、模块以及真实分发口径。
为什么 universal APK 不适合代表所有用户下载?
通用 APK 往往包含多种 ABI、语言和密度资源,应用商店会按设备选择拆分内容,单设备实际获得的集合通常不同。
bundletool 生成的 `.apks` 文件大小就是 Play 下载大小吗?
不是。它是本地归档并含打包开销,可用于同条件比较;Play 仍可能采用不同签名、压缩、服务端处理和动态交付状态。
安装占用为何可能大于下载文件?
设备还可能保存 split、提取 Native 库、运行时编译产物、数据库、缓存和下载资源,需要按安装、首次启动和业务数据阶段分开采集。
包体增大是否一定导致下载转化下降?
不能这样推导。转化受渠道、网络、用户需求和统计口径等多种因素影响,没有同版本真实数据时不得给出比例或因果结论。
申请软件加固体积评估前应准备哪些材料?
准备可比基线与加固 AAB、候选摘要、模块表、device spec、派生 APK set、安装占用和分发平台回执,再从御盾中央平台提交申请。