热修复时的文件下发模式和文件Diff

326 阅读5分钟

热修复框架的另外一个核心功能就是如何从服务器端下发热修复包到客户端。热修复包的如何组织,如何减少热修复包体积,更是提高修复包下发成功率的重中之重。

我们这边设计了3种文件下发模式。Full、Half、Diff模式。

Full模式

这种模式指的是所有资源整包下发,包括里面的代码文件和图片等资源文件。这种模式的特点就是实现简单,功能也稳定。但是一般热修复的成功率受限于包体积。这种模式下也意味着包体积往往都很大,通常最终热修复的成功率并不高。所以热修复时用这种模式做个兜底,一般不会让走这种模式下发数据包。但凡事都有例外,不能一看是整包下发就觉得low。例如做过的WebView离线包项目,这个项目中对每个应用包做了500kb的限制,所以这个项目就采用的是整包下发。因为提交够小,在没有上diff算法的情况下成功率就能走到95%以上。

Half模式

Half模式指的是不使用任何高级的第三方diff算法SDK库,只使用简单的差分逻辑来实现的一种diff策略。

热修复场景描述_20230513214557.png

例如上图所示的场景:存在两个版本,一个是有bug的版本,一个是修复版本。生成差分补丁的步骤如下:

  • 遍历两个project的目录,生成Map结构的遍历结果。(Map的具体生成逻辑和结构细节见另外一篇文章文件一致性校验)以 bug Version的Map为基准做循环,每次循环拿到key后,去fix version的Map结果中查询,如果不能查询到记录,那认为该文件是被删掉了,标记为del,比如图中的x2.png文件。如果可以查询到记录,比对两条记录的Value即文件的Hash值。如果一致,做忽略处理。不一致,那么认为该文件被修改。将fix version 中的文件拷贝进补丁文件包中,并且将该文件标记为mod模式。例如图中的xxx.bundle文件。此时再删除掉fix Version Map中的记录。循环操作做完后,fix Version Map依然遗留的文件标记为add,例如图中的p2.png文件。
  • 用伪代码标示这个过程为:
bug_version_map.forEach{ key1, value -->
    if(fix_version_map.contain(key1)){
        val value2 = fix_version_map.get(key2)
        if(value1 != value2){
            文件标记为mod类型,copy文件到补丁中
        }
        fix_version_map.delete(key1) 删除掉key1文件在修复版本扫描结果中的记录
    }else {
        key1 对应的文件标记为del类型
    }
}
​
if(fix_version_map.size > 0) {
    fix_version_map中遗留的文件标记为add类型,copy到补丁中去
}

最终生成的补丁结构为:

Half模式补丁_20230513214645.png

还需要一个config文件来标记补丁如何使用。根据上面的流程,config文件的内容为:

{ 
    "mod": ["root/xxx.bundle"], 
    "del": ["root/drawable-xhdpi/x2.png"], 
    "add": ["root/drawable-hdpi/p2.png"] 
}
  • 这种模式下,补丁包里包括一个config指引文件,指引客户端接收后如何做文件合并。外加add类型和mod类型的文件。合并时按照指引文件,add类型的文件,直接从补丁包中拷贝到目标地址。del类型的文件,去目标位置上检查是否有同名文件,如果有做删除处理。mod类型的文件,需要在目标位置检查,如果存在同名文件先删除,再从补丁包中拷贝文件到目标位置。

这就是Half模式下,热修复补丁文件的生成和补丁使用流程步骤。

Diff模式

diff模式其实就是上面Half模式的改进,针对的时上面的mod类型的文件。目的是为了进一步减少热更新包的体积,从而增加最终的成功率。比如上图中的xxx.bundle文件如果对应的是我们的代码文件,并且我们的项目非常庞大时,编译完成后该文件的体积通常都很大,基本起步都是按M来计算。采用Half模式是需要将整个包拷贝到补丁包中的,此时整个补丁包都是因为这个文件体积大增。diff模式就是为了解决这个问题。

遇到mod类型的文件时,我们使用diff SDK(比如bsdiff SDK等)来生成补丁文件。再将补丁文件copy到补丁包中去。生成的补丁包结构就会是这样:

diff模式补丁_20230513214631.png

config指引文件结构也会发生变化:

{ 
    "mod": ["root/xxx.bundle"], 
    "del": ["root/drawable-xhdpi/x2.png"], 
    "diff": [{"root/xxx.bundle",“patch_root/xxx.bundle/patch”}] 
}

指引文件会告诉客户端哪些文件是经过diff处理的。合并时先根据文件名在bug Version 包里找到同名文件作为base包,再从patch补丁包中找到补丁文件,进而使用bsdiff库做合并处理,生成修复bug后的xxx.bundle文件。

三种模式的简单对比

--Full-Half-Diff
实现技术难度简单中等复杂
补丁包体积范围几时kb ~ 几时M几时kb ~ 几M平均几时kb
成功率一般

为什么要折腾出来这三种下发模式呢?其实还是为了提高最终的热修复成功率。

  1. 客户端启动时我们根据其版本号和上送的base 文件hash值,就能确定其内容,进而给客户端下发diff模式的更新包。
  2. 如果安装过程中客户端由于网络等其他原因导致异常,客户端自行下载full类型的热更新包即可。(一般这种概率极少,万分之几)
  3. 一般Half只是作为diff模式包的替补,如果监控系统里发现diff包大面积不兼容、无法安装。那大概率diff包使用的bsdiff SDK算法可能有异常,此时重新用half模式生成新的热更新包,下掉旧的diff模式包。客户端更新时下发新的Half模式包即可。
  4. 这里就是通过层层兜底,最终来提高热更新的最终成功率。