Evil Mouth's Blog

差分包

July 12, 2020

调研 Android 差分包过程记录

前测

目前市场差分方案有

  • bsdiff - 最常见的差分方案,例如国内应用市场的增量更新
  • archive-patcher - 谷歌推出的基于 bsdiff 的优化方案,使差分包更小
  • apkdiff - Github 开源的项目,基于 archive-patcher 思想
  • xdelta - 市面使用较少,但比 bsdiff 性能佳

以两个大约 38M 的 apk 作为例子

  • bsdiff:差分包 10M
  • archive-patcher:得基于两个 zlib 包,所以得先将两个 apk 进行 zlib 压缩(体积可能会变大),但是差分效果佳,差分包 400k
  • apkdiff:使用 lzma 压缩,差分包 300k,但是会导致合成包 md5 不一致,也可以先将 apk 进行 zlib 后再使用 apkdiff,则 md5 相同
  • xdelta:差分包 10M,但是差分和合成的速度和内存优于 bsdiff

分析

bsdiff

  • 比较常见的差异算法,不过因为较通用,所以没有考虑 apk 实际上是一个 zip 压缩包的情况,直接将 apk 当成文件去进行差异对比,所以产生的差异包较大
  • patch 时需要用到 O(m+n)的内存

archive-patcher

  • 严格基于 zlib,apk 需要使用 zlib 重新压缩
  • 差分和合成依旧使用 bsdiff
  • 之所以差分包大小比 bsdiff 小是因为先解压后再 diff,比直接 diff 更容易描述差异
  • 如果 apk 没有 zlib,将直接 bsdiff(差异包大小会与 bsdiff 一样大)
  • 将 apk 解压后进行 file-to-file 的差异对比,得到差异包
  • patch 阶段将差异包压缩回 apk(使用了 zlib)得到最终 apk 包

apkdiff

  • 合成后只保证逻辑相同,不保证二进制相同,这一点可以通过 zlib 压缩后解决(因为解压和压缩用了相同的编码)
  • 差分和合成使用 hdiff,性能优于 bsdiff
  • 核心原理是将 apk 解压后进行差异对比,改动越小,差异包越小,比较符合增量更新意义
  • 比 archive-patcher 暴力的是 diff 阶段直接解压然后差异对比,patch 阶段直接 zlib 压缩回 apk 包,所以会导致二进制不相同
  • 所以提供了 ApkNormalized 预处理 apk 包
  • 需要注意的是,ApkNormalized 重新压缩 apk 包后导致的二进制不相同会影响 v2 和 v3 的签名(v1 不影响是 v1 不严格要求),所以如果项目使用 v2 或 v3 的签名方式需要在 ApkNormalized 后进行重签名

xdelta

  • 性能比 bsdiff 好以及稳定,但差异包大小与 bsdiff 差不多甚至更大一些
  • 没有继续研究

接入成本

  • 以 armeabi-v7a 为例
  • jni 以 so 实际大小
  • java 以 jar 实际大小
bsdiff archive-patcher apkdiff xdelta
接入方式 jni java jni jni
接入大小 322KB 190KB 137KB

diff 对比

  • 由于 diff 过程属于预处理,一般放在服务器,所以只做时间对比
  • 设备信息如下
    • macOs Catalina 10.15.4
    • 3.1GHz i5 16GB
bsdiff archive-patcher apkdiff xdelta
差分包生成时间 42s 左右 24s 左右 10s 左右 4s 左右

patch 对比

  • 测试机信息如下
    • 华为 LRA-AL00 Android9 API28
    • 运行内存 8.0GB
    • 屏幕 2400 x 1080
    • 手机存储 128 GB
    • CPU 架构 arm64-v8a
bsdiff archive-patcher apkdiff xdelta
差分包大小 10.6M 448k 304k
cpu 占用 17%-24% 17%-22% 17%-23%
内存占用 86M 80M 6M
合成时间 5104ms 3841ms 3140ms

实际做了挺多次对比,结果大致相同,所以不放出

patch 时多渠道因素

问题

前因

  • 差异包只会打一个,以官方渠道包的新旧两个 apk 进行差异对比得到的差异包
  • 线上会存在多种渠道包,目前渠道包以两种形式存在
  • 1、ng-plugin 生成的渠道包(在 Zip Comment Central Directory 区域实现)
  • 2、Android Manifest 中 meta-data(部分渠道包还会更改应用名)

后果

  • 由于 diff&patch 要求 oldApk 完全一致(渠道包因为 Android Manifest 文件不同已经破坏一致),线上用户使用本地渠道包进行 patch 违反这一条约,所以 patch 会失败

如何解决 Zip Comment Central Directory

ng-plugin 方案是在 Central Directory 的注释部分插入渠道信息,不会到 apk 造成破坏,可以正常安装,但注释也是数据的一部分,也会参与 diff,所以会造成渠道包和原始包不一致导致无法 patch

  • 通过先抹除渠道信息再 patch
  • 如果需要保留原渠道信息,还可以再进行渠道的插入

如何解决 Android Manifest

思路 1

  • 用户在拿到 patch 包后
  • 利用本地渠道包还原 oldApk,再以 oldApk 去进行 patch 得到 newApk
  • 难点 1、还原 oldApk 过程需要与 Jenkins 打包流程一致才能保证 100%还原
  • 不足点 1、还原所需时间=打包所需时间,用户端消耗不起

思路 2

  • diff 阶段排除 Android Manifest 文件的差异对比
  • 下发 patch 包同时下发新的 Android Manifest 文件
  • patch 阶段将新 Android Manifest 文件 copy 进去后再压缩回 apk 文件
  • 难点 1、保证生成的 newApk 与正常流程的 newApk 一致

Demo

https://github.com/izyhang/DiffPatchRecord

— Evil Mouth