先看结论与判断条件
- synthetic 与 bridge 是方法来源和适配角色的线索,不是保护价值、兼容性或防护强度的结论。
- VMP 选择应从手写业务入口向编译产物追踪,记录源码符号、字节码描述符、调用者和最终构建身份。
- 只保护桥接方法可能遗漏真正实现,只保护实现方法也可能漏掉反射或框架实际调用的稳定入口。
- 协程状态机、Lambda 适配器和 Compose 生成函数需要按语义责任分类,不能因数量多或名称复杂而批量处理。
- R8 会改变可达性、名称和方法形态,保护清单必须针对最终候选构建核对,不能复用源码阶段猜测。
- 放行结论需要绑定最终包、映射文件、方法清单和设备端回归;静态标志扫描只能形成待审列表。
先给结论:合成标志不能替代业务判断
是否把一个方法纳入 VMP,首先取决于它是否承载可被滥用的业务决策,而不是它有没有 synthetic 或 bridge 标志。编译器为了泛型擦除、语言互操作、Lambda、协程和界面编译生成辅助方法,这些方法可能只是参数搬运,也可能成为框架稳定调用入口。正确做法是先找到手写的授权、计费、协议或关键算法入口,再沿最终字节码调用关系判断合成层承担了什么责任。
synthetic 通常表示源码中没有一一对应声明,bridge 通常表示编译器为方法签名兼容创建转发入口。两个标志都只能说明来源,不能证明方法没有价值,也不能证明它适合某种变换。一个桥接方法可能只有一次类型转换和委派,另一个合成方法却可能保存协程状态或封装关键参数。审查必须阅读描述符、调用者、被调方法和异常路径,不能按标志直接批量包含或排除。
工程上可把结论分成保护、保留入口、明确排除和人工复核四类。保护类要求方法本身有不可替代的关键分支;保留入口类要求它维持反射、框架或接口调用契约,但核心逻辑在别处;明确排除类是纯生成胶水且变动频繁;人工复核类是调用链尚不完整或无法建立回归的对象。这样的分类能解释每个选择,也能在编译器或依赖升级后重新计算。
| 观察对象 | 常见职责 | 初始处置 | 还需补充的证据 |
|---|---|---|---|
| Java bridge | 签名适配与委派 | 保留入口或人工复核 | 实际调用者与真实实现 |
| Kotlin synthetic | 默认参数、访问器或语言胶水 | 按调用链分类 | 源码映射与描述符 |
| 协程状态机 | 保存挂起点与局部状态 | 单独评估 | 取消、异常和恢复契约 |
| Lambda 适配器 | 封装函数对象与回调 | 通常不批量保护 | 最终业务动作位置 |
| Compose 生成路径 | 重组、跳过与参数跟踪 | 通常排除生成层 | 手写状态变换入口 |
| 手写决策函数 | 授权、计费或协议判断 | 优先候选 | 输入输出与失败回归 |
先看最终字节码,不要从源码名称猜测
源码视角常把一个函数理解为单一入口,最终类文件却可能出现多个描述符不同的方法。泛型擦除、协变返回、默认参数、访问控制和函数对象转换都可能增加编译器生成入口。VMP 规则若只保存类名与短方法名,遇到重载或生成方法时就可能命中错误对象。方法清单至少需要记录 owner、name、descriptor、access flags 和所在构建变体,并保留它与源码入口的关系。
Kotlin 编译产物还会受编译器版本、目标 JVM、协程实现和插件配置影响。同一段源码在工具链升级后,生成方法数量、名称和委派结构可能发生变化。这里不能根据一次反编译截图制定长期规则。团队需要对正式构建产物生成结构化方法索引,再把索引与版本化保护清单比较,发现原目标消失、描述符变化或新增合成入口时阻止静默放行。
R8 会执行缩减、优化和名称混淆,并可能改变方法是否存在及其最终身份。Android 官方文档将 R8 的职责描述为优化构建过程的一部分,但这不等于 VMP,也不保证某个源码方法仍以原形态存在。保护规则的核对点应放在实际进入后续处理的候选产物上,同时保存 mapping 与配置摘要,才能解释为什么某条规则命中、失效或指向另一个重载。
| 阶段 | 关键字段 | 主要风险 | 门禁动作 |
|---|---|---|---|
| 源码 | 文件、符号、业务资产 | 同名方法被混为一类 | 登记资产所有者 |
| 编译后 | owner、name、descriptor、flags | 生成入口遗漏 | 建立方法索引 |
| R8 后 | mapping、可达性、最终描述符 | 目标被删除或改名 | 逐条重绑定 |
| VMP 配置 | 包含、排除、理由、版本 | 规则过宽或过期 | 拒绝无理由规则 |
| 最终候选 | 包摘要、变体、工具版本 | 证据与包错配 | 绑定不可变身份 |
| 回归结果 | 用例、设备、结果、日志 | 静态命中替代语义验证 | 按边界放行 |
批量保护 synthetic 方法为什么容易失去控制
第一类问题是保护对象与资产价值错位。生成方法数量可能远多于手写关键函数,批量纳入会让配置看起来覆盖广,却不能回答授权判断、价格计算或协议签名到底落在哪个方法。生成胶水若只转发参数,保护它只增加维护变量;真正的业务实现仍可能保持原样。范围评审应要求每个候选能回指一个业务资产和一个可观察的输入输出契约。
第二类问题是框架契约变得难以诊断。反射、依赖注入、序列化、协程调度和界面编译都可能依赖稳定的方法形态、元数据或回调约定。Kotlin 反射文档说明运行时发现依赖类、成员和相关元数据,但具体框架的调用方式仍需项目核对。如果生成入口被改变后出现查找失败,团队必须能区分 keep 规则、名称变化、签名变化和 VMP 范围,而不是把所有异常归为兼容问题。
第三类问题是升级成本不可预测。编译器插件或依赖版本变化后,旧的合成方法可能消失,新的方法可能出现,名称也可能不稳定。按包名或标志全选会把这种漂移自动带入发布,导致每次构建的保护面悄然变化。更稳妥的门禁是固定工具链、保存方法索引差异,并要求新增合成对象只有在明确业务责任和回归路径后才能从复核状态进入保护状态。
从手写业务入口反向建立所有权
清单建立应从业务资产开始,而不是从反编译工具的 synthetic 过滤器开始。先列出必须留在客户端且被修改会造成直接损失的规则,例如离线状态转换、请求规范化或本地算法控制,再记录对应源码入口和公开契约。随后在最终构建中定位真实描述符,向上查找框架入口,向下查找被调实现,形成一条可解释的责任链。
每条责任链至少回答五个问题:谁调用这个方法,输入从哪里来,方法是否改变业务状态,异常或取消如何传播,绕过该入口后是否仍能到达同一实现。如果合成方法只有参数转换并立即委派,它更像需要保留的入口;如果它保存关键状态并包含不可替代分支,则可进入人工复核。判断必须来自类文件、调用图和测试,而不能只看反编译后的伪源码是否复杂。
所有权还要处理一对多和多对一关系。一个手写 Lambda 可能生成类与适配方法,一个接口实现可能产生桥接入口,一个业务函数也可能被 R8 内联到多个调用者。清单不应强迫每个源码符号只对应一个字节码方法,而应允许记录主实现、入口集合和替代路径。只要存在未评估的替代路径,就不能声称关键逻辑已经完整纳入保护。
| 判定问题 | 可核对材料 | 通过条件 | 不能据此声称 |
|---|---|---|---|
| 是否承载业务决策 | 源码评审与状态变更 | 存在明确资产和规则 | 已经具备防护强度 |
| 是否为稳定入口 | 调用图与反射清单 | 真实调用者可追踪 | 入口不能被绕过 |
| 是否仅做委派 | 字节码与被调关系 | 没有独立关键分支 | 可以随意删除 |
| 是否存在替代路径 | 重载、接口与内联结果 | 全部路径已登记 | 覆盖所有运行情况 |
| 是否可独立回归 | 确定性输入与失败用例 | 结果和异常可比较 | 所有设备均兼容 |
| 是否适合保护 | 风险、边界与证据汇总 | 理由可审计且可重验 | 攻击一定失败 |
Java bridge 方法要同时核对入口与真实实现
bridge 方法经常承担签名适配:调用方按接口或父类签名进入,桥接入口完成类型转换后委派到具体实现。只把 bridge 加入保护,真正承载业务分支的实现可能仍未进入范围;只处理实现而忽略 bridge,也可能破坏框架、反射或旧调用方依赖的入口。审查结果通常不是二选一,而是把 bridge 标记为契约入口,把具体实现标记为资产候选,再分别决定保留方式和回归责任。
描述符是区分重载与桥接关系的关键字段。方法名相同并不代表调用契约相同,参数和返回类型变化会影响调用解析、类型转换与异常位置。保护清单应记录完整 JVM descriptor,并在 R8 后重新绑定。若配置语法无法唯一表达描述符,应当生成工具可接受的精确规则或直接阻止发布,不能靠扩大类级范围掩盖歧义。
反射路径需要单独列出。Kotlin 反射依赖运行时可发现的成员与元数据,Java 框架也可能按接口签名或注解定位方法。keep 规则的作用是维持 R8 可达性和名称约束,不等于 VMP 范围;VMP 范围也不能修复错误的 keep 配置。两类规则需要共享同一方法身份清单,但必须分别记录责任和失败现象。
Kotlin 状态机、Lambda 与 Compose 要按语义责任拆分
协程会把挂起函数编译为包含 Continuation 交互和状态恢复的结构。这里的重点不是重复讨论协程状态机是否能被处理,而是确认合成层是否成为业务资产的唯一承载点。Kotlin 协程指南把挂起、调度、结构化并发、取消和异常作为整体语义;任何范围决策都要覆盖成功、挂起恢复、取消和异常路径,单看一个 invokeSuspend 方法的静态形态不足以放行。
Lambda 与回调适配器常把手写函数对象接入 Java 接口、事件循环或框架回调。适配器可能只捕获变量并调用目标,也可能加入线程切换、默认值或异常转换。审查时要记录捕获字段、目标方法、调用线程和失败出口。若业务决策仍在目标方法,优先保护稳定的手写入口;若适配器确实加入关键分支,则为该分支建立独立契约后再评估,不能按类名模式一网打尽。
Compose 编译器插件会生成与重组、参数变化和跳过策略相关的调用形态,并与 Kotlin 版本和构建插件耦合。界面生成路径通常不应被当作核心商业算法,真正值得审查的是手写状态变换、权益判断和请求构造。把大量 Compose 生成函数纳入范围会扩大启动、渲染和回归变量,却无法证明业务资产得到更好保护;具体兼容性仍需基于候选构建和设备端测试。
| 生成家族 | 应追踪的语义 | 优先保护对象 | 常见排除理由 |
|---|---|---|---|
| 协程状态机 | 挂起、恢复、取消、异常 | 手写业务状态转换 | 生成结构随工具链变化 |
| Suspend Lambda | 捕获值与最终动作 | 不可替代的目标函数 | 适配器只做转发 |
| SAM 适配器 | 接口签名与线程入口 | 具体业务实现 | 桥接层没有关键分支 |
| 默认参数入口 | 默认值与调用契约 | 改变业务结果的默认策略 | 纯参数补齐 |
| Compose 重组函数 | 状态读取与界面更新 | 手写状态变换 | 生成层与插件强耦合 |
| 反射可见成员 | 名称、签名与元数据 | 稳定业务入口 | 需优先保证可发现性 |
用方法清单把静态扫描变成可复核输入
静态清单工具的职责不是自动决定哪些方法必须进入 VMP,而是把容易被忽略的生成入口变成结构化复核项。输入应来自正式候选的类方法索引,至少包含 owner、name、descriptor、access flags、源码线索和调用者数量。工具先验证字段完整性,再识别 synthetic、bridge、Continuation 与 Compose 线索,最后输出 review、candidate 或 exclude 等待人工确认的建议。
下面的 Python 示例只读取本地 JSON 方法清单,不修改 APK、类文件或保护配置。它把 bridge、synthetic 和生成家族分开,并为每条记录保留判定原因。代码故意拒绝缺失描述符、非法 flags 和空方法集合,因为静默跳过会制造虚假的完整性。输出只是审查队列,不能替代调用图、R8 mapping、真实运行语义或具体产品能力验证。
使用结果时先处理同时具有 bridge 与 synthetic 的方法,再检查 Continuation、Lambda 和 Compose 线索,最后与手写资产清单连接。caller_count 为零只能提示没有从当前索引发现调用者,不能证明方法不可达;source_hint 为空也不能证明方法完全由编译器生成。工具输出需要和构建变体、R8 阶段及最终候选摘要绑定,才有资格进入发布证据。
from pathlib import Path
import json
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: inspect_generated_methods.py methods.json")
input_path = Path(sys.argv[1]).resolve()
if not input_path.is_file():
raise SystemExit(f"method inventory missing: {input_path.name}")
payload = json.loads(input_path.read_text(encoding="utf-8"))
methods = payload.get("methods")
if not isinstance(methods, list) or not methods:
raise SystemExit("methods must be a non-empty array")
allowed_flags = {"public", "private", "protected", "static", "final", "synthetic", "bridge"}
report = []
for index, method in enumerate(methods):
if not isinstance(method, dict):
raise SystemExit(f"method record {index} is not an object")
owner = method.get("owner")
name = method.get("name")
descriptor = method.get("descriptor")
flags = set(method.get("flags") or [])
if not all(isinstance(value, str) and value for value in (owner, name, descriptor)):
raise SystemExit(f"method record {index} lacks a stable JVM identity")
unknown_flags = flags - allowed_flags
if unknown_flags:
raise SystemExit(f"method record {index} has unknown flags: {sorted(unknown_flags)}")
family = "handwritten-or-unclassified"
if "bridge" in flags:
family = "bridge"
elif "synthetic" in flags and "Continuation" in descriptor:
family = "coroutine-state-machine"
elif "synthetic" in flags and ("lambda" in name.lower() or name.startswith("invoke")):
family = "lambda-adapter"
elif "synthetic" in flags and ("composer" in descriptor.lower() or "$composable" in name.lower()):
family = "compose-generated"
elif "synthetic" in flags:
family = "other-synthetic"
caller_count = method.get("caller_count", 0)
if not isinstance(caller_count, int) or caller_count < 0:
raise SystemExit(f"method record {index} has invalid caller_count")
decision = "review" if family != "handwritten-or-unclassified" else "map-to-business-asset"
report.append({
"identity": f"{owner}.{name}{descriptor}",
"family": family,
"caller_count": caller_count,
"source_hint": method.get("source_hint") or "unresolved",
"decision": decision,
"reason": "generated method needs ownership and call-path evidence" if decision == "review" else "business ownership is not yet recorded",
})
print(json.dumps({"method_count": len(report), "review": report}, ensure_ascii=False, indent=2))保护规则要能表达包含、排除和待复核
方法清单不应只有一个布尔字段。至少要区分 include、exclude 和 review,并为每条记录保存理由、责任人、输入哈希与最近验证日期。include 表示方法本身承载关键语义且已有回归;exclude 表示它是可解释的生成胶水或不在资产范围;review 表示身份或调用链证据不足。默认把未知对象放入 review,比默认全选或默认忽略更容易控制漂移。
规则匹配要尽量使用完整方法身份,并明确它适用于 R8 前还是 R8 后的产物。若工具只接受源码名称,流水线就需要用 mapping 生成最终可核对清单;若工具接受描述符,应同时验证 owner、name 和 descriptor 唯一。任何类级通配符都要有可辩护理由,并在方法数量异常变化时触发失败,避免依赖升级后范围无声扩大。
版本化不能只保存配置文件。一次可重验的范围决策还应绑定编译器、Kotlin、Compose 插件、R8 配置、构建变体和候选包摘要。工具链变化后,即使配置文本没有变化,也要重新生成方法索引和差异报告。新增方法不能沿用旧结论,消失的方法也不能简单从报告删除;它们需要在变更记录中说明被内联、重命名、替换还是移除。
| 状态 | 必要条件 | 流水线动作 | 发布表述 |
|---|---|---|---|
| include | 资产明确且回归可执行 | 进入精确规则并重验 | 仅陈述已选范围 |
| exclude | 生成职责明确且无关键分支 | 保存排除理由 | 不声称没有风险 |
| review | 调用链或身份不完整 | 阻止静默放行 | 结论保持未确认 |
| missing | 旧目标在新构建消失 | 追踪内联或替代位置 | 不得沿用旧覆盖结论 |
| ambiguous | 规则命中多个描述符 | 收窄规则或阻断 | 不得登记已准确命中 |
| verified | 最终候选与回归绑定 | 保存不可变回执 | 限定到已测候选和路径 |
放行靠语义回归和候选绑定,不靠方法数量
设备端 instrumented test 适合验证依赖 Android 运行时、反射、线程和组件行为的路径。对桥接与合成方法,测试应从真实入口发起,而不是直接调用内部生成方法;同时覆盖正常返回、类型不匹配、空值边界、取消、异常和重复调用。测试结果必须比较保护前后的可观察语义,并记录设备、系统、构建变体和候选摘要,单一设备通过不能扩写为完整兼容。
静态证据与动态证据承担不同责任。方法索引证明候选中观察到哪些方法及标志,mapping 说明 R8 前后名称关系,保护清单说明配置意图,运行回执说明特定路径在特定环境下的结果。四者必须指向同一最终包。只看到 synthetic 命中数量增加,不能证明业务资产受到更强保护;只看到应用启动,也不能证明桥接、反射和协程失败路径保持语义。
准备实际范围评审时,可先阅读[App VMP 保护范围选择指南](/zh-cn/vmp-protection-selection/),整理最终候选摘要、工具链版本、方法索引、R8 mapping、业务资产清单和回归入口。需要进入项目评估,可从[御盾中央平台提交加固申请](https://www.leonadev.com/console/)。提交材料应脱敏,结论只绑定已核对的候选与路径,不登记未经验证的兼容、性能或攻击阻断结果。
- 最终候选、方法索引、mapping 与保护清单使用同一构建身份
- 每个 include 方法都能回指业务资产和真实调用入口
- bridge、synthetic、协程、Lambda 与 Compose 生成路径分别分类
- 新增、消失和描述符变化都有明确变更原因
- 反射、取消、异常、线程与替代入口均进入回归
- 静态命中、设备通过与防护结论保持不同证据边界
- 登录、申请和控制台动作统一进入御盾中央平台
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| R8 会执行代码缩减、优化和名称混淆,因此源码阶段的方法身份不能直接代表最终候选中的方法身份。 | Enable app optimization with R8 说明 R8 在发布构建中的缩减、优化与混淆职责。 | R8 文档不定义 VMP 范围,也不能证明某条保护规则已命中最终方法。 |
| 反射、JNI 和其他间接入口需要精确 keep 规则,过宽规则会掩盖真正的可达性边界。 | R8 keep rules best practices 说明间接调用场景应使用精确、可维护的保留规则。 | keep 规则只约束 R8 行为,不能替代 VMP 选择、语义回归或防护验证。 |
| 协程生成路径需要同时考虑挂起、调度、取消和异常传播,而不能只检查一个方法是否存在。 | Kotlin coroutines guide 说明协程由挂起、结构化并发、取消和异常等语义共同组成。 | 语言指南不证明任何具体变换后的状态机仍保持语义,也不提供项目兼容结论。 |
| Compose 生成代码与 Kotlin 编译器插件及构建配置紧密相关,工具链变化可能改变生成路径。 | Compose Compiler Gradle plugin 说明 Compose 编译器插件与 Kotlin 和构建插件的配置关系。 | 配置关系不能推导所有 Compose 方法都应排除或都可处理,仍需候选级审查。 |
| 运行时反射依赖可发现的类、成员和元数据,方法身份变化可能影响查找与调用。 | Kotlin reflection 说明 Kotlin 在运行时访问类、成员和相关反射信息的方式。 | 该文档不覆盖每个框架的生成规则,也不能证明特定反射路径已经回归通过。 |
| 依赖 Android 运行时、组件和系统 API 的行为应通过设备端测试核对。 | Android instrumented tests 说明 instrumented test 在 Android 设备或模拟器环境中执行。 | 单一设备或单一路径通过不能代表全部 API、厂商、线程和业务条件。 |
| VMP 候选应从业务资产反向映射到最终方法,而不是按 synthetic 或 bridge 标志批量选择。 | 工程判断:生成标志只描述编译来源,资产价值需要由调用链、状态变化和业务后果确认。 | 该判断不证明某个具体方法可兼容处理,也不代表未选择的方法没有安全价值。 |
| 桥接入口和具体实现需要分别记录,因为它们可能承担调用契约与业务实现两种不同责任。 | 工程判断:完整描述符、调用者和被调关系能区分适配入口与真实业务分支。 | 静态委派关系不能覆盖反射、动态分派、内联和所有运行时替代路径。 |
| 发布证据必须绑定最终候选、工具链、方法索引、保护清单和回归结果。 | 项目证据尚未接入:本文给出所需绑定字段,不声称当前项目已经生成这些回执。 | 文件存在、命令成功或静态命中数量都不能单独构成兼容或防护结论。 |
| 合成方法的性能、兼容和防护效果需要在真实候选与目标环境中验证。 | 项目证据尚未接入:没有候选包、设备矩阵和对照结果时不报告性能数字或攻击阻断。 | 本文的决策表和代码仅用于规划与诊断,不能替代产品能力验证。 |
工程常见问题
带 synthetic 标志的方法是不是都应该排除 VMP?
不是。synthetic 只表示编译来源,仍要检查方法是否包含关键业务分支、是否是唯一稳定入口,以及能否建立保护前后的语义回归。
bridge 方法只有几行委派代码,还有保护价值吗?
它可能是接口、父类、反射或框架依赖的契约入口。通常应先保留入口并保护真实实现;若 bridge 自身没有关键分支,不必因入口重要就自动纳入高成本范围。
直接按包名保护所有 Kotlin 生成类是否更稳妥?
不稳妥。包级通配会把状态机、Lambda、Compose 和其他胶水混在一起,范围会随工具链漂移,也难以说明每个对象的资产价值和回归责任。
R8 keep 规则和 VMP 规则可以合并成同一份吗?
两者可共享方法身份清单,但责任不同。keep 规则解决 R8 可达性与名称约束,VMP 规则解决保护范围;任何一方都不能修复另一方的配置缺陷。
扫描到 invokeSuspend 是否说明协程核心逻辑已经找到?
不能。还要映射手写挂起函数、捕获状态、调用入口、取消与异常路径,并确认是否存在内联或替代入口。方法名称只能形成复核线索。
Compose 生成函数是否一律不进入 VMP?
不能一律判断。通常优先保护手写状态变换与业务规则,生成的重组路径保持清晰;若项目证据显示生成层含不可替代关键分支,再做单独评估。
怎样证明保护清单在编译器升级后仍然有效?
重新生成最终方法索引,与旧清单比较新增、消失和描述符变化,再用同一候选的 mapping、精确命中报告和设备端语义回归形成绑定回执。
静态诊断脚本输出 review 是否表示存在漏洞?
不表示。review 只说明方法身份或业务所有权需要人工确认。没有调用图、运行证据和真实候选时,不能把清单项写成漏洞、兼容失败或防护结论。