一次CodePush热更失败的总结

最近将codepush热更服务器替换成使用开源版本自建服务器后,更新速度和成功率都有不错的提升。

然而在推送某次热更新时,IOS端爆出了问题:在本地已经存在一次热更新时,二次更新会失败。

sentry监控平台上查出如下错误

1
The update contents failed the data integrity check

经过查询 native 端的源码,发现当下载文件的 hash 校验失败时才会抛出这个错误,也就是说,服务器返回的文件和 hash 值不匹配。

先简单说一下 codepush 对文件 hash 的计算规则吧。

计算规则

codepush 对文件的 hash 计算规则如下

  1. 使用 sha256 算法
  2. 将 bundle 文件夹内的每一个文件路径(相对于 bundle 文件夹的相对路径)计算出来,并对每一个文件使用 sha256 算法计算 hash 值,将键值对存放在 {path: hash} 的对象中。
  3. 将对象转换成 [path:hash]形式的数组,并对数组进行最简单的自增排序,如 js 中 sort 方法的默认方式。
  4. 将数组序列化成字符串,由于在客户端端的 oc 和 java 在序列化时会将 / 转换成 \/,所以还需要对路径进行全局替换。由于现在热更服务端是用 nodejs 编写的,所以并没有这一步。
  5. 将生成字符串再次使用 sha256 进行计算,最终得出的结果便是用于校验整个更新包的 hash 值。

使用这种方法,codepush 保证了单个文件的文件的完整性和整个更新包的可靠性。正常来说应该是非常可靠的。

问题

接下来看遇到的问题

  1. 首次热更新 IOS 时可以成功的,因为 app 打包时没有推送 bundle 包到服务端,所以第一次更新是全量的。而全量更新时,codepush 并不会进行 hash 校验。
  2. IOS 端无法更新,但是安卓端却是可以正常更新的,也就是说服务端计算出来的应该是没问题的,问题可能出现在 IOS 客户端。

为了验证这个问题,我参考服务端的 hash 生成规则,将生成的 bundle 文件夹进行本地计算,证实了上面的想法,ios 端计算出来的 hash 值确实不一致。

排查原因

经过热更和断点排查,最终在对上面第 4 步生成的字符串与本地测试生成的进行对比时发现了问题,字符串中有一段 assets\/qualification_workcert_one@2x.png 中的 \/ 没有被替换成 /

这是 ios 替换路径的代码:

1
2
3
// The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png"
manifestString = [manifestString stringByReplacingOccurrencesOfString:@"\\/"
withString:@"/"];

最让人迷惑的问题来了,为什么没有替换成功?在肉眼看来,并没有什么异样。

纠结了一阵,想起了网上曾经有一位同行因为同事的一个特殊字符差点崩溃的故事,心想该不会也是这个问题吧?

于是尝试删除这个字符串,在 \ִ/ִ/ 前尝试删除/时果然没有成功,说明有一个看不见(其实看得见,很小)的字符在这里。

删除那个字符后,果然 git 检测了到了文件删除和添加,对这个文件引用的地方也抛出了错误。

image

将文件名修复后,重新打包热更,一切变恢复正常了。

特殊字符

这个字符是什么,为什么在 js 和 java 没有问题,而到了 oc 却有问题呢?

计算的其 unicode 编码为 \u05b4, 十进制为 1460,身份是 HEBREW POINT HIRIQ,属于希伯来语中的标记字符。

从表现形式上看,这个字符有影响周边字符的效果,如下图中左边的"都已经变形了,那么这个字符可能是个文字的修饰符?苦于对希伯来语一窍不通,并没有查出个所以然来。

那么还是回归原点,用代码证实此符号对其他字符的影响。

在 js 中能正常替换。


java 中的效果同 js

oc 如期无法正常替换

至于为什么在oc上会出现替换不成功,待后续研究吧。

总结

这个文件为什么会出现这个字符已经不得而知,但是肯定的是,我们在开发过程中,应该尽量避免输入特殊字符。特别是当引用的文件名看起来是正确的,编辑器仍在报错时,需要引起警觉。