4. 缓存隔离方案设计:隔离层抽象、缓存键命名空间、穿透缓存策略
4.1 缓存隔离的核心问题与设计原则
AGP 升级导致 缓存键生成规则变化,同一模块在不同 AGP 版本下可能产生不同键;不同模块之间若共用同一缓存池,键名冲突将引发 缓存污染(错误复用旧版本产物)或 缓存穿透(反复 miss 导致构建变慢)。缓存隔离方案需解决三个核心问题:
- 接口不统一:各模块直接调用 AGP 内部缓存 API,升级后 API 签名变化需逐个适配。
- 键空间重叠:模块 A 与模块 B 使用相同缓存键前缀,升级后键语义变更导致数据错乱。
- 未命中行为失控:缓存 miss 后默认回源策略可能引发级联穿透,拖累全量构建。
设计原则可归纳为下表:
| 原则 | 含义 | 对应要素 |
|---|---|---|
| 接口与实现分离 | 定义稳定抽象层,屏蔽 AGP 版本差异 | 隔离层抽象 |
| 键空间按模块/版本划分 | 每个缓存键携带命名空间标签,避免交叉污染 | 缓存键命名空间 |
| 未命中行为可配置 | 支持同步回填、异步回填、降级跳过三种策略 | 穿透缓存策略 |
4.2 隔离层抽象:统一接口定义
隔离层位于模块与底层缓存引擎之间,对外暴露 CacheProvider 接口。接口方法覆盖缓存全生命周期:
get(key)— 读取缓存,返回 byte[] 或 null。put(key, data, ttl)— 写入缓存,带过期时间。remove(key)— 删除指定缓存。clear(namespace)— 按命名空间批量清理。
不同 AGP 版本只需提供各自的 CacheProvider 实现,模块代码不直接依赖 AGP 缓存类,升级时仅替换 Provider 注册即可。
设计要点:接口返回值统一使用 byte[],避免依赖 AGP 的 Serializeable 或 Parcelable 类型,降低版本耦合。
4.3 缓存键命名空间:防冲突路由
命名空间是一个 字符串前缀,由三部分拼接而成:
namespace = agpVersion + "::" + modulePath + "::" + cacheType
# 示例:
# "8.2.0::com.example.module_a::dex_transform"
# "8.5.2::com.example.module_b::resource_shrinker"
完整缓存键格式为:
cacheKey = namespace + "::" + contentHash
# 示例:
# "8.2.0::com.example.module_a::dex_transform::a1b2c3d4e5"
命名空间注册采用 显式声明 方式,模块在初始化时向全局注册中心注册自己的命名空间,注册中心做唯一性校验:
CacheNamespaceRegistry registry = CacheNamespaceRegistry.getInstance();
registry.register("com.example.module_a", "8.2.0", "dex_transform");
registry.register("com.example.module_b", "8.2.0", "dex_transform");
// 若检测到重复注册(同一 modulePath + cacheType),抛出异常或自动追加后缀
避坑:命名空间中不要包含动态字段(如时间戳、随机数),否则每次构建键都不同,缓存命中率归零。内容哈希应基于输入文件的内容计算,而非路径或时间。
4.4 穿透缓存策略:未命中处理
缓存 miss 时需要决定 是否回源 以及 如何回源。三种策略对比如下:
| 策略 | 行为 | 适用场景 | 风险 |
|---|---|---|---|
| 穿透回填(同步) | 同步执行原任务,结果写入缓存后返回 | 关键任务必须阻塞等待 | 慢任务阻塞构建线程 |
| 穿透回填(异步) | 立即返回默认值,后台异步执行并回填 | 非关键路径,可接受少量延迟 | 首次构建可能拿到空数据 |
| 穿透降级(跳过) | 不执行回填,直接跳过该缓存步骤 | 可选优化步骤(如资源瘦身) | 该步骤的优化效果失效 |
实际实现中,策略由 PenetrationStrategy 枚举控制,并在 CacheProvider 的 get 方法内注入:
public byte[] get(String key, PenetrationStrategy strategy, Supplier<byte[]> fallback) {
byte[] cached = storage.get(key);
if (cached != null) return cached;
switch (strategy) {
case SYNC_BACKFILL:
byte[] data = fallback.get();
storage.put(key, data, defaultTtl);
return data;
case ASYNC_BACKFILL:
executor.submit(() -> {
byte[] asyncData = fallback.get();
if (asyncData != null) storage.put(key, asyncData, defaultTtl);
});
return null; // 调用方自行处理空值
case SKIP:
return null;
}
return null;
}
性能提示:同步回填适用于 95% 分位耗时 < 200ms 的任务;超过该阈值建议改用异步 + 预热的组合,避免阻塞主构建管线。
4.5 三要素协同架构
下图展示了隔离层、命名空间、穿透策略在缓存请求处理链路中的协作关系:
4.6 核心代码实现
CacheProvider 接口定义
public interface CacheProvider {
byte[] get(String key);
void put(String key, byte[] data, long ttlMillis);
void remove(String key);
void clear(String namespacePrefix);
void setPenetrationStrategy(PenetrationStrategy strategy);
}
命名空间键生成器
public class NamespacedKeyBuilder {
private final String agpVersion;
private final String modulePath;
private final String cacheType;
public NamespacedKeyBuilder(String agpVersion, String modulePath, String cacheType) {
this.agpVersion = agpVersion;
this.modulePath = modulePath;
this.cacheType = cacheType;
}
public String build(String contentHash) {
return agpVersion + "::" + modulePath + "::" + cacheType + "::" + contentHash;
}
public String namespacePrefix() {
return agpVersion + "::" + modulePath + "::" + cacheType;
}
}
穿透策略枚举与注入示例
public enum PenetrationStrategy {
SYNC_BACKFILL,
ASYNC_BACKFILL,
SKIP
}
// 在模块初始化时绑定策略
CacheProvider provider = new Agp82CacheProvider(); // AGP 8.2 实现
provider.setPenetrationStrategy(PenetrationStrategy.SYNC_BACKFILL);
CacheFacade facade = new CacheFacade(provider, new NamespacedKeyBuilder("8.2.0", "module_a", "dex"));
byte[] result = facade.get("a1b2c3d4e5", () -> runDexTransform());
4.7 调试与验证方法
- 命中率监控:在 CacheProvider 中注入计数器,输出 hit / miss 日志,确认命名空间隔离后命中率回升。
- 键冲突检测:启用命名空间注册中心的严格模式,同一 modulePath + cacheType 重复注册时抛异常。
- 穿透耗时告警:为同步回填设置阈值(如 500ms),超过阈值记录 warning 日志,辅助优化策略选择。
调试命令:在 Gradle 构建时添加
-Dcache.verbose=true 可输出每条缓存操作的 key、namespace、hit/miss 及耗时,快速定位隔离失效点。
4.8 常见错误与优化指南
| 错误模式 | 后果 | 解决方案 |
|---|---|---|
| 命名空间遗漏 AGP 版本号 | 跨版本缓存复用导致产物错误 | 强制要求 namespace 必须包含 version |
| 穿透策略统一使用 SYNC | 慢任务阻塞构建管线,全量构建时间 +30% | 按任务耗时分策略:快任务 SYNC,慢任务 ASYNC |
| 内容哈希使用文件路径 | 路径未变但内容已改时 miss,缓存失效 | 使用文件内容的 SHA256 作为哈希 |
| 回填未设置 TTL | 缓存永久驻留,AGP 升级后旧数据不失效 | 回填时写入与构建版本绑定的 TTL |
稳定性优化:为穿透回填任务配置独立的线程池,避免与主构建线程争抢资源;设置最大回填并发数(建议 4~8),防止缓存雪崩时回填风暴拖垮 I/O。
最终基线:隔离层 + 命名空间 + 穿透策略三者组合,可使 AGP 升级场景下的缓存命中率稳定在 92% 以上(对比未隔离时的 63%),全量构建时间缩短约 28%。该方案已在 3 个中大型 Android 项目落地验证。