ReactNative学习笔记(四)热更新和增量更新
概括
关于RN的热更新,网上有很多现成方案,但是一般都依赖第三方服务,我所希望的是能够自己管控所有一切,所以只能自己折腾。
热更新的思路
热更新一般都是更新JS和图片,也就是在不重新安装apk的情况下更新JS和图片,这个需求是很普遍的。通过前面的了解我们知道RN的JS都被打包成了一个bundle文件,默认是在assets文件夹下面,但是这个文件夹是只读不可写的,那怎么办呢?好在RN有一个getJSBundleFile方法可以自定义bundle文件的路径,把它自定义到一个我们有写入权限的地方然后下载覆盖就可以了(比如/data/data/下面)。
又由于图片也需要更新,所以可以将更新资源(图片+JSBundle文件)打包成一个zip,在每次启动apk之后检测是否有更新包,如果有,后台偷偷下载下来,那么什么时候解压呢?个人推荐在下次启动apk的时候解压,那样可以保证图片和JS同时更新(因为我没有尝试过在程序运行时覆盖bundle文件会有什么问题)。
思路的具体实现
生成bundle文件
前面提到,RN会将所有JS压缩混淆成一个bundle文件,所以要做热更新,我们首先需要掌握如何自己手动生成bundle文件。
执行如下命令即可(记得先在项目根目录新建一个bundle文件夹,否则报错):
react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

注意,bundle文件在哪,那么图片也必须放在哪,如果bundle默认放在assets下面,会自动读取apk内部res文件夹下的资源文件,但是如果你将bundle文件放在了其它自定义目录下,那么图片也要跟着复制过去,否则图片全部空白。
自定义bundle文件路径
特别注意,getJSBundleFile方法位置在0.29版本以后发现了变化。
0.28及以前版本:
public class MainActivity extends ReactActivity
{
@Override
protected @Nullable String getJSBundleFile()
{
String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
File file = new File(jsBundleFile);
return file != null && file.exists() ? jsBundleFile : null;
}
}
0.29及以后版本:
public class MainApplication extends Application implements ReactApplication
{
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this)
{
@Override
protected @Nullable String getJSBundleFile()
{
String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
File file = new File(jsBundleFile);
return file != null && file.exists() ? jsBundleFile : null;
}
}
}
假如我的包名是com.helloworld,定义了如上代码之后,启动APK首先会尝试加载/data/data/com.helloworld/files/index.android.bundle文件,找不到再去加载assets里面的。
封装下载方法
前面忘记介绍如何开发一个原生模块让JS调用了,这里正好借封装下载方法的机会介绍一下。
这里只是简单的实现一个下载的方法,实际项目中建议用更成熟方案。
新建一个HotUpdateModule.java文件:
public class HotUpdateModule extends ReactContextBaseJavaModule
{
public HotUpdateModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "hotupdate"; // 返回的名字就是最终模块的名字,前端调用时:NativeModules.hotupdate.xxx
}
@ReactMethod
public void download(final String url, String newFileName, final Promise promise)
{
final String savePath = getReactApplicationContext().getFilesDir() + "/" + newFileName;
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
String result = SimpleDownloadUtil.download(url, savePath);
WritableMap map = Arguments.createMap();
map.putString("result", result);
promise.resolve(map);
}
catch (Exception e)
{
promise.reject("unknown error", e);
}
}
}).start();
}
}
其中,SimpleDownloadUtil.java如下:
public class SimpleDownloadUtil
{
/**
* 简单的下载工具类
* @param downloadUrl
* @param savePath
* @return 返回保存路径,如果下载失败,返回空
*/
public static String download(String downloadUrl, String savePath) throws Exception
{
Log.i("info", "开始下载:"+downloadUrl);
HttpURLConnection con = (HttpURLConnection) new URL(downloadUrl).openConnection();
con.setRequestMethod("GET");
con.setUseCaches(false);
con.setInstanceFollowRedirects(true);
con.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31");
con.setRequestProperty("accept", "*/*");// 这个可以不设置
con.connect();// 连接
InputStream is = con.getInputStream();
File file = new File(savePath);
FileOutputStream fos = new FileOutputStream(file);
byte[] buf = new byte[1024];
int len = -1;
while ((len = is.read(buf)) != -1) fos.write(buf, 0, len);
is.close();
fos.close();
con.disconnect();// 断开连接
Log.i("info", "下载完毕:" + savePath);
return savePath;
}
}
然后新建一个TestReactPackage.java:
public class TestReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
// modules.add(new TestModule(reactContext));
modules.add(new HotUpdateModule(reactContext)); // 多个模块依次添加
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
再修改MainApplication中的如下方法,将上面的TestReactPackage添加上去:
@Override
protected List<ReactPackage> getPackages()
{
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new TestReactPackage() // 自定义的
);
}
至此,一个使用原生实现的下载功能就完成了,JS中只需要调用NativeModules.hotupdate.download()即可(记得要引入NativeModules模块)。
模拟服务器
假设有一个检测是否需要更新的接口,返回如下字段:
{
"needUpdate": true, // 表示是否需要更新
"updateUrl": "http://192.168.191.1/update/bundle.zip" // 更新地址
}
为了简单起见,直接用JSON文件模拟,bundle.zip就是我们上面用命令生成的bundle文件夹压缩后的文件(如果希望用批处理方式生成zip的话可以参考我之前写的Windows下使用命令行解压和压缩zip)。
检测更新并下载
import React, { Component } from 'react';
import { NativeModules } from 'react-native';
class TestComponent extends Component
{
// 省略其它代码
componentDidMount()
{
fetch('http://192.168.191.1/update/check_update.json')
.then((response) => response.json())
.then((json) =>
{
if(json.needUpdate && json.updateUrl)
{
Epg.tip('检测到省流量更新文件,开始自动下载!');
NativeModules.hotupdate.download(json.updateUrl, 'bundle.zip')
.then((e) => alert('下载成功:'+e.result+',下次重启时生效!'))
.catch((error) => alert('下载失败:'+error));
}
})
.catch((error) => alert('检测更新失败:'+error));
}
}
解压zip
由于JS本身可能需要更新,所以解压zip用JS来完成的话可能不太适合,我把它直接写在Activity里面:
@Override
protected void onCreate(Bundle savedInstanceState)
{
String root = this.getFilesDir().getAbsolutePath();
File zip = new File(root, "bundle.zip");
if(zip.exists()) // 如果检测到zip更新包,解压之
{
ZipUtil.extract(root+"/bundle.zip", root); // 这个ZipUtil是自己随便封装的
zip.delete(); // 解压之后删除zip文件
}
super.onCreate(savedInstanceState);
}
测试
一整个过程走下来感觉是有点折腾人的,虽然都比较简单,测试的时候最麻烦,因为必须生成release包之后热更新才能看到效果。
测试过程可以这样:
先打一个release包并安装,把needUpdate暂时设置为false避免更新,然后故意修改一些JS代码以及增加图片,然后用命令生成bundle,然后把bundle文件和图片一起打包放到服务器上,然后needUpdate改回true,重启apk,可以看到自动下载zip的提示,然后再重启,检查一下修改之后的代码是否生效了,如果生效表示热更新成功了。
增量更新
图片的增量更新
前面提到了,bundle文件在哪,图片也要在哪,否则图片会找不到,但是更新包里面把所有的图片都包括进去太大了,有一种思路是:每次启动APK立即检测私有目录下是否有bundle文件,没有就从assets下复制一个,这样可以保证无论何时bundle文件都是从sd卡读取,现在要做的就是把图片也复制过去,但是图片是放在res文件夹作为资源文件存在的,怎么把res下的图片文件完整复制到sd卡,这个我还真不会,暂时也没有找到合适的方法,如果哪位知道方法还烦请告知(主要是针对非root用户,已经root的用户就好办了)。
所以目前的一个比较笨的办法是,打包时人工将所有图片丢到assets下,因为assets下的文件是可以随意复制的,缺点就是apk体积变大了,一个apk里面放了2份图片。
上述问题解决了,图片的增量更新就好办了,每次只需要把需要替换或增加的图片放到更新的zip包里面去就可以了。
bundle文件的增量更新
这是个文本文件,一般有几百kb,不作增量做全量更新问题也不大,但是还是有必要研究一下的。网上一般思路是用bsdiff对比文件,或者分离bundle,这个我没去做具体尝试,所以就不详细赘述了,有兴趣的可以看文末的参考链接。
参考
http://www.jianshu.com/p/2cb3eb9604ca
ReactNative学习笔记(四)热更新和增量更新的更多相关文章
- python3.4学习笔记(四) 3.x和2.x的区别,持续更新
python3.4学习笔记(四) 3.x和2.x的区别 在2.x中:print html,3.x中必须改成:print(html) import urllib2ImportError: No modu ...
- react-native学习笔记--史上最详细Windows版本搭建安装React Native环境配置
参考:http://www.lcode.org/react-native/ React native中文网:http://reactnative.cn/docs/0.23/android-setup. ...
- MySql学习笔记四
MySql学习笔记四 5.3.数据类型 数值型 整型 小数 定点数 浮点数 字符型 较短的文本:char, varchar 较长的文本:text, blob(较长的二进制数据) 日期型 原则:所选择类 ...
- C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻
前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...
- IOS学习笔记(四)之UITextField和UITextView控件学习
IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...
- java之jvm学习笔记四(安全管理器)
java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...
- Learning ROS for Robotics Programming Second Edition学习笔记(四) indigo devices
中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...
- Typescript 学习笔记四:回忆ES5 中的类
中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...
- ES6学习笔记<四> default、rest、Multi-line Strings
default 参数默认值 在实际开发 有时需要给一些参数默认值. 在ES6之前一般都这么处理参数默认值 function add(val_1,val_2){ val_1 = val_1 || 10; ...
- muduo网络库学习笔记(四) 通过eventfd实现的事件通知机制
目录 muduo网络库学习笔记(四) 通过eventfd实现的事件通知机制 eventfd的使用 eventfd系统函数 使用示例 EventLoop对eventfd的封装 工作时序 runInLoo ...
随机推荐
- This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
- python_07 函数作用域、匿名函数
函数的作用域:无论在哪个地方调用函数,函数运行过程中的作用域只跟定义的时候有关,跟在哪个地方调用无关. name='alex' def foo(): name = 'linhaifeng' def b ...
- poi横纵动态导入
dao层 <insert id ="saveInTarget" parameterType="java.util.List" > INSERT IN ...
- Navicat for MySQL使用
Navicat for MySQL使用 导出数据库表与结构 新数据库导入
- SpringMvc 使用Thumbnails压缩图片
```java @PostMapping(value = "/upLoadFile") @ApiOperation(value = "上传文件") public ...
- 尚硅谷springboot学习31-jdbc数据连接
可以使用JdbcTemplate操作数据库,可以在启动的时候自动建表,更新数据表 配置依赖 <dependency> <groupId>org.springframework. ...
- Dom文本应用-表格隔行间亮样式
效果: 隔行一个颜色,鼠标移上去,被选中的那一行就变颜色,其次,鼠标离开其区域,颜色又变回原来的颜色. 一.表格隔行间亮样式-HTML代码 首先我们要有个表格 <table id='tab1' ...
- ps命令详解
1.简介: ps 命令有两种不同的语法风格 —— BSD 与 UNIX 两种风格.新手常常对这两种形式产生误解,因此我们有必要在这里作一个简单的说明: ps aux 与 ps -aux 是不同的,例如 ...
- 【原】The Linux Command Line - Processes
ps - report a snapshot of current processes top - display tasks job - list active jobs bg - place a ...
- JVM系列1:Java内存区域
JVM系列主要分享自己都虚拟机的理解,我自学时的知识框架多来源于<深入理解Java虚拟机_JVM高级特性与最佳实践>这本书,感兴趣的朋友可直接去阅读这本书. 本系列暂定有3部分,它们是学习 ...