先看结论与判断条件

  • 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设备配置派生物理论选择哪些 APKPlay 线上相同
测试轨道平台交付结果商店返回哪些对象所有设备覆盖
设备安装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 或仅靠运行时字符串维持关系,门禁应要求构建与设备证据,不能凭编译成功放行。

Bundle 模块角色与审计重点
模块类型主要内容交付判断审计重点
base基础代码、资源、Manifest始终作为基础公共组件与依赖
install-time feature安装时功能首次安装包含条件与 fusing
conditional feature条件功能满足设备条件条件覆盖矩阵
on-demand feature按需功能运行后请求未安装分支
configuration APKABI、语言、密度按设备选择配置完整性
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路由条件深链冲突匹配矩阵
providerAuthorityProvider 标识跨模块重复安装与访问测试

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 测试轨道与真实设备工件及安装回执比对。

想用自己的 App 验证?

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

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