先看结论与判断条件
- 旧系统在 MultiDex.install 完成前只能可靠访问主 DEX 可见类,初始化顺序是首要证据。
- ContentProvider 可能早于 Application.onCreate 运行,提前引用的类必须纳入启动边界。
- 最终候选、主 DEX 清单、R8 mapping 与变体要同源,不能用后续构建报告解释线上包。
- 首次安装、冷启动、进程重建、升级和低存储状态需要独立记录。
- 全量 keep 或把所有类塞进主 DEX 会改变问题形态,不能代替精确根因。
- 结论只覆盖指定 API、ABI、候选和入口,不回答多 DEX 中哪些方法应做 VMP。
先确认设备真的走旧版 MultiDex 路径
先把系统 API 与 MultiDex 安装实现写成可复核对象,而不是凭页面印象判断。SDK_INT、进程名、安装调用、异常时间和设备构建号应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,故障是否发生在 MultiDex.install 建立次级 DEX 前才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
从设备回执记录 API 和安装路径,不按市场占有率推测。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
设备 API 或进程身份没有记录时先标记 blocked,并把剩余未知写进报告。原生支持 multidex 的新系统行为不能替代旧系统回归。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 系统 API 与 MultiDex 安装实现身份 | SDK_INT、进程名、安装调用、异常时间和设备构建号 | 故障是否发生在 MultiDex.install 建立次级 DEX 前 | 设备 API 或进程身份没有记录则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 从设备回执记录 API 和安装路径,不按市场占有率推测 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 原生支持 multidex 的新系统行为不能替代旧系统回归 | 限制结论范围 | 不得扩大为全局结论 |
锁定最终候选与主 DEX 构建输出
候选、classes.dex 与主 DEX 类清单要从状态变化而不是最终页面开始核对。把APK SHA-256、variant、classes.dex 摘要、mapping 与主 DEX 报告按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断分析材料是否确实属于发生故障的最终候选发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
解包只读核对 DEX 清单,并与同次流水线旁车摘要绑定。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。主 DEX 报告来自另一个变体或重新构建说明链路仍缺少可归因证据;文件名和 versionName 不足以证明构建输出同源。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 候选、classes.dex 与主 DEX 类清单身份 | APK SHA-256、variant、classes.dex 摘要、mapping 与主 DEX 报告 | 分析材料是否确实属于发生故障的最终候选 | 主 DEX 报告来自另一个变体或重新构建则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 解包只读核对 DEX 清单,并与同次流水线旁车摘要绑定 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 文件名和 versionName 不足以证明构建输出同源 | 限制结论范围 | 不得扩大为全局结论 |
画出 Application 和 Provider 的启动顺序
处理Application.attachBaseContext、onCreate 和 Provider 初始化时,先固定应用身份、候选摘要、平台版本和可变条件。组件声明、initOrder、启动方法、首个类引用与安装完成标记必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪个组件在安装完成前触发了次级 DEX 类加载,否则重试成功也只能说明环境变了,不能支持技术结论。
按系统调用顺序列出 Provider 与 Application 的所有提前入口。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现Provider 在安装前访问次级 DEX 类,应回到上一份同源输入,比较首次分叉点并保留两侧回执。组件顺序受 Manifest 与系统实现影响,必须以目标候选和设备回执为准。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| Application.attachBaseContext、onCreate 和 Provider 初始化身份 | 组件声明、initOrder、启动方法、首个类引用与安装完成标记 | 哪个组件在安装完成前触发了次级 DEX 类加载 | Provider 在安装前访问次级 DEX 类则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 按系统调用顺序列出 Provider 与 Application 的所有提前入口 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 组件顺序受 Manifest 与系统实现影响,必须以目标候选和设备回执为准 | 限制结论范围 | 不得扩大为全局结论 |
区分首次安装、冷启动与进程重建
先把安装状态、进程状态与升级来源写成可复核对象,而不是凭页面印象判断。首次安装、清数据、强停、系统回收、升级和重启回执应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,问题只在首次状态还是所有进程生命周期出现才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
每种状态从干净前置条件开始,禁止覆盖上一轮失败现场。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
清数据后通过但进程重建仍失败时先标记 blocked,并把剩余未知写进报告。清数据会改变安装状态,必须登记为新用例。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
从 ClassNotFoundException 追到首个提前引用
缺失类、加载者、调用入口和异常 cause要从状态变化而不是最终页面开始核对。把异常类名、ClassLoader、线程、首个业务帧与原始堆栈按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断异常源于主 DEX 缺失、名称变化还是错误 ClassLoader发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
保留原始堆栈并沿首个业务帧反查组件和初始化路径。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。异常堆栈被聚合或缺少首个业务帧说明链路仍缺少可归因证据;ClassNotFoundException 也可能来自动态加载或插件,需核对 ClassLoader。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 缺失类、加载者、调用入口和异常 cause身份 | 异常类名、ClassLoader、线程、首个业务帧与原始堆栈 | 异常源于主 DEX 缺失、名称变化还是错误 ClassLoader | 异常堆栈被聚合或缺少首个业务帧则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 保留原始堆栈并沿首个业务帧反查组件和初始化路径 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | ClassNotFoundException 也可能来自动态加载或插件,需核对 ClassLoader | 限制结论范围 | 不得扩大为全局结论 |
检查 R8 keep 规则与主 DEX 规则边界
处理R8 可达性、keep 与主 DEX 规则时,先固定应用身份、候选摘要、平台版本和可变条件。规则来源、匹配类、优化结果、主 DEX 归属与构建警告必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论需要修正间接入口规则还是主 DEX 保留边界,否则重试成功也只能说明环境变了,不能支持技术结论。
从具体缺失类回推规则,不添加无边界全包 keep。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现用全量 keep 后问题消失却没有精确解释,应回到上一份同源输入,比较首次分叉点并保留两侧回执。keep 规则描述 R8 可达性,不决定 VMP 保护范围。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
在旧 API 真机上建立最小设备矩阵
先把旧 API、厂商系统、ABI 与存储状态写成可复核对象,而不是凭页面印象判断。API、型号、系统镜像、ABI、可用空间和重复次数应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,修复是否覆盖受支持旧系统而非只在模拟器通过才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
至少覆盖最低支持 API 的真实设备或可信实验室回执。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
只在高 API 模拟器完成验证时先标记 blocked,并把剩余未知写进报告。CTS 是平台兼容基础,不代替应用自身设备验收。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 旧 API、厂商系统、ABI 与存储状态身份 | API、型号、系统镜像、ABI、可用空间和重复次数 | 修复是否覆盖受支持旧系统而非只在模拟器通过 | 只在高 API 模拟器完成验证则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 至少覆盖最低支持 API 的真实设备或可信实验室回执 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | CTS 是平台兼容基础,不代替应用自身设备验收 | 限制结论范围 | 不得扩大为全局结论 |
用代码核对启动类与主 DEX 清单
公开构建报告、启动入口和设备用例要从状态变化而不是最终页面开始核对。把caseId、候选摘要、启动类、预期归属、观察归属与证据按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断启动前引用类是否全部进入同源主 DEX 证据发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
把脱敏类名和用例写成 JSON,由校验器拒绝错误通过。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。预期主 DEX 类观察为缺失却标记通过说明链路仍缺少可归因证据;代码只校验公开报告,不解密、不修改 APK,也不执行加载攻击。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
import hashlib
import json
import sys
from pathlib import Path
REQUIRED = set([
'caseId',
'candidateSha256',
'deviceApi',
'startupClass',
'expectedDex',
'observedDex',
'result',
'evidenceRef'
])
ALLOWED_RESULTS = set([
'pass',
'blocked',
'mismatch'
])
FORBIDDEN = {'token', 'password', 'privateKey', 'secret', 'customerData'}
def stop(message):
raise SystemExit(message)
def digest(path):
hasher = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
hasher.update(chunk)
return hasher.hexdigest()
def text(record, field, index):
value = str(record[field]).strip()
if not value:
stop(f'record {index} has empty {field}')
return value
def validate(record, index):
if not isinstance(record, dict):
stop(f'record {index} is not an object')
if FORBIDDEN.intersection(record):
stop(f'record {index} contains forbidden fields')
missing = sorted(REQUIRED - record.keys())
if missing:
raise SystemExit(f'record {index} lacks fields: {missing}')
for field in REQUIRED:
if field != 'deviceApi':
text(record, field, index)
result = text(record, 'result', index)
if result not in ALLOWED_RESULTS:
stop(f'record {index} has an unsupported result')
expected = text(record, 'expectedDex', index)
observed = text(record, 'observedDex', index)
evidence = text(record, 'evidenceRef', index)
if expected != observed and result == 'pass':
stop(f'record {index} passes despite a mismatch')
if expected != observed and not evidence:
stop(f'record {index} has a mismatch without evidence')
return text(record, 'caseId', index)
if len(sys.argv) != 2:
stop('usage: python validate_main_dex.py public-checks.json')
source = Path(sys.argv[1]).expanduser().resolve()
if not source.is_file():
stop('public check file is missing')
raw = source.read_bytes()
data = json.loads(raw.decode('utf-8'))
records = data.get('startupCases') if isinstance(data, dict) else None
if not isinstance(records, list) or not records:
raise SystemExit('startupCases must be a non-empty list')
identifiers = [validate(record, index) for index, record in enumerate(records)]
if len(identifiers) != len(set(identifiers)):
raise SystemExit('record identifiers are duplicated')
summary = {
'inputSha256': hashlib.sha256(raw).hexdigest(),
'records': len(records),
'results': {state: sum(1 for item in records if item['result'] == state) for state in sorted(ALLOWED_RESULTS)},
'status': 'pass',
}
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))按入口、设备和候选关闭回归
处理最终候选、入口覆盖与失败回执时,先固定应用身份、候选摘要、平台版本和可变条件。通过设备、阻塞设备、未测入口、owner 与复核状态必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪些设备入口通过、阻塞或尚无运行时证据,否则重试成功也只能说明环境变了,不能支持技术结论。
由兼容负责人逐项复核候选、设备、入口和回执。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现结论没有覆盖最低支持 API 或关键入口,应回到上一份同源输入,比较首次分叉点并保留两侧回执。未测设备保持未验证,不能因相邻 API 通过而推定通过。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 多 DEX 构建和旧系统加载路径可能把代码分散到不同 dex 产物。 | Android multidex:多 DEX 构建和旧系统加载路径可能把代码分散到不同 dex 产物。 | 启用 multidex 不代表保护清单已覆盖全部 DEX。 |
| R8 的职责包括代码和资源缩减、优化与名称混淆,发布构建需要保留对应规则和输出。 | Enable app optimization with R8:R8 的职责包括代码和资源缩减、优化与名称混淆,发布构建需要保留对应规则和输出。 | R8 的编译优化不等于 VMP,也不证明抗动态分析能力。 |
| 反射、JNI 和间接入口需要精确 keep 规则,宽泛规则会掩盖边界错误并削弱优化。 | R8 keep rules best practices:反射、JNI 和间接入口需要精确 keep 规则,宽泛规则会掩盖边界错误并削弱优化。 | keep 规则只描述 R8 可达性,不定义 VMP 的可保护范围。 |
| Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | Android app manifest:Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | 静态清单不能证明运行时访问控制没有被业务代码放宽。 |
| 依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。 | Android instrumented tests:依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵。 |
| CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | Android CTS overview:CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | 设备通过 CTS 不代表第三方 App 加固后的业务兼容通过。 |
| 旧系统首次启动必须把 MultiDex 安装前的所有类引用纳入主 DEX 边界。 | 工程判断:Provider 等组件可能早于 Application.onCreate 执行,提前引用次级 DEX 类会在安装完成前失败。 | 具体提前入口由目标 Manifest 和代码决定,不能套用固定类名单。 |
| 主 DEX 修复需要在最终候选和最低支持 API 上复验。 | 工程判断:构建报告只能说明产出,运行时回执才能确认真实加载顺序。 | 文章不声称任何设备已通过,也不讨论保护强度和覆盖比例。 |
工程常见问题
Android 5 以上正常,是否可以排除 MultiDex 问题?
不能。新系统原生支持多 DEX,旧系统可能依赖安装库。最低支持 API 的首次启动和进程重建需要单独验证。
把 MultiDex.install 放进 Application.onCreate 可以吗?
旧路径通常需要更早建立次级 DEX;还要考虑 Provider 提前初始化。应按官方集成方式和目标组件顺序核对。
ClassNotFoundException 一定表示类没进 APK 吗?
不一定。类可能在次级 DEX、被 R8 改名或移除,也可能由错误 ClassLoader 查找。要结合 DEX 清单、mapping 和原始堆栈判断。
全量 keep 能作为紧急修复吗?
它会改变优化和主 DEX 结果,只能作为受控诊断对照,不能替代精确规则、候选绑定和回归验收。
模拟器通过是否足够?
不足。至少要覆盖最低支持 API 的真实设备或可信云真机,并记录厂商系统、ABI、安装状态和候选摘要。
提交御盾诊断需要什么?
准备最终 APK 摘要、最低支持 API、异常原始栈、Manifest、Application 路径、Provider 顺序、主 DEX 报告、R8 mapping 和设备回执。