需求
OTA(Over-The-Air)升级是一种至关重要的技术,用于更新嵌入式设备的固件或软件,以确保设备具备最新功能和修复漏洞。在OTA升级过程中,使用差异算法工具(如bsdiff、hdiffpatch和xdelta3)能够有效减小升级包的大小,从而降低带宽消耗并提高升级效率。以下是对差异算法在OTA升级中的重要性的更详细说明:
-
带宽节省:通过使用差异算法,云端可以提取新旧升级包之间的差异数据,生成相对较小的差异包。这意味着只需传输差异包,而不是整个新升级包。这极大地降低了数据传输所需的带宽,尤其对于大型升级包而言。
-
成本降低:减少传输数据量不仅有助于带宽节省,还能减少数据传输费用。这对于云端提供OTA服务的成本管理非常关键。
-
快速升级:较小的差异包能更快速地下载到设备上,且设备更容易处理和应用差异数据。这减少了升级过程所需的时间,有助于确保用户设备迅速获取最新功能和修复。
-
存储空间节省:设备的存储空间是有限的,较小的差异包需要更少的存储空间,对设备而言非常有益。
-
版本控制:差异算法还能轻松进行版本控制,确保设备获取正确的升级。
升级流程
OTA升级流程:
-
计算差分包:云端根据升级需求,调用差分服务,输入文件 V1(当前版本)和文件 V2(新版本)来计算差分包 PATCH(差异数据)。hdiffpatch 将生成一个相对较小的 PATCH 差分包,其中包含了新版本相对于当前版本的差异信息。
-
通知设备下载差分包:云端通知设备有可用的升级。设备收到通知后,开始下载差分包 PATCH。
-
应用差分包:设备成功下载差分包 PATCH 后,它使用文件 V1(当前版本)和差分包 PATCH 来还原文件 V2(新版本)。设备内部的 hdiffpatch 工具将应用 PATCH 差异数据到当前版本文件 V1 上,生成新版本文件 V2。这个过程是快速的,因为 PATCH 包是相对较小的,只包含了需要修改的数据。
-
安全性检查:设备在应用差分包后,可以进行安全性检查来确保新版本文件 V2 完整和正确。这可以包括校验文件的哈希值或数字签名,以防止潜在的数据损坏或恶意修改。
-
更新完成通知:设备在成功应用差分包且通过安全性检查后,通知云端升级完成。云端记录设备已完成升级,以便进行版本跟踪和管理。
通过这一流程,我们能够充分利用 hdiffpatch 差异算法,将升级包的大小减小,降低了升级过程中的带宽需求,同时保持了升级的效率。这对于嵌入式设备的OTA升级是一个优化的、可行的方案。
方案设计
技术路线
-
BsDiff:BsDiff(Binary Software Differential)是一种用于生成二进制文件差异(差异数据)的算法。通常用于比较原始文件和新文件,生成差异文件,然后将差异文件应用到原始文件,生成新文件。BsDiff的核心思想是将二进制文件划分成块,计算块之间的差异,然后将这些差异编码成差异文件。BsDiff算法的目标是生成最小的差异数据,以在生成和应用差异时尽可能减小文件大小并提高效率。它在软件更新、版本控制等领域非常有用,因为它减小升级文件的大小,降低下载和存储成本。
-
hdiffpatch:hdiffpatch 是另一个差异生成和应用工具,专门用于减小文件大小,特别是用于固件和软件的更新。它被设计为高性能、低内存占用的工具,可以生成小差异文件以减小升级包的大小,从而节省带宽和提高升级效率。hdiffpatch的基本原理是将文件划分成块,找到相匹配的块,计算不匹配块之间的差异,将差异数据编码成差异文件,然后使用差异文件来应用差异数据以生成新文件。hdiffpatch强调高性能和低内存占用,适用于嵌入式设备和固件更新等场景。它的主要优势在于生成较小的差异文件,减小升级包的大小,降低带宽消耗,以及节省存储空间。
技术对比
BsDiff 和 hdiffpatch 都是用于生成和应用二进制文件差异的算法,用于减小升级包的大小,从而节省带宽和降低成本。它们有各自的特点和优势,下面对它们进行比较:
BsDiff:
-
基本原理:BsDiff 将文件划分成块,找到匹配的块,计算不匹配块之间的差异,然后编码差异文件。差异文件可以应用到原始文件上,生成新文件。
-
优势:
- 生成的差异文件通常较小,节省带宽。
- 被广泛用于软件更新、版本控制等领域。
- 相对成熟的开源实现可供使用。
-
劣势:
- 生成和应用差异的性能可能不如 hdiffpatch。
- 对于某些文件类型,可能生成较大的差异文件。
hdiffpatch:
-
基本原理:hdiffpatch 也将文件划分成块,找到匹配的块,计算不匹配块之间的差异,然后编码差异文件。差异文件可以应用到原始文件上,生成新文件。
-
优势:
- 高性能,生成和应用差异的速度较快。
- 低内存占用,适合嵌入式设备等资源受限的环境。
- 生成的差异文件通常较小,节省带宽和存储空间。
- 支持多平台,可在不同操作系统上使用。
-
劣势:
- 可能不如 BsDiff 在某些文件类型上表现出色。
- 开源实现相对较新,可能需要更多的定制和集成工作。
技术选型
考虑到本次方案将面向多种嵌入式设备arm和x86架构,我们决定采用 hdiffpatch 作为差异算法的核心。主要原因如下:
-
通用性:hdiffpatch 是一种通用性较强的差异算法,适用于各种嵌入式设备。这意味着我们可以在不同设备上实现统一的OTA升级方案,而不需要为每种设备单独开发和维护不同的差异算法。
-
高性能:hdiffpatch 被设计为高性能工具,特别是在生成和应用差异数据时表现出色。对于无人车等对性能要求较高的设备,这一特点尤为重要,因为它可以加速升级过程,降低用户设备的升级时间。
-
低内存占用:嵌入式设备通常拥有有限的内存资源,hdiffpatch 的低内存占用使其非常适合这些资源受限的环境。这将有助于确保升级过程不会占用过多内存,从而不影响设备的正常运行。
-
小差异文件:hdiffpatch 生成的差异文件通常较小,减小了升级包的大小。这对于需要在有限带宽下进行OTA升级的设备(例如无人车)非常重要。
-
多平台支持:hdiffpatch 支持多种操作系统和平台,这意味着可以在不同嵌入式设备上使用,无论其运行的操作系统是什么。这种灵活性将简化方案的部署和维护。
-
版本控制:差异算法允许进行版本控制,确保设备得到正确的升级。这在嵌入式设备的OTA升级中至关重要,因为需要确保安全性和正确性。
因此,通过采用 hdiffpatch 差异算法,我们可以实现一种通用、高性能、低内存占用的OTA升级方案,适用于多种嵌入式设备。这将有助于确保设备快速、安全地获得最新的功能和修复,同时减小了部署和维护成本,提高了升级效率。
基本测试
升级示意图
下图展示了通过两种方式,将图1升级到图片2升级的过程
极端情况测试
-
文件完全不一致
在文件完全不一致的情况下,采用2张完全不一样的图片进行测试,
1.jpg
和2.jpg
内容完全不一致,普通升级需要传输 596KB 数据,差分升级需要传输 488KB 数据。文件类型 普通升级 差分升级 1.jpg 629 KB 629 KB 2.jpg 596 KB 0 KB PATCHA文件 0 KB 488 KB -
文件增量
在文件增量情况下,采用2份txt文件进行测试,
2.txt
在1.txt
基础上进行了字符串增加,普通升级需要传输 119KB 数据,差分升级仅需要传输 0.11KB 数据。文件类型 普通升级 差分升级 1.txt 11.9 KB 11.9 KB 2.txt 119 KB 0 KB PATCHA文件 0 KB 0.11KB
参考资料
-
项目地址
https://github.com/sisong/HDiffPatch/releases/tag/v4.6.7
-
SDK
linux64
arm64
android
windows64
source code
-
java参考代码
HDiffPatch 的官方实现是基于 C/C++ 的库。虽然它没有官方的 Java 实现,但你可以通过 Java 的 JNI(Java Native Interface)机制来调用 C/C++ 库中的函数。以下是一个简单的示例,演示如何使用 JNI 在 Java 中调用 HDiffPatch 的 C/C++ 函数。
首先,需要编写一个 Java 类,用于加载 HDiffPatch 动态链接库并定义与 C/C++ 函数的映射。以下是一个示例 Java 类:
import java.io.*; import java.nio.file.*; import java.nio.ByteBuffer; public class HDiffPatch { static { System.loadLibrary("hdiff"); // 请根据你的库名称进行修改 } // JNI 声明:生成差异数据 private static native int createDiff(byte[] oldData, byte[] newData, byte[] diffData); // JNI 声明:应用差异数据 private static native int applyDiff(byte[] oldData, byte[] diffData, byte[] newData); public static void generateDiffData(String oldFilePath, String newFilePath, String diffFilePath) { try { byte[] oldData = Files.readAllBytes(Paths.get(oldFilePath)); byte[] newData = Files.readAllBytes(Paths.get(newFilePath)); byte[] diffData = new byte[Math.max(oldData.length, newData.length)]; // 设置差异数据的大小 int result = createDiff(oldData, newData, diffData); if (result == 0) { Files.write(Paths.get(diffFilePath), diffData); System.out.println("差异数据生成成功!"); } else { System.out.println("差异数据生成失败。"); } } catch (IOException e) { e.printStackTrace(); } } public static void applyDiffData(String oldFilePath, String diffFilePath, String newFilePath) { try { byte[] oldData = Files.readAllBytes(Paths.get(oldFilePath)); byte[] diffData = Files.readAllBytes(Paths.get(diffFilePath)); byte[] newData = new byte[oldData.length + diffData.length]; // 设置新文件的最大可能大小 int result = applyDiff(oldData, diffData, newData); if (result == 0) { Files.write(Paths.get(newFilePath), newData); System.out.println("差异数据应用成功!"); } else { System.out.println("差异数据应用失败。"); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { String oldFile = "old.bin"; String newFile = "new.bin"; String diffFile = "diff.bin"; String restoredFile = "restored.bin"; // 生成差异数据 generateDiffData(oldFile, newFile, diffFile); // 应用差异数据 applyDiffData(oldFile, diffFile, restoredFile); } }
上述示例代码假定你已经编译了 HDiffPatch 的 C/C++ 动态链接库(.so 文件),并将其放在正确的位置。你需要根据你的库文件名和路径进行修改。请注意,这是一个简单的示例,你可能需要根据你的项目需求和环境进行更复杂的设置和错误处理。
-
c++参考代码
下面是一个基本的 C++ 示例,展示如何使用 HDiffPatch 来生成差异数据和应用差异数据以还原文件。请注意,HDiffPatch 需要在项目中添加相关的头文件和链接到库文件。
#include <iostream> #include <vector> #include "HDiff/diff.h" // 请根据你的项目实际情况包含正确的头文件 int main() { // 原始文件数据 std::vector<uint8_t> oldData = {1, 2, 3, 4, 5}; // 新文件数据 std::vector<uint8_t> newData = {1, 2, 3, 9, 5}; // 更改第四个字节的值 // 差异数据存储 std::vector<uint8_t> diffData; // 生成差异数据 int result = create_diff(oldData.data(), oldData.size(), newData.data(), newData.size(), diffData); if (result == 0) { // 差异数据生成成功,你可以保存它或传输给其他设备 std::cout << "差异数据生成成功!" << std::endl; } else { std::cout << "差异数据生成失败。" << std::endl; return 1; } // 还原新文件 std::vector<uint8_t> restoredData; restoredData.resize(oldData.size() + diffData.size()); result = apply_diff(oldData.data(), oldData.size(), diffData.data(), diffData.size(), restoredData.data(), restoredData.size()); if (result == 0) { // 新文件已还原,你可以保存它 std::cout << "新文件还原成功!" << std::endl; } else { std::cout << "新文件还原失败。" << std::endl; return 1; } return 0; }