0%

OTA差分升级技术

  • OTA差分升级,主要是解决整车OTA升级中升级包更新推送不必全量推送的问题。
  • 差分算法用于减小升级包体积,从而缩短下载时间、节省网络带宽、减少用户流量使用。
  • 本文主要介绍几种差分算法,并实践应用一种常见的差分算法。

差分介绍

例如,一个新版本的升级包1G,通过云端差分服务,可以把两个新旧版本的升级包之间有差异的地方抽离出来新生成一个比较小的差分包,这个差分包的体积仅为整包大小的5%-20%,云端只要向车端推送这个比较小的差分包,车端拿到差分包之后,再通过对应的差分还原算法,利用旧包和拿到的差分包还原成新的升级包实现升级包的替换安装。

常见差分算法

OTA差分算法,常见的开源方案有bsdiff、hdiffpatch、xdelta3等:

  • bsdiff:这是一种高效的文件差分算法,常用于生成二进制文件的差异。它在 OTA 更新中被广泛使用。

    项目链接:http://www.daemonology.net/bsdiff/

  • hdiffpatch:这是一个开源的差分和补丁库,可以在二进制文件或目录之间进行差分和补丁。它运行速度快,生成的差分小,支持大文件,且在差分和补丁时内存需求较小。

    项目链接:https://github.com/sisong/HDiffPatch

  • xdelta3:这是一个开源的二进制文件差分算法,它可以生成和应用 VCDIFF 格式的差分和补丁。

    项目链接:https://github.com/jmacd/xdelta

  • courgette:这是 Google Chrome 使用的差分算法,它特别适合于生成二进制可执行文件的差分。

    项目链接:https://chromium.googlesource.com/chromium/src/courgette/

商业解决方案

算法实践

上述几种差分算法都可以完成对升级软件包生成差分包,其中hdiffpatch是开源且性能比较好、占用内存比较少的方案,下面针对该算法做一个详细介绍,并最终利用该算法实现一个REST API供业务调用。

hidffpath的源代码托管在Github上,可以在这里查看和下载。

简介

hdiffpatch 包含两个主要的工具:hdiff 和 hpatch:

  • hdiff:这个工具用于生成两个文件之间的差分。它会比较两个文件的内容,找出它们的差异,然后生成一个差分文件。这个差分文件可以用来将旧版本的文件更新到新版本。

  • hpatch:这个工具用于将差分应用到旧版本的文件上,生成新版本的文件。它会读取旧版本的文件和差分文件,然后根据差分文件的指示修改旧版本的文件,生成新版本的文件。

其接口实现在hdiffz.cpphpatch.c文件中,包含了 hdiffpatch 的主要接口:

1
2
3
4
5
6
7
8
int hdiff_cmd_line(int argc,const char * argv[]);

int hdiff(const char* oldFileName,const char* newFileName,const char* outDiffFileName,
const hdiff_TCompress* compressPlugin,const TDiffSets& diffSets);

int hpatch(const char* oldFileName,const char* diffFileName,const char* outNewFileName,
hpatch_BOOL isLoadOldAll,size_t patchCacheSize,hpatch_StreamPos_t diffDataOffert,
hpatch_StreamPos_t diffDataSize,hpatch_BOOL vcpatch_isChecksum,hpatch_BOOL vcpatch_isInMem);

基于源码自行封装接口参数后得到以下接口:

1
2
int ota_diff(const char *old_path, const char *new_path, const char *diff_path);
int ota_patch(const char *diff_path, const char *old_path, const char *new_path);

编译

  • Linux

    1
    2
    3
    4
    5
    6
    7
    $ git clone https://github.com/sisong/HDiffPatch
    $ cd HDiffPatch
    $ git clone https://github.com/sisong/bzip2.git ../bzip2
    $ git clone https://github.com/sisong/libmd5.git ../libmd5
    $ git clone https://github.com/sisong/lzma.git ../lzma
    $ git clone https://github.com/sisong/zstd.git ../zstd
    $ make LZMA=1 ZSTD=1 MD5=1 BZIP2=1
  • Android

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1.下载安装 Android NDK
    $ wget https://dl.google.com/android/repository/android-ndk-r21d-linux-x86_64.zip
    $ unzip android-ndk-r21d-linux-x86_64.zip

    2.执行build_libs.sh
    $ cd HDiffPatch/builds/android_ndk_jni_mk

    3.在安卓项目中添加 HPatch.java 和 libhpatchz.so 文件,java 代码就可以调用 patch 函数了
    所在路径 HDiffPatch/builds/android_ndk_jni_mk/java/
  • Cross Compile

    • 针对不同平台ECU,利用不同交叉编译工具编译相应平台的还原库。

步骤

  • 把hdiffpatch源文件编译成Linux下的动态库,Java侧调用动态库libhdiffpatch.so

  • 把libhdiffpatch.so文件放在src/main/resource/linux-x86-64/文件夹下

  • Maven引入JNA

    1
    2
    3
    4
    5
    <dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.10.0</version>
    </dependency>
    PS.
      要用Java调用C的接口需要用到JNI,过程较为晦涩繁琐,此处采用JNA调用C代码。
    1. JNA provides Java programs easy access to native shared libraries without writing anything but Java code - no JNI or native code is required.
    2. Doc:https://java-native-access.github.io/jna/5.10.0/javadoc/index.html
  • 创建HDiff类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.hh.ota.diff;

    import com.sun.jna.Library;
    import com.sun.jna.Native;

    public interface HDiff extends Library{
    HDiff INSTANCE = (HDiff) Native.load("hdiffpatch", HDiff.class);
    //diff usage: ota_hdiff [options] oldPath newPath outDiffFile
    void ota_hdiff(String oldData, String newData, String outPatchFile, long size);
    }
  • 创建HPatch类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.hh.ota.patch;

    import com.sun.jna.Library;
    import com.sun.jna.Native;

    public interface HPatch extends Library {
    HPatch INSTANCE = (HPatch) Native.load("hdiffpatch", HPatch.class);
    //patch usage: ota_hpatch [options] oldPath diffFile outNewPath
    void ota_hpatch(String oldPath, String diffFile, String outNewPath);
    }
  • 创建FileUtils类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.hh.ota.cache;

    import java.io.File;
    import java.io.FileNotFoundException;

    public class FileUtils {
    /**
    * Verify if the file exists, if not, throw an exception
    * @param filePath:local file path
    * @throws FileNotFoundException if filePath not exist
    */
    public static void requireFileExist(String filePath) throws FileNotFoundException {
    File file = new File(filePath);
    if (!file.exists()) {
    throw new FileNotFoundException(filePath);
    }
    }
    }
  • JNA实例化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * @param oldFile:old version file path
    * @param newFile:new version file path
    * @param diffFile:diff package file path
    * @throws FileNotFoundException if oldFile or newFile not exist
    */
    public static void diff(String oldFile, String newFile, String diffFile) throws FileNotFoundException {
    FileUtils.requireFileExist(oldFile);
    FileUtils.requireFileExist(newFile);
    HDiff.INSTANCE.hdiffz(oldFile,newFile, diffFile, 1024*1024*128);
    }

    /**
    * @param oldPath:old version file path
    * @param diffPath:diff package file path
    * @param outNewPath:patch out file path
    * @throws FileNotFoundException if oldFile or patchFile not exist
    */
    public static void patch(String oldPath, String diffPath, String outNewPath) throws FileNotFoundException {
    FileUtils.requireFileExist(oldPath);
    FileUtils.requireFileExist(diffPath);
    HPatch.INSTANCE.hpatchz(oldPath, diffPath, outNewPath);
    }
  • 创建调用主类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) {
    String oldFile = "/home/jack_xie/cloud_api_java/bigFile/soc_old.zip";
    String newFile = "/home/jack_xie/cloud_api_java/bigFile/soc_new.zip";
    String diffFile = "/home/jack_xie/cloud_api_java/bigFile/soc_diff.zip";
    String outNewPath = "/home/jack_xie/cloud_api_java/bigFile/soc_patch.zip";
    try {
    diff(oldFile, newFile, diffFile);
    patch(oldFile, diffFile, outNewPath);
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    }
    }
  • Java层调用接口结果
    image.png

  • Java层封装REST接口
    将上述 Java 方法封装成 REST API供外部调用,使用 Spring Boot 框架实现。