先看结论与判断条件
- AAB 是发布输入,设备实际安装的是按配置派生的 APK 集,因此最终 Manifest 结论必须绑定具体设备配置。
- 安装时、条件和按需模块拥有不同生命周期,同一组件不能假设在所有设备首次启动时都存在。
- 每个交付模块要记录条件、依赖、fusing、split 名称、最终组件和对应 APK 摘要,缺失依赖应失败关闭。
- bundletool 适合生成和检查 APK set,但签名参数与工件角色必须明确,默认调试签名不能冒充生产候选。
- 本地模拟只能覆盖客户端流程,真实下载、安装、失败和商店派生结果仍需测试轨道与设备回执。
- Manifest 静态声明可以核对组件、权限和 intent filter,不能证明运行时访问控制或模块加载后的加固强度。
直接答案:以设备派生 APK 集为最终 Manifest 验收对象
动态功能的源码模块 Manifest 只是构建输入。Android App Bundle 会把 base、feature、配置和资产模块组织成发布包,分发服务再按设备能力与交付条件生成 APK 集。相同 AAB 对不同语言、ABI、密度、系统版本和条件可能产生不同工件,审计必须写清 deviceSpec 与实际选择结果。
验收流程先冻结 AAB SHA-256、构建工具与模块图,再为代表设备生成 APK set,列出将安装的 base、配置和 feature APK。对每个 feature 核对 deliveryType、条件、依赖、fusing、split 名称、组件、权限和摘要。未交付模块的组件不能进入该设备已安装能力清单。
本地 bundletool 结果仍不是线上交付结论。未显式签名时工具可能使用调试签名,Play 服务器也可能执行平台侧处理。发布报告把 local-derived、test-track-observed 和 device-installed 分成不同证据等级,只有真实工件和设备回执存在时才登记对应通过。
| 证据层 | 对象 | 回答的问题 | 不能单独证明 |
|---|---|---|---|
| 源码模块 | Gradle 与 Manifest | 声明意图是什么 | 设备一定收到 |
| AAB | 冻结发布输入 | 模块如何打包 | 可直接安装 |
| APK set | 设备配置派生物 | 理论选择哪些 APK | Play 线上相同 |
| 测试轨道 | 平台交付结果 | 商店返回哪些对象 | 所有设备覆盖 |
| 设备安装 | Package Manager 状态 | 模块是否真实存在 | 业务授权正确 |
| 运行验证 | 组件与流程 | 加载后是否可用 | 所有攻击被阻断 |
先画模块图:base、feature、配置和资产不能混成文件列表
App Bundle format 区分 base、feature、配置和资产模块。base 提供应用基础代码与资源,feature 可以依赖 base 或其他允许关系,配置 APK 承载 ABI、密度或语言差异。审计表必须保留 moduleType 和 parent,不用文件名猜测模块角色,也不把资产包误当成可执行组件。
组件归属必须明确。Activity、Service、Receiver、Provider、权限和 metadata 可能在 feature Manifest 中声明,只有相应 split 安装后才进入设备能力。base 如果通过反射或类名提前引用按需模块,首次启动可能出现类缺失;代码审查需要把可达性与交付时机对应。
资源依赖与代码依赖分别检查。feature 使用 base 资源属于常见路径,但另一个未交付 feature 的类或资源不能被默认认为存在。模块图若出现未知依赖、循环、缺失 parent 或仅靠运行时字符串维持关系,门禁应要求构建与设备证据,不能凭编译成功放行。
| 模块类型 | 主要内容 | 交付判断 | 审计重点 |
|---|---|---|---|
| base | 基础代码、资源、Manifest | 始终作为基础 | 公共组件与依赖 |
| install-time feature | 安装时功能 | 首次安装包含 | 条件与 fusing |
| conditional feature | 条件功能 | 满足设备条件 | 条件覆盖矩阵 |
| on-demand feature | 按需功能 | 运行后请求 | 未安装分支 |
| configuration APK | ABI、语言、密度 | 按设备选择 | 配置完整性 |
| asset pack | 大型资源 | 按模式交付 | 代码不可错误引用 |
安装时、条件和按需交付必须使用不同验收路径
Play Feature Delivery 定义安装时、条件和按需等生命周期。安装时模块可与首次安装一起交付,条件模块依赖国家、设备特性或其他配置,按需模块在运行后由应用请求。测试计划不能用一个模块存在用例覆盖三种路径,每种路径的初始状态和失败点都不同。
安装时路径检查首次启动前工件集合、组件注册和依赖。条件路径至少选择满足与不满足条件的 device spec,确认模块只在预期配置出现。按需路径要覆盖未安装 UI、请求开始、下载、确认、安装、取消、失败、重试以及更新后的状态恢复。
业务代码必须把 moduleAvailable 当成运行状态,而不是编译常量。按需模块未到达时不实例化其类、不访问其资源,也不暴露只能由模块实现的组件入口。服务端不能因为客户端声称模块已安装就放宽权限,模块交付与账号授权仍是不同边界。
| 交付类型 | 初始状态 | 关键失败 | 真实回执 |
|---|---|---|---|
| install-time | 首次安装前 | APK 缺失或依赖断裂 | 安装与首次启动 |
| conditional-yes | 条件满足 | 条件表达错误 | 派生集包含模块 |
| conditional-no | 条件不满足 | 模块意外交付 | 派生集不包含模块 |
| on-demand | 模块未安装 | 提前类加载 | 安全未安装界面 |
| on-demand install | 请求下载 | 取消、网络、空间 | 商店安装状态 |
| on-demand update | 已有旧模块 | 版本集合混搭 | 同版本与 signer |
交付条件要转成代表设备矩阵,而不是停留在 XML
条件声明只有转成设备配置才能验证。团队先列出支持的 API、ABI、语言、密度、国家和硬件特性,再用边界值生成 deviceSpec。每条条件至少包含匹配、临界和不匹配用例,记录模块选择原因,避免只测试开发者手中的一台高配设备后就宣告覆盖完成。
矩阵需要控制组合爆炸。优先覆盖条件边界、主流设备、最低系统、最高系统和历史故障配置,配置 APK 的 ABI 与语言另行做成对覆盖。未测试组合标为 not-tested,不用相邻配置通过自动填充,也不虚构全设备兼容。
条件如果来自运行时服务或账号,不要误写成 Bundle 静态交付条件。静态设备条件决定派生 APK,账号资格决定业务入口,两者必须在数据模型和报告中分开。否则会出现模块已交付但账号无权使用,或账号有权但设备从未收到模块的混乱状态。
fusing 与旧系统路径必须明确记录,不凭通用包推断
动态 feature 的 fusing 配置会影响特定旧系统或通用 APK 路径是否把模块合并进基础工件。验收不能只检查现代 split 安装,还要根据产品 minSdk 与实际分发模式核对旧路径。fusing 值、模块条件和生成结果必须并列保存,不用源码默认值代替最终结果。
通用 APK 方便本地测试,但不等于设备定向 Play 交付。若 universal 构建包含更多 feature,测试通过可能掩盖真实设备缺失模块;若排除某些模块,也不能据此判断线上一定排除。报告必须标注 buildMode,禁止把 universal、device-specific 和 store-delivered 结果混为一项。
最低系统路径还涉及组件是否在 base 或 fused Manifest 中可见、资源是否合并和启动是否提前引用模块代码。门禁使用实际生成 APK 提取最终 Manifest 并检查类可达性,再在真实最低系统上安装。没有设备证据时只登记静态派生结果。
最终 Manifest 要核对组件、权限、intent filter 与元数据
Android app manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。对选中的每个 APK,审计器应提取最终合并 Manifest,不只读取源码 XML。组件记录包括完全限定名、exported、permission、process、enabled 和所属模块,便于发现合并规则或条件交付造成的变化。
同名组件跨模块出现时,不能简单集合去重。需要确认最终安装集合是否产生重复、属性冲突或覆盖,尤其是 Provider authorities、deep link 和自定义 permission。某模块未交付时,base 的 intent filter 不应把请求路由到不存在实现;交付后也不应意外扩大导出面。
静态 Manifest 通过不证明运行时授权正确。exported、permission 和组件禁用状态只是平台入口控制的一部分,业务代码仍要验证用户、对象和数据。本文只验收声明与交付一致性,不把它扩写为组件篡改或运行时鉴权的第二套答案。
| 字段 | 为何记录 | 冲突示例 | 后续验证 |
|---|---|---|---|
| componentName | 唯一标识实现 | 跨模块重复 | 解析最终类 |
| module | 定位交付归属 | 未交付却被引用 | 设备安装状态 |
| exported | 外部可达面 | 合并后变化 | 组件调用测试 |
| permission | 入口权限 | 属性被覆盖 | 授权拒绝用例 |
| intentFilter | 路由条件 | 深链冲突 | 匹配矩阵 |
| providerAuthority | Provider 标识 | 跨模块重复 | 安装与访问测试 |
bundletool 本地派生要固定版本、签名与 device spec
bundletool 可以从 AAB 生成、检查和部署 APK set。流水线要固定工具版本和摘要,保存 build-apks 命令、device spec、AAB SHA-256、签名角色和输出 APKS 摘要。未显式提供生产签名信息时可能产生调试签名,因此本地工件只能按其实际角色登记。
每次生成后列出 APK set 元数据、模块名、split ID、targeting、文件摘要和 signer,再用 extract-apks 或 install-apks 对代表配置验证。不要只保留命令 exit 0;必须检查选择结果是否符合计划,尤其是条件模块、配置 APK 和 feature 依赖。
Build and test Android App Bundles 支持本地重现与测试轨道验证。线上测试轨道用于观察 Play 真实处理和设备交付,本地生成用于快速诊断,两者相互补充。任何差异都保持 blocked 并记录平台、工具和配置,不修改预期去迎合单次结果。
用结构化投影检查交付模块、组件和依赖
下面的 Python 示例读取 bundletool 或构建流水线导出的公开 JSON 投影,包含 deviceSpec、modules、selectedModules、dependencies 和 finalComponents。它检查 base 必选、模块唯一、所有已选模块依赖已选、未选模块组件未进入最终清单,以及组件名和归属完整。
脚本不解包生产工件、不执行安装、不读取签名密钥。输入派生的缺失依赖、未知模块、重复组件或交付泄漏会生成错误并非零退出。真实项目可以先用 bundletool 与 Manifest 解析器生成投影,再用这段逻辑作为可复核门禁。
通过只说明投影内部一致,不证明投影来自真实 AAB、签名正确或 Play 线上选择相同。下游应把投影、AAB、APKS、device spec 和工具摘要绑定,并追加测试轨道与设备回执;任何输入变化都必须重新生成。
import json
import sys
from collections import Counter
from pathlib import Path
if len(sys.argv) != 2:
raise SystemExit("usage: audit_feature_delivery.py delivery-projection.json")
projection_path = Path(sys.argv[1]).resolve()
if not projection_path.is_file():
raise SystemExit(f"projection missing: {projection_path.name}")
with projection_path.open("r", encoding="utf-8") as stream:
projection = json.load(stream)
if not isinstance(projection, dict):
raise SystemExit("projection root must be an object")
modules = projection.get("modules")
selected = projection.get("selectedModules")
final_components = projection.get("finalComponents")
if not isinstance(modules, list) or not isinstance(selected, list):
raise SystemExit("modules and selectedModules must be arrays")
if not isinstance(final_components, list):
raise SystemExit("finalComponents must be an array")
by_name = {item.get("name"): item for item in modules if isinstance(item, dict)}
if len(by_name) != len(modules) or "base" not in by_name:
raise SystemExit("module names must be unique and include base")
selected_set = set(selected)
if "base" not in selected_set or not selected_set <= set(by_name):
raise SystemExit("selected modules must include base and known modules")
errors = []
for module_name in sorted(selected_set):
dependencies = by_name[module_name].get("dependencies", [])
if not isinstance(dependencies, list):
errors.append(f"invalid dependency list for {module_name}")
continue
missing = sorted(set(dependencies) - selected_set)
if missing:
errors.append(f"{module_name} misses dependencies {missing}")
component_names = []
for component in final_components:
if not isinstance(component, dict):
errors.append("component entry is not an object")
continue
name = component.get("name")
owner = component.get("module")
if not name or owner not in selected_set:
errors.append(f"component {name!r} has an invalid owner")
component_names.append(name)
duplicates = sorted(name for name, count in Counter(component_names).items() if name and count > 1)
if duplicates:
errors.append(f"duplicate final components: {duplicates}")
if errors:
print(json.dumps({"status": "failed", "errors": errors}, ensure_ascii=False, indent=2))
raise SystemExit("dynamic feature delivery projection is inconsistent")
print(json.dumps({
"status": "passed",
"device_spec": projection.get("deviceSpec", {}),
"selected_modules": sorted(selected_set),
"component_count": len(final_components),
"next_gate": "bind the projection to AAB/APKS hashes and test-track delivery",
}, ensure_ascii=False, indent=2))报告要按设备配置列出实际交付与证据缺口
最终证据包包含 AAB 摘要、构建与 bundletool 版本、模块图、delivery 与 fusing 声明、device specs、APKS 摘要、selectedModules、最终 Manifest、组件差异、签名回执、测试轨道、设备安装和按需失败恢复。每项都要绑定同一 release。
可以陈述某个 device spec 的本地派生结果或某台设备的测试轨道观察,不能写成所有设备得到相同模块、所有按需流程兼容或模块已被正确保护。没有项目证据时,不提供客户、性能、攻击阻断、排名或收录数字。
组件外露问题可继续查看[Manifest 导出组件篡改审查](/zh-cn/articles/manifest-exported-component-tamper-review/),本篇只负责交付条件与最终声明。需要提交 AAB 和设备矩阵,可从[御盾中央平台申请加固服务](https://www.leonadev.com/console/)。登录、注册、价格、购买和控制台统一由中央平台承接。
- AAB、APKS 和每个派生 APK 都有不可变摘要
- 安装时、条件和按需模块使用不同测试路径
- 代表 device spec 覆盖条件匹配、边界和不匹配
- selected module 的全部依赖进入同一安装集合
- fusing 与旧系统或 universal 路径按实际配置验证
- 最终组件、权限、intent filter 和 metadata 从派生 APK 提取
- bundletool 本地、测试轨道和设备安装证据分层登记
- 未测试配置与线上差异保持显式 blocked 或 not-tested
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 动态功能具有安装时、条件和按需交付生命周期,代码不一定随 base 存在。 | Play Feature Delivery 说明动态 feature 的交付模式和生命周期。 | 交付生命周期不保证模块加载后已被正确保护。 |
| 按需模块需要覆盖下载、安装、失败和本地模拟路径。 | On-demand feature delivery testing 说明按需测试与本地模拟。 | FakeSplitInstallManager 不能替代真实商店交付验证。 |
| AAB 由 base、feature、配置和资产模块组成,设备安装派生 APK 集。 | Android App Bundle format 说明 Bundle 文件结构与模块角色。 | AAB 本身不是设备上直接安装的最终 APK。 |
| bundletool 可从 AAB 生成、检查和部署按设备配置选择的 APK set。 | bundletool 说明 build-apks、device spec、检查和部署能力。 | 未显式签名时调试签名不能作为发布候选。 |
| bundletool 与测试轨道可重现和验证 AAB 派生 APK 交付。 | Build and test Android App Bundles 说明本地与 Play 测试流程。 | 本地生成不能完全替代 Play 线上交付回执。 |
| Manifest 定义组件、权限、intent filter、SDK 约束和元数据。 | Android app manifest 说明 Manifest 元素与平台职责。 | 静态清单不能证明运行时访问控制没有被业务代码放宽。 |
| 最终 Manifest 验收必须绑定 device spec 与 selectedModules。 | 工程判断:交付条件和配置拆分使不同设备获得不同 APK 集。 | 本地投影仍需 AAB、APKS、签名和线上回执绑定。 |
| selected feature 依赖缺失或未选模块组件泄漏应失败关闭。 | 工程判断:这会造成不可达代码、类缺失或声明范围错误。 | 结构异常不等于已在所有设备复现,仍需实际安装归因。 |
| 真实 Play 派生与按需安装必须通过测试轨道和设备取证。 | 项目证据尚未接入:这是上线前门禁,不表示当前候选已完成。 | 本地 exit 0 和模拟器流程不能登记线上交付通过。 |
| 本文不提供客户、性能、兼容、排名、收录或攻击阻断结论。 | 项目证据尚未接入:缺少真实 AAB、轨道和设备回执。 | 文章只给出可复核交付审计方法。 |
工程常见问题
为什么不能只审动态模块源码 Manifest?
设备安装的是按配置派生 APK 集,模块可能未交付或合并结果变化,必须审最终 Manifest。
安装时模块是否一定出现在所有设备?
不一定。还要考虑条件、fusing、系统路径和实际分发模式,并按 device spec 验证。
按需模块用 FakeSplitInstallManager 通过是否足够?
不足。它适合客户端流程测试,真实商店下载、安装、失败和设备状态仍需测试轨道。
bundletool 生成 APKS 就是生产候选吗?
不一定。签名参数与工件角色必须明确,默认调试签名或本地派生不能冒充线上交付。
一个 device spec 通过能否代表全部用户?
不能。至少覆盖条件边界、ABI、语言、密度、最低最高系统和历史风险配置。
未交付模块的组件可以留在最终清单吗?
不应作为该设备已安装能力出现;发现归属不一致需阻止发布并调查合并与投影来源。
最终 Manifest 通过是否证明组件授权安全?
不能。静态声明只证明配置,运行时主体、对象权限和业务校验还需独立测试。
怎样证明线上和本地派生一致?
绑定 AAB、APKS、device spec 和工具摘要,再用 Play 测试轨道与真实设备工件及安装回执比对。