美文网首页一起学起来reactNativeHybird开发
React-Native 实现增量热更新的思路(1)

React-Native 实现增量热更新的思路(1)

作者: 3z鸿杰Libra | 来源:发表于2016-04-07 14:19 被阅读23174次

所谓热更新就是在不重新安装的前提下进行代码和资源的更新,相信在整个宇宙中还不存在觉得热更新不重要的程序猿。

增量热更新就更牛逼了,只需要把修改过和新增的代码和资源推送给用户下载即可,增量部分的代码和资源都比较小,所以整个热更新流程可以在用户无感的情况下完成,我已经想不到更好的更新方式可以让我装更大的逼了。

一.实现脚本的热更新
1.为什么可以热更新

简单地说,因为RN是使用脚本语言来编写的,所谓脚本语言就是不需要编译就可以运行的语言,也就是“即读即运行”。我们在“读”之前将之替换成新版本的脚本,运行时执行的便是新的逻辑了,稍微抽象一下,图片资源是不是也是“即读即运行”?所以脚本本质上和图片资源一样,都是可以进行热更新的。

2.RN加载脚本的机制

要实现RN的脚本热更新,我们要搞明白RN是如何去加载脚本的。
在编写业务逻辑的时候,我们会有许多个js文件,打包的时候RN会将这些个js文件打包成一个叫index.android.bundle(ios的是index.ios.bundle)的文件,所有的js代码(包括rn源代码、第三方库、业务逻辑的代码)都在这一个文件里,启动App时会第一时间加载bundle文件,所以脚本热更新要做的事情就是替换掉这个bundle文件。

3.生成bundle文件

我们在RN项目根目执行以下命令来得到bundle文件和图片资源:

react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

其中--entry是入口js文件,android系统就是index.android.js,ios系统就是index.ios.js,--bundle-output就是生成的bundle文件路径,--platform是平台,--assets-dest是图片资源的输出目录,这个在后面的图片增量更新中会用到,--dev表示是否是开发版本,打正式版的安装包时我们将其赋值为false。
生成的bundle文件体积还是不小的,空项目的话恐怕至少也有900K,所以我们将其打成zip包并放到web服务器上以供客户端去下载。

4.下载bundle文件

下载文件可以使用原生语言来写,也可以使用js实现,我个人推荐使用React Native FileTransfer来实现下载功能。
实现方法很简单:

import FileTransfer from 'react-native-file-transfer';

let fileTransfer = new FileTransfer();
fileTransfer.onprogress = (progress) => {
  console.log(parseInt(progress.loaded * 100 / progress.total))
};
// url:新版本bundle的zip的url地址
// bundlePath:存在新版本bundle的路径
// unzipJSZipFile:下载完成后执行的回调方法,这里是解压缩zip
fileTransfer.download(url, bundlePath, unzipJSZipFile, (err) => {
    console.log(err);
  }, true
);

解压缩的工作我们可以使用react-native-zip来完成。

import Zip from 'react-native-zip';

function unzipJSZipFile() {
  // zipPath:zip的路径
 // documentPath:解压到的目录
  Zip.unzip(zipPath, documentPath, (err)=>{
    if (err) {
      // 解压失败
    } else {
      // 解压成功,将zip删除
      fs.unlink(zipPath).then(() => {
        // 通过解压得到的补丁文件生成最新版的jsBundle
      });
    }
  });
}

解压成功后,我们使用react-native-fs来将zip删除。

5.替换bundle文件

安装包中的bundle文件是在asset目录下的,而asset目录我们是没有写权限的,所以我们不能修改安装包中的bundle文件。好在RN中提供了修改读取bundle路径的方法。以android为例(ios的类似),在ReactActivity类中有这么一个方法:

/**
 * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
 * from a custom path. By default it is loaded from Android assets, from a path specified
 * by {@link getBundleAssetName}.
 * e.g. "file://sdcard/myapp_cache/index.android.bundle"
 */
protected @Nullable String getJSBundleFile() {
  return null;
}

该方法返回了一个自定义的bundle文件路径,如果返回默认值null,RN会读取asset里的bundle。我们在MainActivity类中重写这个方法,返回可写目录一下的bundle文件路径:

@Override
protected @Nullable String getJSBundleFile() {
    String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
    File file = new File(jsBundleFile);
    return file != null && file.exists() ? jsBundleFile : null;
}

如果可写目录下没有bundle文件,还是返回null,RN依然读取的是asset中的bundle,如果可写目录下存在bundle,RN就会读取可写目录下的bundle文件。

我们将下载好的zip解压到getFilesDir().getAbsolutePath()目录下,再次启动App时便会读取该目录下的bundle文件了,以后再有新版本的bundle文件,依然是下载、解压并覆盖掉这个bundler文件,至此,我们便完成了代码的热更新工作。

6.图片不见了

当我们使用可写目录下的bundle文件时会出现一个很严重的问题:所有的本地图片资源都无法显示了。

我们的图片资源都是通过require来获取的:

<Image source={require('./imgs/test.png')} />

为了找到图片消失的原因,我们打开image.android.js或者image.ios.js,找到渲染图片的方法:

render: function() {
  var source = resolveAssetSource(this.props.source);
  var loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
  // ...
}

原来是通过resolveAssetSource方法来获取资源,那么找到resolveAssetSource方法:

function resolveAssetSource(source: any): ?ResolvedAssetSource {
  if (typeof source === 'object') {
    return source;
  }

  var asset = AssetRegistry.getAssetByID(source);
  if (asset) {
    return assetToImageSource(asset);
  }

  return null;
}

function assetToImageSource(asset): ResolvedAssetSource {
  var devServerURL = getDevServerURL();
  return {
    __packager_asset: true,
    width: asset.width,
    height: asset.height,
    uri: devServerURL ? getPathOnDevserver(devServerURL, asset) : getPathInArchive(asset),
    scale: pickScale(asset.scales, PixelRatio.get()),
  };
}

又发现是通过getPathInArchive方法来获取资源的,那么继续找到getPathInArchive方法:

/**
 * Returns the path at which the asset can be found in the archive
 */
function getPathInArchive(asset) {
  var offlinePath = getOfflinePath();
  if (Platform.OS === 'android') {
    if (offlinePath) {
      // E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
      return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
    }
    // E.g. 'assets_awesomemodule_icon'
    // The Android resource system picks the correct scale.
    return assetPathUtils.getAndroidResourceIdentifier(asset);
  } else {
    // E.g. '/assets/AwesomeModule/icon@2x.png'
    return offlinePath + getScaledAssetPath(asset);
  }
}

该方法的逻辑是如果有离线脚本,那么就从该脚本所在目录里寻找图片资源,否则就从asset中读取图片资源,所谓离线脚本就是我们刚刚下载并解压的bundle文件,而我们并没有将图片资源放在这个目录下,所以所有的图片都不见了。
找到原因就好办了,我们在使用bundle命令生成bundle文件的时候也将图片资源输出出来了,那打包bundle文件的时候我们将所有图片也一并打包进zip,客户端下载zip并解压缩后,客户端可写目录下也就有了所有的图片资源,这样就即实现了脚本的热更新又实现了图片的热更新。

二.减小更新包体积

将一个完整bundle文件和所有图片都打成zip,zip的体积让人不敢直视。

1.增量更新图片

每一次的版本更新我们都将所有图片装进zip包未免有点太任性了,其实我们只需要将修改过和新增的图片资源放进zip就行了。
我们修改一下获取图片资源的方法里的逻辑:

/**
 * Returns the path at which the asset can be found in the archive
 */
function getPathInArchive(asset) {
  var offlinePath = getOfflinePath();
  if (Platform.OS === 'android') {
    if (offlinePath) {
      // 热更新修改  开始
      if(global.patchList){
        let picName = `${asset.name}.${asset.type}`;
        for (let i = 0; i < global.patchList.length; i++) {
          if(global.patchList[i].endsWith(picName)){
            return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
          }
        }
      }
      // 热更新修改  结束
      // E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
      // return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
    }
    // E.g. 'assets_awesomemodule_icon'
    // The Android resource system picks the correct scale.
    return assetPathUtils.getAndroidResourceIdentifier(asset);
  } else {
    // E.g. '/assets/AwesomeModule/icon@2x.png'
    return offlinePath + getScaledAssetPath(asset);
  }
}

其中global.patchList是一个数组,里面放的是自安装包版本以来所有修改过和新增的图片名,如果访问的图片名在这个数组中就从离线脚本所在目录里寻找图片资源,否则还是从asset中寻找图片资源。
我们在打包zip的时候,就只装修改过和新增的图片,并将这些图片名记录在更新配置文件里,客户端去读取更新配置文件时将配置中的图片名读取到并生成global.patchList,这样我们的更新包就小了许多了。
这么做的缺点就是每次更新RN版本的时候,都需要修改下RN的源码,不过我觉得这点小麻烦还是可以接受的,毕竟已上线的产品,我们还是以稳定为主,能不升级RN就不升级RN。

2.增量更新脚本

bundle文件的体积,我们也得想想办法去减少它。
有两种思路:

  1. 分离bundle。bundle里存放了RN源码、第三方库代码和业务逻辑代码,其中频繁更新的就只有业务逻辑代码,所以我们将RN源码和第三方库代码打包成一个bundle,业务逻辑打包成一个bundle,热更新的时候就只更新业务逻辑的bundle即可。

  2. 打包补丁文件。我们可以使用bsdiff对比两个版本的bundle文件得到差异文件,也就是“补丁”,客户端下载好补丁文件,将其与本地的bundle进行融合从而得到最新版本的bundle文件。

这里重点讲解第二个思路的做法。

  1. 生成补丁。

我们从bsdiff官网上下载到最新的源码,然后进行编译就得到可执行的二进制文件了。

如果是win系统,可以直接到我的百度网盘下载,下载密码:zq1x。解压下载好的zip,使用命令行进入到bsdiff的目录,输入命令:

bsdiff a.txt b.txt c.pat

上面的命令就是生成a.txt、b.txt两个文件的补丁c.pat。

如果是linux系统,可以依次执行以下命令:

yum install bzip2-devel
wget http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
tar zxvf bsdiff-4.3.tar.gz
cd bsdiff-4.3

编译完成后,会在目录下生成2个二进制文件:bsdiff、bspatch,这2个二进制文件可以直接使用,不过推荐拷贝到/usr/local/sbin/下:

cp bsdiff /usr/local/sbin/
cp bspatch /usr/local/sbin/

这样就可以在命令行中直接使用了:

bsdiff a.txt b.txt c.pat
  1. 使用补丁。
    得到了补丁文件,下一步就会使用补丁了,拿上面的a.txt、b.txt、c.pat做测试:
bspatch a.txt d.txt c.pat

得到文件d.txt,将其开打看看是否和b.txt一样,如果一样,说明测试成功。

  1. 在RN中使用bsdiff。
    待续。。。
三.制作一键热更新工具

待续。。。

相关文章

网友评论

  • 5239fc238b16:有没有DEMO的下载地址呀
  • paintingStyle:作者你好,我们公司需要做H5网页资源的增量更新,这种情景属于热更新吗,会影响上架Appstore吗?
  • b034f0a76bf9:“我们将下载好的zip解压到getFilesDir().getAbsolutePath()目录下,再次启动App时便会读取该目录下的bundle文件了“ ----这里请问安全性怎么保证,该目录下的bundel被篡改了怎么办呢
    3z鸿杰Libra:@Ljx_x 各个补丁和各个版本的bundle,都会有对应的md5,该md5都存在服务器上,app在更新之前,获取本地bundle文件的md5与服务器上的md5进行对比,若不一致,则弹窗提示并不执行更新流程。
  • 不是鱿鱼:催更催更
  • keayou:打扰。
    想问下,生成diff后的patch文件后,应该是吧这个patch文件下发给客户端对吧? 那么在客户端怎么还原成b.txt了? 在电脑上可以执行bspatch,难道是要把bspatch也放进客户端去执行?
  • 宇宙只有巴掌大:纠正一个小错误,这不是增量更新额,增量更新是只下载差异部分。。
    宇宙只有巴掌大:呃呃呃 我坑壁了。。。我没看完 我就评论 对不起 作者 :sob:
  • yunFeng:react-native-zip 路劲添加 按照GitHub上的 不对 啊 楼主 能帮忙解决下吗
    Look for Header Search Paths and make sure it contains both $(SRCROOT)/../../../react-native/React as recursive.
    这里的both 哪里是要添加2个吗 我只看到 Header Search Paths 啊 跪谢
  • 信仰码农:请教下,如何将RN源码和第三方库代码打包成一个bundle,业务逻辑打包成一个bundle?能否指点下,感激不尽
  • 3c306b7052ae:请教下楼主 怎么把图片一起打包了,替换了bundle文件后
    我现在所有的本地图片都显示不出来
  • 9c45fcc97b7a:楼主能给个demo吗?初学者很需要demo,万分感激
  • 5e7fe5aad05c:filetransfer缺少cordova,求助~
    3z鸿杰Libra:@Simulator 安装个这个就好了:react-native-cordova 地址:https://github.com/remobile/react-native-cordova
  • 宇光十色_FLY:检查更新肯定是要在应用启动后进行,然后下载、解压并覆盖掉这个bundler文件,问题是应用在运行的时候去修改bundle文件,会不会导致应用崩溃?
    3z鸿杰Libra:@宇光十色_FLY 不会。app启动时执行的逻辑还在原生层。
  • studentliubo:实地考察了一下ReactActivity 下面没有 getJSBundleFile~~
    俞其荣:@Neo12306 新版本的 React Native 已经改到 Application 中了,创建一个 Application 实现 ReactApplication ,重写里面的 ReactNativeHost 的 getJSBundleFile 方法
  • 宇光十色_FLY:getJSBundleFile 这个方法不是在ReactActivity里的吧?是在ReactNativeHost里的啊!
  • 喝咖啡的鱼儿:楼主,jsBundle 生成的时候需要同时转化多个js文件,需要每一个文件都执行一遍吗?还是单单执行index.android.js就可以啦?
    d7441c20e1fd:@张再东 可以搭建https://github.com/lisong/code-push-server 服务,交给code-push-cli管理
  • 微凉一季:我已经想不到更好的更新方式可以装更大的逼了。看到这句话,立马关注了你,我怕遇不到更好的学习装逼的人了
  • 木子尚武:留着以后用
    d7441c20e1fd:@木子尚武 可以搭建https://github.com/lisong/code-push-server 服务,交给code-push-cli管理
  • ddc5af71d127:这种方法有问题、用户清除一下应用数据就回到了老版本
    d7441c20e1fd:@藤真 对于资源更新方式应该都会有这样的问题,但是当用户打开应用的时候可以再次去请求更新,可以搭建https://github.com/lisong/code-push-server 服务,交给code-push-cli管理
  • stefanli:求指教,我把图片资源放到bundle文件的同级目录,但是并没有生效,图片资源还是显示不出来。
    d7441c20e1fd:@糖花 用微软的codepush 可以搭建https://github.com/lisong/code-push-server服务,没有必要重复造轮子
    d7441c20e1fd:@stefanli 可以搭建https://github.com/lisong/code-push-server
    029aaaf0473c:解决了吗 我也遇到这个问题了
  • stefanli:赞,学习了!
  • 佳小豆:麻烦问一下,有iOS的增量更新的demo吗
    d7441c20e1fd:@佳小豆 https://github.com/Microsoft/react-native-code-push
    服务器端可以自己搭建 https://github.com/lisong/code-push-server
  • 22ee0fa80171:楼主,想问一下如何在ios工程中去使用这个bspatch
  • Xerath:楼主我又来了,现在碰到两个问题:1.第一次从assets下读取bundle文件时图片资源找不到;2.从assets拷贝资源和bundle文件到本地,第一次reload的时候本地图片资源没找到。 只有这两次本地图片不显示,可是我的图片资源始终都存在,assets和local都有一份,这是什么原因?
    d7441c20e1fd:@Xerath https://github.com/lisong/code-push-server
    Xerath:@betheman 竟然回复了。。这个问题已经好了,稀里糊涂就没了都忘了怎么解决的。话说楼主现在用微软的code-push了?我还在用之前的方法,自己搭服务器需要哪些条件?不太懂这方面,免费的有吗?
    d7441c20e1fd:@Xerath https://github.com/Microsoft/react-native-code-push
  • 含泪若笑:有 demo吗 楼主 想参考下 有些地方不对
    d7441c20e1fd:@hanleirx https://github.com/Microsoft/react-native-code-push
  • zackzheng:下载的bundle存放路径 bundlePath 这个在iOS下怎么给呢?
  • zackzheng:你好,请问react-native-file-transfer怎么安装的?

    npm install @remobile/react-native-file-transfer --save
    但是提示没找到~
    d7441c20e1fd:@zackzheng 可以尝试使用微软的codepush,如果觉得微软服务慢,可以自己搭建 https://github.com/lisong/code-push-server 更新服务
    皮卡亿千万:@zackzheng 你的npm是不是用的中国镜像?我一开始找不到,换了原来的https://registry.npmjs.org/就好了。。你可以装个nrm管理你的registry
  • d7e0b4ee988e:你好 我是一个前端 不懂ios getJSBundleFile()方法对应的ios方法是什么!类似也看不懂
    d7441c20e1fd:@laiyangde https://github.com/Microsoft/react-native-code-push
  • 乱尘:学习了

本文标题:React-Native 实现增量热更新的思路(1)

本文链接:https://www.haomeiwen.com/subject/evkylttx.html