v1.0.15 已正式上线,为亿级手机终端用户提供更新服务,当前最新版本 v1.1.3
sfpatcher 命令行工具下载(支持Windows、Linux、MacOS),
命令行使用说明
需要商业授权(含源代码&培训),请联系作者: [email protected]
app:本文档一般指智能手机和Pad设备上的软件,包括应用和游戏等。
应用商店:这里指当前安卓智能设备上负责管理app的软件,支持app的下载、更新、推荐等功能;软件也可能分成应用市场和游戏中心两个独立应用。
apk:安卓设备上的约定的软件包格式,它属于特殊约定格式的zip压缩档案格式的一种。为了防止篡改,发布的apk都带有签名。
diff:这里指一种创建补丁的算法,它能计算出两个数据之间的差异,比如生成从旧版本数据到新版本之间的补丁数据。
patch:打补丁,diff算法的逆算法,它能在旧版本数据基础上应用补丁来得到新版本的数据。
随着智能手机的普及和应用数量的爆发式增长,手机上大量app的下载和更新将产生巨大的数据流量。手机上的应用商店一般都会部署增量更新系统来节省大量的服务带宽成本和提升用户体验(节省用户流量和减少下载时间),而不用每次都下载完整版本。
应用商店使用的增量更新算法一般选择使用的是基于字节的 diff 和 patch 算法,包括:BsDiff 、xdelta3、HDiffPatch等。
现在提到的增量更新实现方案都只是把apk单纯的看作文件数据直接用算法进行diff&patch,而没有考虑apk包本身是一个zip压缩包的事实。这种简单使用方式可以概括为公式:
diffFile:= diff(oldApk,newApk)
newApk := patch(oldApk,diffFile)
大家应该都有这样的经验:一个大文件在1/4处简单进行修改,如果把修改前后的2个文件分别压缩成zip文件,用二进制工具打开2个zip文件对比会发现:前面一部分的编码相同(如果前后压缩率相当,那这部分约占1/4),但后面部分的编码数据会完全不同。
压缩算法破坏了修改“现场”,diff算法的优势无法真正发挥出来(生成了巨大的补丁)。那么我们是否可以开发针对apk文件格式的diff&patch算法呢? 它能够识别出上面的情况:其实只是修改了一个字符!(即生成极小的补丁)
很容易想到的一个优化思路,先解压再diff不就好了!原理可以概括为公式:
diffFile:=diff(decompress(oldApk),decompress(newApk))
newApk :=recompress(patch(decompress(oldApk),diffFile))
公式里面的diff和patch函数很好办,选择上面提到的一种基于字节的diff&patch算法就行;decompress函数的实现也较简单,就是zip包的解压缩算法;recompress函数的实现比较麻烦一些,需要保证精确的原样还原出newApk(避免破坏签名和运行等)。
而 archive-patcher 和 ApkDiffPatch 正是这种思路的实现方案,也包括本文章要介绍的 sfpatcher 方案都基于这个思路。
针对压缩档案文件的高性能增量更新方案。类似于 archive-patcher 方案可部署于应用商店的diff&patch算法,该领域的重要技术进展。
内部使用了 HDiffPatch 算法作为基础,用C\C++语言开发,当前支持为deflate格式压缩的数据创建优化的补丁(用zlib压缩的支持最好),支持在多种档案格式文件之间创建优化的补丁。
注1:所有测试数据来源于收集的一些常用apk应用,共32个用例;并在ARM CPU Kirin980上测试了部分patch。(见性能测试对比数据)
对比多种diff&patch方案在apk文件增量更新方面的运行数据;
测试项主要包括:diff速度、diff内存占用、补丁大小(用压缩率代替,压缩后补丁大小/新版本apk大小)、patch速度、patch内存占用(后面这3项指标可能更重要一些)
收集了32组测试用例;这些apk下载于小米应用商店、谷歌Play商店,按照下载量大和最近进行过更新为标准进行收集。
限于有限的用例和收集偏差,数据和实际情况可能略有差异。 旧版本apk平均大小103.2MB,新版本apk平均大小103.8MB。
在一台笔记本PC上对比测试:Windows11, CPU R9-7945HX, SSD PCIe4.0x4 4T, DDR5 5200MHz 32Gx2
测试时关闭了HDiffPatch和sfpatcher在diff时的多线程,而开启多线程时一般可以成倍的提高diff速度。
patch时标注tmpf表示使用了临时文件来储存中间数据;mem表示在内存中执行不使用临时文件;limit表示使用限制内存占用的模式执行;而标注MT表示开启了多线程(8个)并行。
BsDiff v4.3 还是保持着使用bzip2算法压缩补丁。
xdelta v3.1.0 使用-e -n -f -s
来创建补丁, 而用-d -f -s
参数来执行的patch。
xdelta3 -B 是指diff时添加-B oldSize
增大引用窗口到oldApk文件相同大小进行的测试,其结果仅供参考,因为patch时内存占用也会随之同样增大。
HDiffPatch v4.6.3 支持2种diff模式,-s-16
和-m-1 -cache -block
模式分别测试,输出补丁时分别测试了用lzma2、zstd压缩的测试。HDiffPatch支持输出兼容bsdiff的补丁(bzip2压缩),补充了-BSD -m-1 -cache -block
参数后的测试结果。
archive-patcher v1.0 一般使用gzip或brotli算法压缩补丁,这里为了diff速度并更好的和其他方案对比补丁大小,diff时输出不压缩的补丁,然后再额外使用lzma2压缩补丁。 需要注意:这时收集到的diff数据不包含额外压缩时的时间和内存消耗,收集到的patch数据也不包含解压的时间和内存消耗等。
ApkDiffPatch v1.6.0 使用了lzma2来压缩输出的补丁。
sfpatcher v1.1.1 支持4个级别的diff,-0,-1,-2和-3分别测试; sfpatcher支持不需要旧版本apk而直接重新压缩新版本apk的模式(-pre);sfpatcher支持多种可选压缩输出,这里测试了-c-zstd-21和-c-lzma2-9这2种。
sfpatcher补充测试了用ApkNormalized(ApkDiffPatch方案)处理过的apk文件,分别进行增量测试和lzma2重压缩测试(标记为Norm)。
另外在一部安卓手机(CPU:Kirin980 2×A76 2.6G + 2×A76 1.92G + 4×A55 1.8G)上对hpatchz&hsynz&sfpatcher进行了一些patch时间测试,补充到了最后一列。
其中:平均压缩率=(补丁大小/新apk大小)的平均值; 单次测试的内存占用统计值为峰值私有内存;
diff方案 | 平均压缩率 | 平均内存 | 平均速度 | patch | 平均内存 | 最大内存 | 平均速度 | arm Kirin980 |
---|---|---|---|---|---|---|---|---|
zstd --patch-from | 53.18% | 2199M | 3.6MB/s | mem | 209M | 596M | 609MB/s | |
xdelta3 | 54.51% | 422MB | 3.8MB/s | mem | 98MB | 99MB | 170MB/s | |
xdelta3 -B | 53.53% | 848M | 4.3MB/s | mem | 183M | 548M | 171MB/s | |
bsdiff | 53.84% | 931MB | 1.2MB/s | mem | 218MB | 605MB | 54MB/s | |
hdiffz | 54.40% | 509M | 8.8MB/s | mem | 5M | 6M | 682MB/s | 443MB/s |
hdiffz lzma2 | 52.93% | 525M | 4.1MB/s | mem | 21M | 22M | 260MB/s | 131MB/s |
hdiffz zstd | 53.04% | 537MB | 5.4MB/s | mem | 21MB | 22MB | 598MB/s | 371MB/s |
hdiffz -s zstd | 53.44% | 221M | 10.1MB/s | mem | 20M | 22M | 620MB/s | |
archive-patcher | 31.65% | 1448MB | 0.9MB/s | temf | 558MB | 587MB | 14MB/s | |
ApkDiffPatch | 18.44% | 1003M | 2.2MB/s | mem | 164M | 453M | 20MB/s | |
ApkDiffPatch | 18.44% | 1003M | 2.2MB/s | memMT | 257M | 628M | 64MB/s | |
ApkDiffPatch | 18.44% | 1003M | 2.2MB/s | tmpf | 21M | 25M | 17MB/s | |
ApkDiffPatch | 18.44% | 1003M | 2.2MB/s | tmpfMT | 101M | 211M | 46MB/s | |
sfpatcher-3 Norm | 18.35% | 1147M | 2.2MB/s | limit | 43M | 59M | 21MB/s | |
sfpatcher-3 Norm | 18.35% | 1147MB | 2.2MB/s | limitMT | 49MB | 65MB | 77MB/s | |
sfpatcher-3 Norm | 18.35% | 1147M | 2.2MB/s | memMT | 170M | 452M | 99MB/s | |
sfpatcher-3pre Norm | 64.91% | 610M | 1.7MB/s | mem | 37M | 41M | 14MB/s | |
sfpatcher-3pre Norm | 64.91% | 610MB | 1.7MB/s | memMT | 42MB | 46MB | 64MB/s | |
sfpatcher-0 zstd | 53.04% | 537M | 5.4MB/s | mem | 19M | 20M | 564MB/s | 371MB/s |
sfpatcher-0 zstd | 53.05% | 1251M | 11.0MB/s | memMT | 20M | 22M | 739MB/s | 489MB/s |
sfpatcher-0 lzma2 | 52.92% | 525M | 4.1MB/s | mem | 18M | 20M | 253MB/s | 131MB/s |
sfpatcher-0 lzma2 | 52.94% | 557M | 18.8MB/s | memMT | 19M | 22M | 346MB/s | 157MB/s |
sfpatcher-1 zstd | 31.08% | 818M | 2.3MB/s | limit | 15M | 19M | 201MB/s | 92MB/s |
sfpatcher-1 zstd | 31.07% | 1025MB | 4.6MB/s | limitMT | 18MB | 25MB | 424MB/s | 189MB/s |
sfpatcher-1 lzma2 | 29.75% | 819M | 2.3MB/s | limit | 14M | 19M | 104MB/s | 50MB/s |
sfpatcher-1 lzma2 | 29.75% | 809M | 5.3MB/s | limitMT | 17M | 24M | 167MB/s | 74MB/s |
sfpatcher-2 zstd | 26.27% | 975M | 2.1MB/s | limit | 15M | 20M | 43MB/s | |
sfpatcher-2 zstd | 26.29% | 1002M | 4.7MB/s | limitMT | 20M | 27M | 155MB/s | |
sfpatcher-2 lzma2 | 24.11% | 976M | 2.1MB/s | limit | 15M | 20M | 37MB/s | 19MB/s |
sfpatcher-2 lzma2 | 24.15% | 968MB | 5.0MB/s | limitMT | 20MB | 26MB | 108MB/s | 45MB/s |
sfpatcher-3 lzma2 | 23.53% | 997M | 2.0MB/s | limit | 16M | 20M | 31MB/s | 17MB/s |
sfpatcher-3 lzma2 | 23.56% | 987M | 4.5MB/s | limitMT | 21M | 26M | 98MB/s | 40MB/s |
sfpatcher-2pre zstd | 81.36% | 376M | 2.8MB/s | mem | 19M | 23M | 37MB/s | |
sfpatcher-2pre zstd | 81.36% | 1646M | 7.1MB/s | memMT | 24M | 30M | 193MB/s | |
sfpatcher-3pre zstd | 79.20% | 387M | 2.4MB/s | mem | 20M | 23M | 29MB/s | |
sfpatcher-3pre zstd | 79.20% | 1698M | 6.5MB/s | memMT | 25M | 30M | 144MB/s | |
sfpatcher-2pre lzma2 | 75.23% | 378M | 1.9MB/s | mem | 20M | 23M | 24MB/s | 12MB/s |
sfpatcher-2pre lzma2 | 75.42% | 1091MB | 8.3MB/s | memMT | 24MB | 28MB | 63MB/s | 26MB/s |
sfpatcher-3pre lzma2 | 73.34% | 386M | 1.7MB/s | mem | 20M | 23M | 21MB/s | 11MB/s |
sfpatcher-3pre lzma2 | 73.53% | 1126M | 8.1MB/s | memMT | 25M | 29M | 60MB/s | 24MB/s |
最近收集了32组游戏测试用例,这些apk下载于小米应用商店、TapTap商店、谷歌Play商店,按照下载量大和最近进行过更新为标准进行收集。
对比测试了 xdelta、bsdiff、HDiffPatch、sfpatcher; 因为有大游戏所以放弃了无法完成测试的archive-patcher。
sfpatcher v1.1.1 测试时,调整了部分参数,相比前面的测试增大了patch时的内存需求。
旧版本apk平均大小1029.5MB,新版本apk平均大小1040.9MB。
diff方案 | 平均压缩率 | 平均内存 | 平均速度 | patch | 平均内存 | 最大内存 | 平均速度 | 最差速度 |
---|---|---|---|---|---|---|---|---|
zstd | 36.35% | 5297MB | 3.2MB/s | mem | 2088MB | 4033MB | 852MB/s | 576MB/s |
xdelta3 lzma | 54.14% | 423MB | 3.5MB/s | mem | 99MB | 104MB | 94MB/s | 30MB/s |
xdelta3 lzma +hpatchz -m | 54.14% | 423MB | 3.5MB/s | mem | 79MB | 83MB | 258MB/s | 84MB/s |
xdelta3 lzma -B | 35.89% | 6424MB | 6.6MB/s | mem | 1299MB | 2090MB | 233MB/s | 76MB/s |
xdelta3 lzma -B +hpatchz -m | 35.89% | 6424MB | 6.6MB/s | mem | 1043MB | 2028MB | 291MB/s | 180MB/s |
bsdiff | 36.15% | 9284MB | 1.1MB/s | mem | 2085MB | 4042MB | 89MB/s | 32MB/s |
bsdiff +hpatchz -m | 36.15% | 9284MB | 1.1MB/s | mem | 1045MB | 2029MB | 95MB/s | 33MB/s |
bsdiff +hpatchz -s | 36.15% | 9284MB | 1.1MB/s | mem | 14MB | 14MB | 87MB/s | 32MB/s |
hdiffz p1 zstd | 35.41% | 3812MB | 6.3MB/s | mem | 23MB | 23MB | 712MB/s | 291MB/s |
hdiffz p8 zstd | 35.42% | 4249MB | 25.3MB/s | mem | 22MB | 23MB | 703MB/s | 284MB/s |
hdiffz p1 lzma2 | 35.26% | 3813MB | 5.3MB/s | mem | 22MB | 23MB | 344MB/s | 136MB/s |
hdiffz p8 lzma2 | 35.28% | 3842MB | 29.9MB/s | mem | 22MB | 23MB | 344MB/s | 135MB/s |
sfpatcher-1 zstd | 22.23% | 4491MB | 5.4MB/s | limit | 23MB | 35MB | 412MB/s | 152MB/s |
sfpatcher-1 zstd | 22.20% | 4684MB | 15.5MB/s | limitMT | 26MB | 38MB | 634MB/s | 308MB/s |
sfpatcher-2 lzma2 | 19.61% | 5162MB | 4.9MB/s | limit | 26MB | 47MB | 101MB/s | 13MB/s |
sfpatcher-2 lzma2 | 19.62% | 5121MB | 16.8MB/s | limitMT | 32MB | 54MB | 249MB/s | 44MB/s |
sfpatcher-3 lzma2 | 17.87% | 5682MB | 4.6MB/s | limit | 32MB | 47MB | 49MB/s | 6MB/s |
sfpatcher-3 lzma2 | 17.91% | 5631MB | 15.6MB/s | limitMT | 39MB | 54MB | 157MB/s | 26MB/s |
收集了Top500中多个app应用(不含游戏)和其多个历史版本,形成了4695个测试用例,进行了diff和patch多种参数测试并分别使用了lzma2压缩和zstd压缩输出补丁。
测试条件:v1.0.15 用了-lp-2m加8m压缩字典 -pre时用的16m压缩字典
方案 | 平均压缩率 |
---|---|
sfpatcher-0 lzma2 | 50.8% |
sfpatcher-1 lzma2 | 31.5% |
sfpatcher-2 lzma2 | 29.3% |
sfpatcher-3 lzma2 | 26.7% |
sfpatcher-2pre lzma2 | 81.9% |
sfpatcher-3pre lzma2 | 76.6% |
sfpatcher-0 zstd | 50.9% |
sfpatcher-1 zstd | 32.6% |
sfpatcher-2 zstd | 30.7% |
sfpatcher-3 zstd | 28.3% |
sfpatcher-2pre zstd | 86.3% |
sfpatcher-3pre zstd | 82.3% |
单个apk一次升级节省的流量估算:用户的安卓手机现在经常使用的应用apk一般都越来越大(游戏平均更大),假设按平均100MB算。
一般bsdiff或HDiffPatch创建的补丁平均为原apk大小的50%–60%(按51%计算);sfpatcher按-o-1算,创建的补丁为平均原apk大小的33%。
那sfpatcher相比bsdiff或HDiffPatch方案单次继续节省 100x(51%-33%) = 18MB (游戏apk平均节省会成倍数增加)
假设apk商店每天有1亿次apk升级,按峰值带宽计费,假设单价每天0.48元/Mbps; 而按经验,峰值带宽一般是平均带宽的2倍:
每月节省费用:100000000x(18x1024x1024x8/1000/1000)/(3600x24)x2x0.48*30 = 503.3万元
如果按流量计费,假设单价0.11元/GB:
每月节省费用:100000000x(18/1024)x0.11x30 = 580.1万元
需要商业授权,请联系作者: [email protected]