先看结论与判断条件
- Android 新版本同时包含适用于所有应用的变化与只在新 targetSdk 下启用的变化,必须用不同候选和用例隔离,不能把全部故障归给 VMP。
- VMP 清单应逐方法记录平台触点、线程、生命周期、权限、调度、窗口与 IPC 依赖;targetSdk 变化后按触点重新决定保留、排除或专项重验。
- 系统回调、兼容适配、反射与生成代码通常需要更透明的诊断路径,纯业务状态机也必须重新验证输入契约和异常语义。
- compatibility framework 的 change ID 可以帮助定位某项平台行为,但只作为诊断变量,最终验收必须在真实 targetSdk 和默认平台配置下完成。
- build type、flavor、source set、applicationId、依赖、签名和 targetSdk 共同形成候选身份,不能只修改一个数字后复用旧 APK 或旧保护报告。
- 最小测试梯度是新系统旧 target、未保护新 target、保护后新 target与最终签名候选,所有回执都绑定 APK 摘要、设备和 change 状态。
targetSdk 是平台行为输入,也是 VMP 评估输入
targetSdk 不只是商店提交字段,它会决定平台是否对应用启用一组目标版本行为。Android 16 target behavior changes 说明 targetSdk 36 会涉及大屏、权限、调度和安全等行为。若受保护方法直接处理系统回调、权限结果、窗口状态或调度事件,输入、调用时机和异常路径可能随 target 改变,旧清单的兼容结论就不再完整。
重审不等于把所有 VMP 方法全部删除。正确动作是先建立平台触点清单,再判断每个方法是否仍位于稳定的纯业务边界。系统适配器、生命周期入口、Binder 或 Intent 处理、权限桥、窗口与调度封装需要优先复核;只处理已规范化业务数据的纯函数也要重新跑等价测试,但通常更容易保持边界。
任何 targetSdk 升级都会生成新的发布候选身份。报告至少绑定源码提交、完整 variant、targetSdk、compileSdk、依赖、构建工具、VMP 清单、APK 摘要和签名证书。只把旧报告中的方法名复制到新构建,无法证明 R8 后方法身份、平台触点或保护结果仍相同。
| 对象 | 变化来源 | VMP 风险 | 重审动作 |
|---|---|---|---|
| 系统回调 | 平台调用时机与参数 | 异常或状态机变化 | 移到薄适配层并回归 |
| 权限路径 | 授权与返回行为 | 旧分支不再成立 | 重建正负用例 |
| 窗口和大屏 | 布局与生命周期 | 保护入口难诊断 | 保持 UI 胶水透明 |
| 调度任务 | 后台和任务规则 | 触发时机变化 | 设备端专项测试 |
| IPC/Intent | 平台安全边界 | 输入契约变化 | 重新规范化输入 |
| 纯业务核心 | 上游投影变化 | 输入语义漂移 | 绑定新契约重验 |
先区分所有应用变化和目标版本变化
Android 16 all-app behavior changes 说明部分平台变化不依赖 targetSdk,旧目标版本的应用在新系统上也可能受到影响。于是,某个旧 APK 在 Android 16 首次失败,并不能证明 targetSdk 升级或 VMP 造成问题。第一份必要回执是旧 target 候选在新系统上的运行结果,用于识别仅由操作系统升级触发的基线差异。
第二份回执使用相同业务代码和尽可能一致的 variant,将 targetSdk 升级但先不加入 VMP 或保持可诊断保护关闭。它与旧 target 在同一设备和用例上的差异,才更接近目标版本行为影响。第三份回执再启用新 VMP 清单;若前两层通过而第三层失败,才进入保护变换、目标选择和工具兼容调查。
这三个候选不是同一个发布物,必须分别计算摘要并保存配置。不能在运行时改一个开关后声称完成 target 对照,也不能把 debug 未保护包与 release 保护包直接比较而忽略其他 variant 差异。若无法生成控制变量清晰的候选,报告应说明混杂因素,不给出单一根因结论。
| 候选 | 系统 | target/VMP | 回答的问题 |
|---|---|---|---|
| A | 新系统 | 旧 target、原保护状态 | 是否属于 all-app 变化 |
| B | 新系统 | 新 target、未保护或可诊断 | target 行为是否触发 |
| C | 新系统 | 新 target、新 VMP 清单 | 保护是否引入差异 |
| D | 旧支持系统 | 新 target、新 VMP 清单 | 是否存在平台回归 |
| E | 目标矩阵 | 最终签名候选 | 发布候选真实结果 |
| 对照要求 | 相同设备类 | 尽量固定其他输入 | 减少混杂因素 |
把平台变化映射到方法,而不是把文档当测试清单
平台行为文档是筛选入口,不是项目已经触发的证明。团队应按应用实际功能过滤大屏、权限、调度、安全、媒体、存储、网络和其他类别,再为每项记录适用条件、受影响组件、调用方法和验证用例。没有使用某项平台能力时可以标记不适用,但需要代码或配置依据,不能只因测试没有崩溃就忽略。
方法触点清单应精确到 post-R8 可追踪身份或稳定 sourceIntentId,并记录 role、thread、callback、platformApis、reflection、JNI、generatedCode 和 currentDisposition。一个方法可能只间接接触平台,例如业务函数接收由权限桥生成的布尔值;这种间接依赖也要记录输入契约,否则平台变化会在业务层表现为看似随机的结果差异。
重验路径按依赖方向展开。先验证平台适配器把新系统输入规范化为旧业务契约,再验证业务核心在同一投影下保护前后等价。若契约本身必须改变,旧 VMP 基线失效,需要新的输入、输出和异常快照。不能要求 VMP 通过保持一个已经不符合平台规则的旧行为。
| 字段 | 内容 | 来源 | 停线条件 |
|---|---|---|---|
| changeId/category | 平台变化标识和类别 | 官方文档与兼容框架 | 未分类 |
| appliesWhen | target、系统和功能条件 | 候选配置 | 条件未知 |
| components | Activity、Service 等 | Manifest 与调用图 | 范围不完整 |
| methodIds | 受影响方法身份 | 构建方法清单 | 旧名称无法解析 |
| contract | 输入、输出和异常 | 测试与接口定义 | 契约未版本化 |
| disposition | 保留、排除或重验 | 审查决定 | 默认继承旧结论 |
平台适配器、系统回调和纯业务核心采用不同处置
Activity、Service、Receiver、ContentProvider、窗口回调、权限回调和任务入口属于平台适配层。它们承接 Android 对象、生命周期、线程和异常,targetSdk 升级后最需要透明诊断。通常应保持薄而不做大范围 VMP,只负责验证与投影,再把稳定业务数据交给后续函数。若旧清单直接保护这些入口,应优先评估移出。
反射、序列化、JNI、动态代理和生成代码也要重审。平台或依赖升级可能改变保留规则、方法签名和调用时机,VMP 工具对这些结构的支持需要当前候选证据。不能因为旧 target 设备曾通过就沿用。必要时把 VMP 边界移动到不依赖框架元数据的纯函数,并为边界前后的投影增加一致性校验。
纯业务核心并非自动安全。targetSdk 变化可能改变上游权限、调度或窗口路径,导致业务函数接收新的状态组合。它仍需使用新 target 的真实输入回放,检查正常、拒绝、超时、取消和恢复语义。通过后可以保留为 VMP 候选,但报告必须绑定新的方法摘要和设备回执。
| 角色 | 平台耦合 | 默认处置 | 重验重点 |
|---|---|---|---|
| 生命周期入口 | 高 | 排除或薄化 | 回调和恢复 |
| 权限/调度适配 | 高 | 保持透明 | 新平台语义 |
| 窗口和 UI 胶水 | 高 | 排除 | 大屏与布局行为 |
| 反射/JNI 桥 | 中到高 | 谨慎选择 | 签名、keep 与 ABI |
| 数据投影器 | 中 | 窄函数可评估 | 输入契约 |
| 纯业务状态机 | 低 | 优先保留候选 | 全状态回放 |
完整 variant 和最终候选身份必须重新绑定
Android build variants 由 build type、product flavor、source set、applicationId 和签名配置等组合形成。targetSdk 可能由共享配置、flavor、模块或构建脚本设置,升级时也常伴随依赖和工具链变化。对照构建必须记录完整 variant,不能只比较 targetSdk 数值,否则 source set、证书、资源或 SDK 变化会混入结果。
每个 VMP 方法要绑定新构建的 post-R8 身份、输入字节摘要、选择理由和变换报告。若 R8 或依赖升级导致方法被内联、重命名或删除,旧 target list 不能自动匹配名字相近的方法。未解析、skipped 和 failed 都要进入审查;只有逐条闭合且最终 APK 含有对应变换证据时,才能进入设备门禁。
候选链还要包含 targetSdk、compileSdk、minSdk、Gradle 与插件版本、依赖图、Manifest、VMP 配置、APK 摘要和证书摘要。升级期间若使用 debug 签名或临时配置做定位,应标记为诊断候选,不能转成生产通过。最终发布结论只引用签名候选和真实设备回执。
| 字段组 | 必须记录 | 用途 | 禁止复用 |
|---|---|---|---|
| SDK | min、target、compile | 平台条件 | 只写 target |
| variant | type、flavor、source set | 固定控制变量 | 用文件名猜测 |
| 依赖工具 | 解析图与版本 | 解释方法和行为变化 | 旧锁文件回执 |
| VMP | post-R8 目标与报告 | 确认保护对象 | 旧方法名清单 |
| 产物 | APK 摘要和证书 | 绑定最终候选 | 中间包结果 |
| 设备 | 系统、API、ABI、厂商 | 界定覆盖范围 | 单机外推 |
compatibility framework 只用于定位 change ID
Android 16 compatibility framework 可以按 change ID 辅助启用或禁用部分行为,用于判断某个失败是否与特定平台变化相关。诊断时应记录设备、APK、命令、change ID、修改前后状态和用例结果,并在隔离实验后恢复。compat flag 不是生产修复,也不能把最终 target 配置下的失败改成通过。
一次只改变一个可控变量。若同时切换多个 change ID、关闭 VMP、换 debug variant 和修改依赖,即使问题消失也无法确定原因。建议先在未保护新 target 候选上隔离平台 change,再在相同 change 状态下比较保护前后;最后恢复平台默认状态,使用最终保护候选完成验收。
有些 Android 16 行为适用于所有应用,不一定提供可用于绕开的目标开关;有些 target 行为也可能涉及多个组件。compatibility framework 结果只能作为归因线索。若 change 关闭后问题消失,仍需调整应用或保护边界,使它在默认启用状态下正确运行,而不是依赖测试设备上的临时覆盖。
| 字段 | 要求 | 用途 | 错误做法 |
|---|---|---|---|
| candidateSha256 | 确切 APK | 防止包错配 | 只记版本名 |
| changeId | 单项标识 | 隔离平台变化 | 一次切换多项 |
| before/after | 状态和结果 | 建立因果线索 | 只保存成功截图 |
| VMP state | 保护开关和清单摘要 | 区分变换影响 | 临时配置不记录 |
| restore | 恢复平台默认 | 最终验收准备 | 把 override 留在设备 |
| final gate | 默认状态真机结果 | 形成发布证据 | compat 关闭即通过 |
设备测试按最早失败层归因,不把平台变化算作保护故障
Android instrumented tests 适合验证依赖真实 Android 运行时、组件和系统 API 的语义。targetSdk 升级回归需要覆盖平台触点、业务核心和保护边界,记录正常、拒绝、权限变化、后台恢复、进程死亡、配置变化和异常路径。用例必须运行在当前候选的真实入口,不能只调用被保护纯函数后宣称整条平台链通过。
故障按最早失败层处理。旧 target 在新系统已经失败,先调查 all-app 行为;未保护新 target 失败,先调查 target 行为和应用适配;只有未保护新 target 通过、保护新 target 失败,才进入 VMP 目标、工具支持和保护前后异常语义。最终签名包失败则继续检查打包、签名和发布配置。
NIST SP 800-218 SSDF 强调来源、构建、验证和变更证据。每次修复都产生新 APK 和新回执,不能把旧结果改日期。发布报告列出已覆盖 API、ABI、厂商和 change 状态,以及明确未覆盖范围。没有真实项目数据时,只能给出测试设计和决策门槛,不写兼容率或通过结论。
- 旧 target 在新系统上的 all-app 基线已验证
- 未保护新 target 已隔离平台行为变化
- 保护后候选使用同一用例和平台状态比较
- compat change 只用于诊断且最终恢复默认
- 平台适配、业务核心和最终 APK 分层取证
- 所有结论绑定新候选摘要与设备覆盖范围
用范围差分脚本生成 targetSdk 升级后的重验路径
下面的 Python 示例读取旧、新两个候选清单。它要求 variantKey、applicationId 和签名证书保持可比较,新 targetSdk 必须升高,并检查 APK、VMP 清单和方法摘要格式。每个新平台 change 需要说明适用类型、受影响方法和证据状态;方法按 platform-callback、compat-adapter、generated、reflection-jni 或 business-core 分类,生成 remove-or-isolate、revalidate 或 retain-with-regression。
脚本不读取 Android 官方文档,也不执行 compat 命令或设备测试。behaviorChanges 必须由团队根据实际 target 和功能从官方资料整理,method 列表必须由当前构建自动导出。若为了通过脚本把未知影响写成 not-applicable,静态一致也没有证据价值。输出只是重验计划,不能登记兼容或 VMP 通过。
申请 targetSdk 升级后的商业加固复评时,可准备新旧 APK、完整 variant、SDK 与依赖差分、平台 change 清单、post-R8 方法身份、VMP 报告、compat 诊断和分层设备回执,再从御盾中央平台提交申请。真实候选和回执齐备前,不宣称新平台兼容、旧保护清单仍有效或任何攻击已被阻断。
- 新旧候选的完整 variant 与签名身份可比较
- behaviorChanges 来自当前 target 与实际功能筛选
- 方法清单由新构建 post-R8 产物自动导出
- 平台角色默认移出或隔离后专项重验
- 纯业务核心也绑定新摘要和输入契约回归
- 脚本输出只作为计划,设备通过另行取证
from pathlib import Path
import json
import re
import sys
DIGEST = re.compile(r"^[0-9a-f]{64}$")
PLATFORM_ROLES = {"platform-callback", "compat-adapter", "generated", "reflection-jni"}
ALLOWED_ROLES = PLATFORM_ROLES | {"business-core"}
def load_manifest(path_text):
path = Path(path_text)
if not path.is_file():
raise SystemExit(2)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
raise SystemExit(2)
if not isinstance(value, dict):
raise SystemExit(2)
return value
def valid_digest(value):
return isinstance(value, str) and DIGEST.fullmatch(value.lower()) is not None
def methods_by_id(document):
methods = document.get("vmpMethods")
if not isinstance(methods, list):
raise SystemExit(3)
result = {}
for method in methods:
required = ("sourceIntentId", "postR8Digest", "role")
if not isinstance(method, dict) or any(field not in method for field in required):
raise SystemExit(3)
method_id = method["sourceIntentId"]
if not isinstance(method_id, str) or method_id in result or not valid_digest(method["postR8Digest"]):
raise SystemExit(3)
if method["role"] not in ALLOWED_ROLES:
raise SystemExit(3)
result[method_id] = method
return result
if len(sys.argv) != 3:
raise SystemExit(2)
old = load_manifest(sys.argv[1])
new = load_manifest(sys.argv[2])
for field in ("variantKey", "applicationId", "signingCertificateSha256"):
if old.get(field) != new.get(field):
raise SystemExit(3)
if not valid_digest(old.get("apkSha256")) or not valid_digest(new.get("apkSha256")):
raise SystemExit(3)
if not isinstance(old.get("targetSdk"), int) or not isinstance(new.get("targetSdk"), int) or new["targetSdk"] <= old["targetSdk"]:
raise SystemExit(3)
old_methods = methods_by_id(old)
new_methods = methods_by_id(new)
changes = new.get("behaviorChanges")
if not isinstance(changes, list) or not changes:
raise SystemExit(4)
affected = set()
for change in changes:
required = ("changeId", "appliesTo", "affectedMethodIds", "evidenceStatus")
if not isinstance(change, dict) or any(field not in change for field in required):
raise SystemExit(4)
if change["appliesTo"] not in {"all-apps", "new-target"} or change["evidenceStatus"] not in {"applicable", "not-applicable-with-evidence"}:
raise SystemExit(4)
if not isinstance(change["affectedMethodIds"], list):
raise SystemExit(4)
for method_id in change["affectedMethodIds"]:
if method_id not in new_methods:
raise SystemExit(4)
affected.add(method_id)
plan = []
for method_id, method in sorted(new_methods.items()):
if method["role"] in PLATFORM_ROLES:
action = "remove-or-isolate"
elif method_id in affected or method_id not in old_methods or old_methods[method_id]["postR8Digest"] != method["postR8Digest"]:
action = "revalidate"
else:
action = "retain-with-regression"
plan.append({"sourceIntentId": method_id, "role": method["role"], "action": action})
if not plan or any(item["action"] == "remove-or-isolate" and new_methods[item["sourceIntentId"]].get("reviewStatus") != "pending" for item in plan):
raise SystemExit(5)
print(json.dumps({"status": "review-plan-only", "oldTargetSdk": old["targetSdk"], "newTargetSdk": new["targetSdk"], "plan": plan}, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| targetSdk 36 会改变大屏、权限、调度和安全等平台行为。 | Android 16 target behavior changes 列出以 Android 16 为目标的应用行为变化。 | 列表会随平台文档更新,必须按实际 targetSdk、应用功能和当前候选筛选。 |
| 部分 Android 16 行为变化不依赖 targetSdk,旧目标应用也需要新系统回归。 | Android 16 all-app behavior changes 列出适用于所有应用的平台变化。 | 平台变化不意味着所有加固应用都会触发同一问题,也不能直接归因到 VMP。 |
| compatibility framework 可以按 change ID 辅助隔离 Android 16 行为变化。 | Android 16 compatibility framework 描述兼容 change 的标识与调试方式。 | compat flag 只用于定位,不能替代最终 targetSdk 和默认平台状态下的验收。 |
| build type、product flavor、source set、applicationId 和签名配置能够组成不同发布变体。 | Android build variants 描述 variant 的组成和相关构建配置。 | 同一仓库不代表新旧 target 候选具有相同 SDK、依赖、资源、证书和运行行为。 |
| 依赖真实 Android 运行时、组件和系统 API 的语义需要设备端测试。 | Android instrumented tests 说明设备测试用于验证依赖 Android 运行环境的行为。 | 单一设备通过不能代表完整 API、ABI、厂商和平台 change 矩阵。 |
| 安全发布流程应保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 描述安全软件开发中的来源、构建、验证和变更实践。 | SSDF 是组织级框架,不定义具体 VMP 工具、targetSdk 适配或御盾产品功能。 |
| 应使用新系统旧 target、未保护新 target 和保护后新 target 三层候选隔离根因。 | 工程判断:三个控制变量分别暴露 all-app、target 行为和 VMP 变换差异。 | 其他 variant、依赖和签名差异仍需固定;无法固定时只能报告混杂因素。 |
| 平台适配器和系统回调通常应移出宽 VMP 边界,纯业务核心也需重新绑定并回归。 | 工程判断:平台角色需要透明诊断,纯业务函数仍可能接收由新平台行为改变的输入投影。 | 最终范围由当前工具支持、调用图、性能和真实设备证据决定,不能直接外推。 |
工程常见问题
targetSdk 只改一个数字,为什么 VMP 清单不能直接复用?
targetSdk 会改变平台行为,升级也可能伴随 variant、依赖、R8 和方法身份变化。旧清单没有绑定新候选、post-R8 摘要和设备回执,不能继承通过结论。
应用在 Android 16 失败,是否说明 targetSdk 或 VMP 有问题?
不能直接说明。部分 Android 16 变化适用于所有应用。先测试旧 target 候选,再测试未保护新 target,最后比较保护后候选,按最早失败层归因。
compat change 关闭后问题消失,是否可以这样发布?
不应把诊断 override 当作最终方案。它只帮助定位 change ID,应用仍需在真实 targetSdk 和平台默认状态下修复并完成最终候选验收。
哪些 VMP 方法在 targetSdk 升级后最需要重审?
优先检查生命周期入口、权限和调度适配、窗口与 UI 胶水、IPC、反射、JNI 和生成代码。纯业务核心也要用新输入契约重跑保护前后测试。
新 target 的未保护包通过,能否证明 VMP 包一定兼容?
不能。它只隔离了平台适配层。还需要对保护后候选使用同一用例和平台状态回归,并最终在签名 APK 和目标设备矩阵上验证。
提交 targetSdk 升级加固复评需要哪些资料?
准备新旧 APK、完整 variant、SDK 与依赖差分、平台 change 清单、post-R8 方法、VMP 报告、compat 诊断和分层设备回执,再通过御盾中央平台提交申请。