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 ...
随机推荐
- java细节知识
代码优化细节 (1)尽量指定类.方法的final修饰符 带有final修饰符的类是不可派生的.在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是fin ...
- Idea中类上有叉的解决方法
idea中类的头上出现X解决办法 ctrl+alt+s 在弹出的菜单上选择Compiler下的Excludes 右边会有 移除掉,点击ok, 重启idea就可以了
- leetcode1032
class StreamChecker: def __init__(self, words: 'List[str]'): self.maxLen = 0 self.List = set(words) ...
- Ado.net之对数据库的增删改查
一.了解Command对象 1.Command对象:封装了所有对外部数据源的操作,包括增删改查和执行存储过程,并在执行完成后返回合适的结果,同Connection一样,对于不同的数据源,Ado.net ...
- 微信开发 invalid openid
微信开发时候测试号运行正常,换到正式号就会报invalid openid的错误. 看了微信问答系统里的答案,说是json格式的问题,但是我这边不是这个原因. 后来突然想到了,应该是AppId和AppS ...
- py2与py3区别总结
1. py2中的str是py3中的bytes py2中的Unicode是py3中的str 声明一个字符串变量时,py2 和py3都是str类型,但py2代表字节类型,py3代表文本类型 隐式转换: p ...
- Linux进程的原理及与信号的联系
第1节 程序.进程.守护进程.僵尸进程的区别 程序.进程.守护进程.僵尸进程: 程序:c/php/java,代码文件,静态的,放在磁盘里的数据. 进程:正在内存中运行的程序,进程是动态的,会申请和使用 ...
- 706. Design HashMap 实现哈希表
[抄题]: public MyHashMap() { 主函数里面是装非final变量的,如果没有,可以一个字都不写 } [暴力解法]: 时间分析: 空间分析: [优化后]: 时间分析: 空间分析: ...
- [leetcode]297. Serialize and Deserialize Binary Tree 序列化与反序列化二叉树
Serialization is the process of converting a data structure or object into a sequence of bits so tha ...
- oracle 中的sql 语句
1.update 表名 set 表字段=(select 另一个表中的相同字段 from 另一个表表名 where 表.字段=另一个表.字段) where 表.字段=? 例子:将某个表中的更新到另一个 ...