先看结论与判断条件
- 兼容性开关是诊断变量,不是产品修复;最终候选必须在计划支持的系统、targetSdk 和默认平台行为下重新通过验收。
- 每轮只改变一个 Compat Change ID,并固定候选摘要、设备身份、应用数据、输入步骤、网络条件和观察窗口。
- Android 16 同时存在与 targetSdk 相关的行为变化和面向所有应用的行为变化,定位前要先把故障放入正确类别。
- 原包与加固包、开关开启与关闭应组成正交矩阵,避免把重新构建、签名差异或测试数据漂移误判为加固回归。
- ApplicationExitInfo、ANR trace、Native tombstone 和业务断言要绑定同一候选与复现时间窗,退出原因不能脱离路径解释。
- 设备通过 CTS 不能代替第三方应用的加固验收,单一设备的 instrumented test 结果也不能外推完整 API、ABI 和厂商矩阵。
兼容性开关的正确用途是隔离变量,不是绕过变化
Android 兼容框架把部分平台行为变化表示为可识别的 Change ID,并允许开发测试阶段按应用调整状态。遇到加固后启动失败、组件行为异常或后台任务改变时,可以在同一候选上切换一个相关变更,观察故障是否随状态稳定翻转。这个结果只是因果线索,不是根因结论。
如果关闭某个变更后问题消失,至少存在三种解释:应用本身未适配新平台语义,加固过程改变了触发条件,或者测试环境与应用状态没有真正保持一致。若直接把关闭开关当作修复,发布时仍会面对系统默认行为、targetSdk 升级和设备厂商实现差异,问题只是被延后。
定位报告应写成可复核实验,而不是一句“兼容开关有效”。至少记录 Change ID、默认状态、强制状态、targetSdk、系统构建、应用候选摘要、签名证书摘要、复现步骤、观察结果和恢复动作。完成定位后要撤销实验覆盖,回到目标默认状态执行最终回归。
| 观察 | 可以说明 | 不能说明 | 下一步 |
|---|---|---|---|
| 状态翻转且结果稳定翻转 | 变更与故障高度相关 | 加固一定是根因 | 比较原包和加固包 |
| 两种状态都失败 | 该变更不足以解释故障 | 平台完全无关 | 检查其他变量和退出证据 |
| 两种状态都通过 | 当前路径未复现 | 其他设备也兼容 | 核对步骤和覆盖矩阵 |
| 结果随机波动 | 实验控制不足或存在竞态 | 可以凭多数放行 | 固定状态并增加诊断证据 |
| 仅原包或加固包失败 | 候选差异需要检查 | 保护机制已经被证实 | 核对构建、签名与处理差异 |
先建立故障假设,再选择具体 Change ID
不应从完整 Change ID 列表逐个试开关。先根据可观察症状建立假设:组件无法启动时检查生命周期、后台启动或 Intent 相关变化;文件与网络异常检查权限、存储和安全行为;大屏布局或输入问题检查窗口与界面变化;Native 退出则先确认信号、加载阶段和 ABI。假设决定实验范围。
Android 16 compatibility framework 提供具体变更的 ID、默认启用条件和调试入口。工程人员要把文档描述与实际调用路径连接,只有当业务路径使用了相关平台能力时才纳入实验。搜索结果、日志关键词相似或模型推测都不能替代调用链与平台文档的对应关系。
一个实验轮次只切换一个 Change ID。若同时关闭多个变化后故障消失,无法区分单一原因、组合影响还是环境重置带来的偶然结果。确需验证交互时,应先获得每个单变量结果,再建立明确的组合实验,并保持候选文件、设备快照和测试输入不变。
| 症状层 | 先收集 | 筛选依据 | 排除条件 |
|---|---|---|---|
| 启动失败 | 退出原因、首个组件、时间线 | 生命周期和启动行为 | 候选身份不同 |
| 权限异常 | 授权状态、调用 API、targetSdk | 权限与安全行为变化 | 测试数据未重置 |
| 后台任务 | 调度请求、进程状态、约束 | 调度和后台限制 | 网络条件漂移 |
| 界面问题 | 窗口尺寸、旋转、输入状态 | 大屏和窗口行为 | 资源包不一致 |
| Native 退出 | 信号、tombstone、加载模块 | 运行时与内存相关变化 | ABI 或 SO 集合不同 |
用四格矩阵分开平台变化与加固差异
最小有用矩阵包含两个候选和两个平台状态:未加固基线包与加固候选包,指定 Change ID 的 enabled 与 disabled。四格必须使用同一源码基线、相同业务配置、相同 targetSdk、同类签名阶段和同一设备状态。若原包与加固包来自不同提交,任何差异都无法只归因于处理过程。
解读时先看平台维度。若原包和加固包都只在 enabled 状态失败,平台行为变化更可能暴露了共同应用缺陷;若只有加固包随状态翻转,才需要继续检查保护范围、生成桥、反射、异常处理和启动顺序。即便如此,也不能仅凭四格矩阵声称加固是唯一根因。
再看候选维度。两个文件要分别保存 SHA-256、versionCode、applicationId、证书摘要、构建 variant、R8 mapping 摘要和加固配置摘要。测试人员不能用文件名或下载位置识别候选。重新签名、重新构建或重新加固都会产生新对象,旧实验回执不得复制。
| 基线 disabled | 基线 enabled | 加固 disabled | 加固 enabled | 工程判断 |
|---|---|---|---|---|
| 通过 | 失败 | 通过 | 失败 | 优先检查共同平台适配 |
| 通过 | 通过 | 通过 | 失败 | 检查加固与新行为交互 |
| 通过 | 失败 | 通过 | 通过 | 核对路径和处理后语义差异 |
| 失败 | 失败 | 通过 | 通过 | 基线或环境异常,不能评价加固 |
| 波动 | 波动 | 波动 | 波动 | 实验不可判定,先修复控制条件 |
区分 targetSdk 行为变化与面向所有应用的变化
Android 16 target behavior changes 描述以 targetSdk 36 为条件的一组行为变化,涉及大屏、权限、调度和安全等领域。测试应用若尚未以该目标版本构建,某些变化可能默认不生效;升级 targetSdk 后,系统行为才进入新的默认状态。报告必须记录编译产物中的真实 targetSdk,不能只引用项目计划。
Android 16 all-app behavior changes 列出的部分变化不依赖 targetSdk,旧目标版本应用在 Android 16 上也可能受影响。因此“没有升级 targetSdk”不能作为跳过新系统回归的理由。故障分层时要标注变更属于 target-gated、all-app 还是仍未确认,避免选择错误对照。
targetSdk 自身也不能在同一单变量实验中来回改变,因为它通常需要重新构建候选并可能触发多项行为变化。合理顺序是先在同一候选上使用兼容开关隔离某个 Change ID,再以目标 targetSdk 生成正式候选,撤销覆盖并执行完整回归。两个阶段的结论和文件摘要要分开保存。
| 变量 | 能影响什么 | 实验中如何控制 | 常见误判 |
|---|---|---|---|
| 系统版本 | 平台实现和默认行为 | 固定构建与设备 | 只写 Android 大版本 |
| targetSdk | 目标版本门控行为 | 从候选读取并固定 | 用项目配置代替产物事实 |
| Change ID | 特定兼容行为状态 | 每轮只切换一个 | 同时关闭多项 |
| 应用候选 | 代码、资源、签名与加固结果 | 以摘要锁定 | 用文件名识别 |
| 应用数据 | 权限、缓存与迁移状态 | 每格按规则重置 | 把残留状态当平台差异 |
CTS 说明设备兼容,不替代应用加固验收
Android CTS overview 说明 CTS 用于验证设备实现与 Android 兼容性定义的一致性。设备或系统镜像通过相关兼容测试,有助于减少明显的平台实现偏差,但它并不执行某个第三方应用的业务流程,也不知道 VMP 选择器、初始化逻辑、反射入口和异常边界。
所以“设备通过 CTS”与“加固应用兼容”属于不同证据层。前者主要回答设备是否符合平台兼容要求,后者要回答特定候选在指定设备、系统、ABI 和业务输入下是否保持语义。厂商系统即使具备合规基础,仍可能因资源压力、驱动、预装组件或策略差异暴露应用问题。
工程上可以把 CTS 状态作为设备实验元数据,而不是放行凭证。若问题只在某一设备出现,应同时保留系统构建指纹摘要、设备型号、API、ABI、内存状态和退出记录,再寻找同 API 的第二设备做对照。没有设备实现证据时,结论应保持为待定位,而不是猜测厂商缺陷。
| 证据 | 主要对象 | 可以回答 | 不能回答 |
|---|---|---|---|
| CTS 状态 | 设备或系统实现 | 平台兼容基础 | 特定应用业务通过 |
| 静态候选扫描 | APK、DEX 与 SO | 目标是否存在 | 运行语义一致 |
| instrumented test | 应用与 Android 运行时 | 指定路径结果 | 完整设备矩阵 |
| 退出历史 | 进程终止事件 | 原因与诊断材料 | 单独确定业务根因 |
| 兼容开关矩阵 | 特定平台变化 | 相关性和复现条件 | 长期修复已完成 |
设备测试要固定路径、状态和可观察结果
Android instrumented tests 可在真实 Android 环境中访问组件和系统 API,适合验证 Application、Activity、Service、Provider、Binder、ClassLoader 与 Keystore 等路径。加固回归定位应把失败路径写成确定步骤,并为输出、异常、组件状态和允许副作用建立断言,不能只观察界面是否出现。
每个四格实验都应从一致状态开始。权限、账号、数据库版本、缓存、网络响应和后台限制都会影响结果;是否清数据要由用例定义,而不是遇到失败才临时处理。测试日志必须包含 caseId、candidateSha256、changeId、overrideState、targetSdk、deviceIdHash、startedAt 和 resultSignature。
单一设备上的稳定翻转能提高定位价值,却不能覆盖 API、ABI、厂商系统和硬件差异。完成单变量定位后,应在支持矩阵中选择代表性设备撤销开关覆盖,运行相同契约和关键业务路径。只有绑定最终候选的真实结果才能进入兼容验收。
| 状态 | 记录方式 | 漂移后果 | 控制动作 |
|---|---|---|---|
| 候选文件 | SHA-256 与证书摘要 | 处理差异混入平台实验 | 每格安装前核对 |
| 应用数据 | 快照或重置策略 | 权限和迁移影响结果 | 按 caseId 初始化 |
| 设备系统 | 构建与设备身份摘要 | 系统更新改变行为 | 冻结实验设备 |
| 业务输入 | 版本化数据与步骤 | 不同分支被触发 | 自动化输入和断言 |
| 网络与时间 | 响应集与观察窗 | 超时被误判为平台变化 | 使用受控测试环境 |
用 ApplicationExitInfo 把退出事件连接到同一轮实验
Android ApplicationExitInfo 提供历史进程退出信息,包括退出原因、状态和可用诊断材料;文档还说明特定新版本可返回 Native tombstone。它适合补充启动闪退、ANR、系统终止和 Native 退出的事实,但记录必须和应用版本、进程、复现时间窗及用户路径关联。
不要看到某个 exit reason 就直接写根因。低内存终止、ANR、Java 异常和 Native 信号分别需要结合内存状态、trace、堆栈、tombstone、系统日志与业务断言。加固后堆栈可能需要相应 mapping 或符号材料才能解释,脱离候选身份的旧诊断文件不能用于新包。
对照实验可以为每一格计算 resultSignature,例如规范化业务断言、退出原因、异常类型和关键状态后取摘要。相同签名说明可观察结果相近,不代表内部路径完全一致;签名不同则提示进一步比较诊断材料。敏感日志、客户数据和原始 tombstone 应留在受控位置。
- 退出记录匹配包名、进程和时间窗
- 候选摘要与诊断材料一同保存
- ANR trace 和 Native tombstone 分开解释
- 退出原因不直接等同业务根因
- 敏感原始材料进入受控归档
- resultSignature 算法固定且可复现
- 旧候选诊断不复制到新候选
用只读实验台账检查配对、身份和结果翻转
下面的 Python 示例读取兼容实验记录,校验每条记录包含 changeId、overrideState、candidateRole、candidateSha256、targetSdk、deviceIdHash、caseId 与 resultSignature。它要求每个候选对同一 Change ID 同时存在 enabled 和 disabled 记录,再输出结果是否随状态翻转。脚本不执行设备命令,也不改变平台配置。
记录按 deviceIdHash、caseId、changeId 和 candidateRole 分组。若候选摘要、targetSdk 或结果字段非法,脚本返回非零状态;缺少配对状态同样失败。报告分别列出基线包和加固包的 disabled 与 enabled 签名,供工程人员判断平台相关性,而不自动宣称任何一方是根因。
准备 Android 加固兼容定位时,可整理原包与加固包、候选摘要、targetSdk、Change ID、设备状态、instrumented test、退出信息和恢复默认状态的最终回执,再通过御盾中央平台提交申请。兼容开关实验通过只代表定位记录完整,不证明正式候选已通过全部兼容测试。
- 每组只改变一个 Change ID 状态
- enabled 与 disabled 绑定同一候选摘要
- targetSdk 和设备身份保持一致
- 基线包与加固包分别成对
- resultSignature 来自规范化可观察结果
- 脚本不把相关性写成根因
- 实验后撤销覆盖并执行正式回归
from pathlib import Path
import json
import re
import sys
if len(sys.argv) != 2:
raise SystemExit(2)
input_path = Path(sys.argv[1])
if not input_path.is_file():
raise SystemExit(2)
records = json.loads(input_path.read_text(encoding="utf-8"))
if not isinstance(records, list) or not records:
raise SystemExit(2)
required = ["changeId", "overrideState", "candidateRole", "candidateSha256", "targetSdk", "deviceIdHash", "caseId", "resultSignature"]
sha_pattern = re.compile(r"^[0-9a-f]{64}$")
allowed_states = {"enabled", "disabled"}
allowed_roles = {"baseline", "hardened"}
groups = {}
for row in records:
if any(field not in row for field in required):
raise SystemExit(2)
if row["overrideState"] not in allowed_states or row["candidateRole"] not in allowed_roles:
raise SystemExit(2)
if not str(row["changeId"]).isdigit() or int(row["targetSdk"]) < 1:
raise SystemExit(2)
for field in ["candidateSha256", "deviceIdHash", "resultSignature"]:
if not sha_pattern.fullmatch(str(row[field]).lower()):
raise SystemExit(2)
key = (row["deviceIdHash"], row["caseId"], str(row["changeId"]), row["candidateRole"])
state_map = groups.setdefault(key, {})
if row["overrideState"] in state_map:
raise SystemExit(2)
state_map[row["overrideState"]] = row
reports = []
for key, state_map in sorted(groups.items()):
if set(state_map) != allowed_states:
raise SystemExit(2)
disabled = state_map["disabled"]
enabled = state_map["enabled"]
if disabled["candidateSha256"] != enabled["candidateSha256"] or disabled["targetSdk"] != enabled["targetSdk"]:
raise SystemExit(2)
reports.append({"deviceIdHash": key[0], "caseId": key[1], "changeId": key[2], "candidateRole": key[3], "resultFlipped": disabled["resultSignature"] != enabled["resultSignature"], "disabledSignature": disabled["resultSignature"], "enabledSignature": enabled["resultSignature"]})
print(json.dumps({"status": "experiment-pairs-valid", "reports": reports}, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 兼容框架可按 Change ID 辅助隔离 Android 16 平台行为变化。 | Android 16 compatibility framework 列出兼容变更、启用条件和调试方式。 | 兼容开关用于开发定位,不能替代目标默认状态下的最终验收。 |
| 以 targetSdk 36 为目标会触发一组 Android 16 平台行为变化。 | Android 16 target behavior changes 描述面向目标版本应用的大屏、权限、调度和安全变化。 | 具体影响要按候选真实 targetSdk 与功能路径筛选,文档列表也可能更新。 |
| 部分 Android 16 行为变化面向所有应用,不依赖 targetSdk 升级才出现。 | Android 16 all-app behavior changes 描述所有应用需要关注的平台变化。 | 平台变化不意味着每个加固应用都会触发同一故障,仍需真实路径复现。 |
| CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | Android CTS overview 描述兼容性测试套件的目标和设备验证角色。 | 设备通过 CTS 不代表第三方应用加固后的业务路径已经兼容。 |
| 依赖真实 Android 运行时、组件和系统 API 的语义可以通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在 Android 设备环境执行。 | 单一设备通过不能代表完整 API、ABI、系统版本和厂商矩阵。 |
| 进程退出历史可以提供退出原因、ANR trace,并在适用版本提供 Native tombstone。 | Android ApplicationExitInfo 描述历史退出信息及相关诊断数据。 | 退出记录仍需关联同一候选、进程、时间窗和用户路径,不能单独确定根因。 |
| 原包与加固包、Change enabled 与 disabled 应组成四格单变量矩阵。 | 工程判断:正交对照能减少构建、候选、平台状态和测试数据互相混淆。 | 矩阵只提高相关性判断,不能排除所有隐藏变量或证明唯一根因。 |
| 关闭兼容性变更不能作为加固应用的长期修复或发布验收结论。 | 工程判断:正式交付必须面对计划系统与 targetSdk 下的平台默认行为。 | 临时关闭可用于诊断,最终处理仍需代码、配置或保护边界修正及真实回归。 |
工程常见问题
关闭某个 Compat Change 后加固包恢复正常,能确认是加固问题吗?
不能直接确认。还要比较同源码原包、核对候选身份,并排除应用自身未适配、数据状态和环境漂移,四格矩阵只能提供相关性证据。
可以一次关闭多个兼容性变更来加快定位吗?
不建议作为首轮方法。多个变量同时变化会破坏因果判断,应先逐个获得稳定结果,再针对明确交互建立组合实验。
应用没有升级 targetSdk,还需要测试 Android 16 吗?
需要。Android 16 有面向所有应用的行为变化;同时也要区分哪些变化受 targetSdk 门控,不能把两类结论混合。
设备通过 CTS 是否说明加固应用兼容?
不能。CTS 验证设备实现的兼容基础,应用仍需在真实 Android 环境对特定候选、业务路径、异常和副作用执行独立回归。
ApplicationExitInfo 能直接给出闪退根因吗?
通常不能单独给出。退出原因和诊断材料要与同一候选、复现时间窗、业务断言、堆栈、trace 或 tombstone 联合解释。
申请兼容回归定位前应准备哪些材料?
准备原包和加固包、摘要、targetSdk、Change ID、设备系统、四格结果、instrumented test、退出证据和恢复默认状态的回执,再从御盾中央平台提交申请。