“真正的”Apk增量更新方案ApkDiffPatch

“真正的”Apk增量更新方案ApkDiffPatch

作者: HouSisong@gmail.com 2018.03.31


Android的Apk包增量更新原理:

服务端对新旧版本的2个Apk文件进行diff得到差异部分生成补丁;客户端只需下载补丁,与已经安装的旧Apk执行patch就可以生成新版本的Apk进行安装;达到降低下载流量和节约下载时间的目的。

但现在所有的实现方案几乎都只是把Apk看作文件数据直接用BsDiff(或HDiffPatch)进行diff,而没有考虑Apk包本身是一个zip压缩包的事实
比如你只是修改了程序的一行代码,但很可能造成生成一个过M大小的补丁(典型的Unity游戏这时补丁甚至可能会上10M);
比如你重新用新工具优化压缩了新版Apk,但和旧版diff生成补丁后发现和Apk包几乎一样大;…
这是因为在压缩成Apk(zip)包的时候,压缩算法已经破坏了“现场”,文件的一处改动或者压缩参数的变动,将生成完全不同的压缩编码;diff算法的优势没有真正发挥出来。

#ApkDiffPatch方案:
你可以做一个这样的tar实验:将新旧Apk当作zip包解压,并把解压结果打成2个tar包,然后针对tar用diff算法; 一般情况下你会发现你得到了小得多的补丁!

ApkDiffPatch就是类似该tar实验的一个增量更新方案的实现,考虑了Apk包本身是一个zip压缩包的事实:将包抽象成未压缩数据来进行diff,充分发挥diff算法的优势。
对比多个Apk的不同版本间求增量的结果来看,新方案的补丁大小一般是直接diff方案的1/3 – 1/10大小! (部分测试结果见

ApkDiffPatch简介:

ApkDiffPatch是一个通用的Zip包Diff&Patch工具(MIT协议,C++实现,开源地址
支持Zip\Jar\Apk文件的Diff和Patch,生成最小的差异补丁,支持Apk的V2版签名和Jar(Apk V1)的签名;
(库不支持zip64格式,压缩算法也只支持Deflate,依赖的第三方库:HDiffPatch, zlib, lzma。)

超简单的使用方式:

ZipDiff(oldZip,newZip,out diffData)

该函数生成oldZip到newZip的补丁diffData;

ZipPatch(oldZip,diffData,out newZip)

用diffData加上oldZip就可以重新生成newZip;

特别说明

  • 库默认并不保证patch生成的newZip包和diff时的newZip包二进制完全一致(只保证逻辑上一样),因为压缩算法和zip包的组织方式不一定能够和原来的完全一致;
  • 但是在使用ApkV2签名的情况下,二进制完全一致又是必须的要求;
  • 所以库提供了一个解决方案:用工具ApkNormalized来预处理发布包:Released newZip := AndroidSDK#apksigner(ApkNormalized(newZip)) before ZipDiff;并且最好不要再改变zlib库的版本(除非证明兼容);
    当然如果你不需要ApkV2签名,但也要求patch结果二进制始终一致;也可以这样用(Released newZip :=ApkNormalized(newZip) before ZipDiff)
  • ZipPatch运行时内存需要4*(decompress stream memory) + ‘ref old decompress memory’ + O(1), 其中oldZip引用到的压缩文件临时解压出的数据’ref old decompress’也可以选择用临时磁盘文件来代替内存占用。

设计和实现:

  • 理论上oldZip包不应被修改,否则patch会失效; 库在实现的时候,对oldZip的“轻微”修改是兼容的:比如重新调整压缩率、重新对齐了每个文件起始位置、在CentralDirectory之前或V2签名区里插入了渠道数据等;当然对于ApkV2签名的oldZip包,只允许在V2签名区中修改;patch的时候会对依赖的“标准”旧数据进行crc32校验以保证安全;
  • zip包中在CentralDirectory之前或V2签名区里是兼容性比较好的能够写入自己私有数据的区域,所以库也不去依赖这一块数据不能改变;生成的补丁包文件也支持添加附加数据用以在patch时把数据写到该对应区域(见ZiPatExtraDemo演示代码);
  • 为了保证需要的时候控制输出数据的每一个字节,库自己实现了一个简单版本的zip读写库,而不是去依赖minizip等第三方库;
  • patch的时候需要重新生成zip包,而压缩一般是比较慢的过程,所以大小和速度需要整体考虑:默认用压缩率略低但速度较快的压缩参数;patch时如果可以时就直接拷贝oldZip的压缩数据;
  • 因为是FileByFile的diff,所以在patch时也需要对引用到的并且是压缩状态的oldZip中的文件先进行解压,这些数据可以选择放到内存或写到临时文件中;这就要求尽量优化减少这类文件引用量;当前的实现如果发现和newZip中的某文件相同就不会被引用,算是一个“便宜”的可以改进的实现;
  • 库的底层diff&patch算法选择了HDiffPatch库,而不是BsDiff;原因有:
    0. HDiffPatch是我实现的:)
    1. HDiffPatch支持O(1)内存的patch过程(库当前的默认选择),而不需要加载O(m+n)的数据到内存,patch速度也快;
    2. diff速度比BsDiff快很多,得到的补丁一般都更小;
    3. HDiffPatch支持选择压缩插件,库默认用了其lzma插件,这样可以使补丁数据更小;(选择lzma插件解压时需要占用16M内存)
  • 为了Apk运行时速度,库支持设置未压缩文件的数据对齐(比如4或8等),与 AndroidSDK#zipalign 类似;对齐值也会在diff运行时进行识别(运行时也会识别可能的压缩参数);
  • patch过程在写zip时,设计上不使用Data Descriptor信息的方式保存文件压缩后大小,而是用了一些方式尽量(保存某些数据的压缩后大小)提早写入正确的压缩大小,这样有利于patch速度和Apk包性能;
  • newZip包进行V2重签名后,V1签名文件会被签名工具重新生成,也就是会重新压缩这几个文件;这时diff时会尝试能否兼容其压缩(并计算出兼容的压缩参数),如果能兼容那就可以和其他数据一起执行解压状态的diff(补丁更小),否则就保持其压缩状态的diff;
  • 现在的解决方案无法解决这样的应用场景:Apk必须使用V2及以上签名,而自己又无法签名;比如自己是下载服务商,包不是自己进行的V2签名;或者将自己的Apk交给了渠道商V2签名发布了;这种场景实际上自己已经没有了控制力;(这种情况下专门提供了一个针对应用商店的与签名无关的新版本 sfpatcher

欢迎提交bug和建议

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值