diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
new file mode 100644
index 0000000..9ccd54a
--- /dev/null
+++ b/dependency-reduced-pom.xml
@@ -0,0 +1,35 @@
+
+
| 工具 | Android 端形态 | 官方 Maven/AAR | 结论 |
|---|---|---|---|
| HDiffPatch | C++,需 NDK 交叉编译 + 自行 JNI 封装 | 无 | + *{@link Engine#HDIFFPATCH}:仅运行时探测,本工程未内置 |
| xdelta3 | C,需 NDK 交叉编译 + JNI,生态仅零散移植 | 无 | + *{@link Engine#XDELTA3}:同上 |
| bsdiff | C,有 Android 移植(Tinker / AOSP),非 Maven 包 | 无 | + *由纯 Java 实现 jbsdiff 替代 |
| jbsdiff | 纯 Java,无 native 依赖,Android 可直接运行 | 有 | + *{@link Engine#BSDIFF}:默认执行引擎 |
| UpdateEngine / imgdiff | {@code @SystemApi},系统签名才能调用 | 无 | + *普通 App 不可用,排除 |
结论:HDiffPatch / xdelta3 无现成 Android 依赖,接入需自建 NDK/JNI;本工程为纯 Java + * Maven 工具,执行层使用 {@code io.sigpipe:jbsdiff}。分档仍按各档理论最优引擎规划, + * native 引擎可用时可平滑替换。
+ * + *| 档位 | 大小 | 首选引擎 | 模式 | 压缩 | + *单条目上限 |
|---|---|---|---|---|---|
| SMALL | < 64MB | HDiffPatch | 整包 | bzip2 | - |
| MEDIUM | 64~128MB | xdelta3 | 整包 | gzip | - |
| LARGE | 128~256MB | bsdiff | 条目级 | bzip2 | 64MB |
| XLARGE | 256~512MB | bsdiff | 条目级 | bzip2 | 32MB |
| HUGE | > 512MB | bsdiff | 条目级 | bzip2 | 16MB |
128MB 起改条目级:整包 diff 峰值约 3~5 倍文件大小,会超过服务器 1G 可用内存; + * 条目级把单次 diff 对象降为单条目。补丁率超过 40% 时建议回退全量下发。
+ * + *patch 本身是 zip,含 {@code delta-data/NNNNNN.patch|raw} 与清单 + * {@code META-INF/delta-manifest},清单每行一条操作(tab 分隔):
+ *+ * COPY <name> old 同名同 CRC,原样拷贝 + * ADD <name> <dataEntry> old 无同名或超上限,直接携带 + * DIFF <name> <dataEntry> old 条目 + 容器内 bsdiff 补丁还原 + *+ */ +public final class ApkDeltaUpdater { + + private static final long MB = 1024L * 1024L; + + /** 条目级 patch 容器中清单条目的名称。 */ + private static final String MANIFEST_ENTRY = "META-INF/delta-manifest"; + + /** 补丁率超过该值即失去省流意义,应回退全量下发。 */ + public static final double FULL_PACKAGE_THRESHOLD = 0.40; + + private static final int BUFFER_SIZE = 64 * 1024; + + private ApkDeltaUpdater() { + } + + // ========================================================== + // 一、差量引擎与可用性探测 + // ========================================================== + + /** 差量引擎。native 引擎需自行 NDK 编译与 JNI 绑定,本工程仅内置 jbsdiff。 */ + public enum Engine { + /** HDiffPatch:补丁最小,native。 */ + HDIFFPATCH("HDiffPatch", "hdiffpatch"), + /** xdelta3:流式、内存可控,native。 */ + XDELTA3("xdelta3", "xdelta3"), + /** bsdiff:纯 Java(jbsdiff),默认执行引擎。 */ + BSDIFF("bsdiff(jbsdiff)", null); + + private final String displayName; + private final String nativeLibrary; + + Engine(String displayName, String nativeLibrary) { + this.displayName = displayName; + this.nativeLibrary = nativeLibrary; + } + + public String displayName() { + return displayName; + } + + /** 是否为 native 引擎(需 NDK 编译与 JNI 绑定)。 */ + public boolean isNative() { + return nativeLibrary != null; + } + + /** 本工程是否内置该引擎的执行实现(目前仅 jbsdiff)。 */ + public boolean hasExecutor() { + return this == BSDIFF; + } + + /** 运行时探测:native 引擎尝试加载动态库,失败即视为不可用。 */ + public boolean isAvailable() { + if (!isNative()) { + return true; + } + try { + System.loadLibrary(nativeLibrary); + return true; + } catch (UnsatisfiedLinkError | SecurityException e) { + return false; + } + } + } + + /** 打印各引擎的 Android 实现形态与当前可用性。 */ + public static void printEngineEvaluation() { + System.out.println("[差量引擎评估]"); + for (Engine engine : Engine.values()) { + String form = engine.isNative() + ? "需 NDK 交叉编译 + JNI(无官方 Maven/AAR)" + : "纯 Java(io.sigpipe:jbsdiff)"; + System.out.printf(" %-18s %-44s 可用=%s 内置执行=%s%n", + engine.displayName(), form, engine.isAvailable(), engine.hasExecutor()); + } + System.out.println(" 结论: HDiffPatch/xdelta3 无现成 Android 依赖,执行层回退 bsdiff(jbsdiff)"); + } + + // ========================================================== + // 二、按文件大小分档 + // ========================================================== + + /** 档位(上界不含):SMALL < 64MB < MEDIUM < 128MB < LARGE < 256MB + * < XLARGE < 512MB < HUGE。 */ + public enum SizeTier { + SMALL(64 * MB), + MEDIUM(128 * MB), + LARGE(256 * MB), + XLARGE(512 * MB), + HUGE(Long.MAX_VALUE); + + private final long upperBoundExclusive; + + SizeTier(long upperBoundExclusive) { + this.upperBoundExclusive = upperBoundExclusive; + } + + public long upperBoundExclusive() { + return upperBoundExclusive; + } + + /** 根据文件大小(字节)归档。 */ + public static SizeTier of(long fileSize) { + for (SizeTier tier : values()) { + if (fileSize < tier.upperBoundExclusive) { + return tier; + } + } + return HUGE; + } + } + + /** 执行模式。 */ + public enum Mode { + /** 整包 bsdiff。 */ + WHOLE_FILE, + /** zip 条目级 bsdiff(压低单次 diff 内存峰值)。 */ + ENTRY_LEVEL, + /** 不差量,直接携带全量。 */ + FULL_PACKAGE + } + + /** + * 差量执行计划,由 {@link DeltaPlan#plan(long)} 按新包大小生成。 + * + * @param tier 命中的档位 + * @param preferredEngine 该档理论最优引擎(不可用时回退) + * @param mode 执行模式 + * @param compressor bsdiff 压缩算法(commons-compress 名称) + * @param maxDiffEntrySize 条目级下单条目 diff 上限(字节),整包模式为 0 + */ + public record DeltaPlan(SizeTier tier, Engine preferredEngine, Mode mode, + String compressor, long maxDiffEntrySize) { + + /** 根据新包大小生成差量计划(分档策略的唯一入口)。 */ + public static DeltaPlan plan(long newFileSize) { + SizeTier tier = SizeTier.of(newFileSize); + return switch (tier) { + case SMALL -> new DeltaPlan(tier, Engine.HDIFFPATCH, Mode.WHOLE_FILE, + CompressorStreamFactory.BZIP2, 0); + case MEDIUM -> new DeltaPlan(tier, Engine.XDELTA3, Mode.WHOLE_FILE, + CompressorStreamFactory.GZIP, 0); + case LARGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL, + CompressorStreamFactory.BZIP2, 64 * MB); + case XLARGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL, + CompressorStreamFactory.BZIP2, 32 * MB); + case HUGE -> new DeltaPlan(tier, Engine.BSDIFF, Mode.ENTRY_LEVEL, + CompressorStreamFactory.BZIP2, 16 * MB); + }; + } + + /** 实际可执行引擎:首选引擎无内置实现或不可用时回退 jbsdiff。 */ + public Engine resolveEngine() { + if (preferredEngine.hasExecutor() && preferredEngine.isAvailable()) { + return preferredEngine; + } + return Engine.BSDIFF; + } + } + + // ========================================================== + // 三、生成 / 应用 + // ========================================================== + + /** 按新包大小自动分档,生成 patch。 */ + public static void createPatch(File oldFile, File newFile, File patchFile) + throws IOException, CompressorException, InvalidHeaderException { + createPatch(oldFile, newFile, patchFile, DeltaPlan.plan(newFile.length())); + } + + /** + * 按指定计划生成 patch。生成后计算补丁率,超过 + * {@link #FULL_PACKAGE_THRESHOLD} 时输出回退全量的警告。 + */ + public static void createPatch(File oldFile, File newFile, File patchFile, DeltaPlan plan) + throws IOException, CompressorException, InvalidHeaderException { + long startNanos = System.nanoTime(); + Engine engine = plan.resolveEngine(); + if (engine != plan.preferredEngine()) { + System.out.printf("[createPatch] 首选引擎 %s 不可用(native 未内置),回退 %s%n", + plan.preferredEngine().displayName(), engine.displayName()); + } + switch (plan.mode()) { + case WHOLE_FILE -> FileUI.diff(oldFile, newFile, patchFile, plan.compressor()); + case ENTRY_LEVEL -> createEntryLevelPatch(oldFile, newFile, patchFile, plan); + case FULL_PACKAGE -> Files.copy(newFile.toPath(), patchFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + double ratio = patchRatio(newFile, patchFile); + System.out.printf( + "[createPatch] tier=%s engine=%s mode=%s old=%d new=%d patch=%d ratio=%.1f%% 耗时 %s%n", + plan.tier(), engine.displayName(), plan.mode(), oldFile.length(), newFile.length(), + patchFile.length(), ratio * 100, formatDuration(System.nanoTime() - startNanos)); + if (ratio > FULL_PACKAGE_THRESHOLD) { + System.out.printf("[createPatch][warn] 补丁率超过 %.0f%%,建议回退全量下发%n", + FULL_PACKAGE_THRESHOLD * 100); + } + } + + /** 应用 patch 还原新包,自动识别整包 bsdiff 与条目级容器两种格式。 */ + public static void applyPatch(File oldFile, File patchFile, File newFile) + throws IOException, CompressorException, InvalidHeaderException { + long startNanos = System.nanoTime(); + boolean entryLevel = isEntryLevelPatch(patchFile); + if (entryLevel) { + applyEntryLevelPatch(oldFile, patchFile, newFile); + } else { + FileUI.patch(oldFile, newFile, patchFile); + } + System.out.printf("[applyPatch] %s old=%d patch=%d new=%d 耗时 %s%n", + entryLevel ? "entry-level" : "whole-file", + oldFile.length(), patchFile.length(), newFile.length(), + formatDuration(System.nanoTime() - startNanos)); + } + + /** 补丁率:patch 大小 / 新包大小。 */ + public static double patchRatio(File newFile, File patchFile) { + long newSize = Math.max(1L, newFile.length()); + return (double) patchFile.length() / newSize; + } + + // ========================================================== + // 四、条目级实现 + // ========================================================== + + /** 条目级 diff:未变化条目记 COPY,超过上限的改动条目直接携带(ADD)。 */ + private static void createEntryLevelPatch(File oldFile, File newFile, File patchFile, + DeltaPlan plan) + throws IOException, CompressorException, InvalidHeaderException { + Map
用法:
+ *+ * diff <oldFile> <newFile> <patchFile> [-c|--compressor bzip2|gz|pack200|xz] + * patch <oldFile> <patchFile> <newFile> + *+ * + *
示例:
+ *+ * java -jar MavenUtils.jar diff old.bin new.bin patch.bin + * java -jar MavenUtils.jar patch old.bin patch.bin restored.bin + *+ * + *
每次 bsdiff / bspatch 执行完成后都会输出耗时。
+ */ +public class BsDiffTool { + + private static final String DEFAULT_COMPRESSOR = CompressorStreamFactory.BZIP2; + + private BsDiffTool() { + } + + public static void main(String[] args) { + if (args.length < 1) { + printUsage(); + return; + } + + String command = args[0].toLowerCase(Locale.ROOT); + try { + switch (command) { + case "bsdiff" -> diff(args); + case "bspatch" -> patch(args); + default -> printUsage(); + } + } catch (CompressorException | InvalidHeaderException | IOException e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + /** + * bsdiff:由 oldFile 与 newFile 生成 patchFile。 + */ + private static void diff(String[] args) + throws CompressorException, InvalidHeaderException, IOException { + if (args.length < 4) { + printUsage(); + return; + } + + File oldFile = new File(args[1]); + File newFile = new File(args[2]); + File patchFile = new File(args[3]); + String compression = parseCompressor(args); + + requireExists(oldFile, "oldFile"); + requireExists(newFile, "newFile"); + + long startNanos = System.nanoTime(); + FileUI.diff(oldFile, newFile, patchFile, compression); + + System.out.printf( + "[bsdiff] %s (%d bytes) -> %s (%d bytes), compressor=%s%n", + oldFile, oldFile.length(), newFile, newFile.length(), compression); + System.out.printf("[bsdiff] patch 文件已生成: %s (%d bytes)%n", + patchFile, patchFile.length()); + System.out.printf("[bsdiff] bsdiff 完成,耗时 %s%n", + formatDuration(System.nanoTime() - startNanos)); + } + + /** + * bspatch:由 oldFile 与 patchFile 还原出 newFile(压缩算法自动识别)。 + */ + private static void patch(String[] args) + throws CompressorException, InvalidHeaderException, IOException { + if (args.length < 4) { + printUsage(); + return; + } + + File oldFile = new File(args[1]); + File patchFile = new File(args[2]); + File newFile = new File(args[3]); + + requireExists(oldFile, "oldFile"); + requireExists(patchFile, "patchFile"); + + long startNanos = System.nanoTime(); + FileUI.patch(oldFile, newFile, patchFile); + + System.out.printf( + "[bspatch] %s (%d bytes) + %s (%d bytes) -> %s (%d bytes)%n", + oldFile, oldFile.length(), patchFile, patchFile.length(), + newFile, newFile.length()); + System.out.printf("[bspatch] bspatch 完成,耗时 %s%n", + formatDuration(System.nanoTime() - startNanos)); + } + + /** + * 从参数中解析压缩算法,支持 "-c xxx" / "--compressor xxx",默认 bzip2。 + */ + private static String parseCompressor(String[] args) { + for (int i = 4; i < args.length - 1; i++) { + if ("-c".equals(args[i]) || "--compressor".equals(args[i])) { + String name = args[i + 1].toLowerCase(Locale.ROOT); + // CompressorStreamFactory 中 gzip 的别名为 "gz" + return "gzip".equals(name) ? CompressorStreamFactory.GZIP : name; + } + } + return DEFAULT_COMPRESSOR; + } + + private static void requireExists(File file, String name) { + if (!file.isFile()) { + System.err.printf("Error: %s 不存在或不是文件: %s%n", name, file); + System.exit(1); + } + } + + /** + * 将时长(纳秒)转换为可读字符串: + * 不足 1 秒用 ms,不足 1 分钟用 s,不足 1 小时用 min,否则用 h。 + */ + private static String formatDuration(long nanos) { + double ms = nanos / 1_000_000.0; + if (ms < 1_000.0) { + return String.format(Locale.ROOT, "%.3f ms", ms); + } + double totalSeconds = ms / 1_000.0; + if (totalSeconds < 60.0) { + return String.format(Locale.ROOT, "%.3f s", totalSeconds); + } + long minutes = (long) (totalSeconds / 60.0); + if (minutes < 60) { + return String.format(Locale.ROOT, "%d min %.3f s", minutes, totalSeconds - minutes * 60.0); + } + long hours = minutes / 60; + return String.format(Locale.ROOT, "%d h %d min %.3f s", + hours, minutes % 60, totalSeconds - minutes * 60.0); + } + + private static void printUsage() { + System.out.println(""" + 用法: + diff