前言

作为技术方向选型的重点,热更新/热修复是一个绕不过去的问题。本文将介绍目前的React Native(简称RN)解决方案,之后重点介绍我们即将采用的方案(包括源代码)。

React Native热更新分析

React Native热更新核心的问题是如何进行js代码的动态更新。如果不考虑更新包的大小,完全可以将整个js代码包(即编译后的jsbundle)放到服务器,由客户端来进行更新,可如果为了修复一个bug,要下载所有的js代码,接受不了...
怎么办?拆分!RN热更新最核心的一点是编译后的jsbundle是稳定的,即如果代码不变的情况下,每次编译后的jsbundle是一样的;而如果只是改动了部分代码,编译后前后差异就在这改动的代码。当然前提是RN的版本是不变的情况。
基于这点,目前公开的热更新方案一个是微软的Code Push,一个是React Native中文网中的react-native-pushy。通过这篇小文对这两个方案的实际分析应用来看,比较麻烦,容易出错。58同城也有对这两个方案进行了分析,并根据自身的业务特点实现了自己的热更新方案。

我们的方案

热更新的方案是基于jsbundle的稳定性,我们的方案也是这样。我们方案的确定包括两个主要的点:1)jsbundle差异化处理;2)jsbundle的加载逻辑理解。理解了这两点,就理解了我们的方案:即在一个迭代周期中,上线版本包括所有的js代码(基础jsbundle),随后产生的多个热更新都将和这个基础jsbundle进行差异化处理,产生多个补丁,每个新的补丁覆盖前一个补丁,即对每一个线上版本始终需要加载一个补丁。客户端下载补丁后,重新加载基础jsbundle,在加载过程中将最新下载的jsbundle合并到基础jsbundle中,实现热更新。
我们的这个方案并非一个完美方案,大家的方案都不是。原因在于,热更新中的js代码依赖线上的RN环境,依赖客户端提供的桥接接口,一旦出现当前的接口环境不支持新业务的开发,客户端版本就需要迭代。因此可以说,RN可以减少发版的次数,并不是说完全不用发版了。
这个问题清楚了,对于补丁可能很大的顾虑就可以消除了。据测试发现,修改了一个文件,补丁小于1KB。

1) 生成补丁

差异化代码的拆分和合并使用了google-diff-match-patch,支持各个平台。以下示例代码为iOS,安卓/js对应的方法类似。
基本的思路是,将基础jsbundle和包含热更新jsbundle转为string,然后对string进行比较,最后将差异代码存储为文件放到server端。
这里是生成补丁的代码:

+ (NSString *)getDiffOfOldString:(NSString *)oldString newString:(NSString *)newString
{
    DiffMatchPatch  *diffMatchPatch = [[DiffMatchPatch alloc] init];  

    NSMutableArray *diff = [diffMatchPatch diff_mainOfOldString:oldString andNewString:newString];
    
    NSMutableArray *patchDiff = [diffMatchPatch patch_makeFromDiffs:diff];
    NSString *patchString = [diffMatchPatch patch_toText:patchDiff];
    
    return patchString;
}

实际操作中建议写一个简单的生成补丁的web页面,支持上传基础jsbundle和新jsbundle,这样可以方便的获取补丁文件、测试以及上传。

2) 合并补丁

客户端检测到server端有新的补丁后进行下载,下载后通知RN重新加载基础jsbundle,在加载的过程中将新的补丁合并到基础jsbundle中,随后一并加载到内存中。合并的过程需要hack jsbundle的加载类:

@interface RCTBatchedBridge (RN)
@end
@implementation RCTBatchedBridge (RN)
+ (void)load
{
    NSError *error = nil;
    [self jr_swizzleMethod:@selector(executeSourceCode:) withMethod:@selector(wb_executeSourceCode:) error:&error];
    if (error) {
        NSLog(@"inject patch code fail: %@", error);
    }
}
- (void)wb_executeSourceCode:(NSData *)sourceCode
{
    //合并patch
    if ([WBBridgeManager sharedManager].hasNewPatch) {
        sourceCode = [WBPatchManger combinePatchWithSourceCode:sourceCode];
    }
    [self wb_executeSourceCode:sourceCode];
}
@end

上面的sourceCode就是jsbundle加载到内存中的二进制数据,将其转为string,补丁也转为string,将这两个string进行合并(代码如下),再转换为新的二进制数据,最后将新的二进制数据加入到加载流程中,从而实现热更新。

+ (NSString *)combinePatch:(NSString *)patchString withOldString:(NSString *)oldString
{
    DiffMatchPatch  *diffMatchPatch = [[DiffMatchPatch alloc] init];
    
    NSError *error = nil;
    NSMutableArray *patchs = [diffMatchPatch patch_fromText:patchString error:&error];
    if (error) {
        NSLog(@"diff error: %@", error);
    }
    
    NSArray *result = [diffMatchPatch patch_apply:patchs toString:oldString];
    
    return  result && result.count > 0 ? result[0] : oldString;
}

如果合并出现

AssertMacros: hash <= (~(UniChar)0x00), Hash value has exceeded UniCharMax! file: /Users/…/Pods/Google-Diff-Match-Patch/DiffMatchPatchCFUtilities.c, line: 391

错误,请检查存\取文件的数据类型是否一致。

后记

涉及热更新最核心的部分已经介绍完了。

回头看看我们的热更新方案,生成补丁的方法简单、补丁小、客户端热更新下载/合并逻辑简单,基本满足了我们对于热更新的需求。