ReactNative学习笔记(四)热更新和增量更新
- 作者: 五速梦信息网
- 时间: 2026年04月04日 13:29
概括
关于RN的热更新,网上有很多现成方案,但是一般都依赖第三方服务,我所希望的是能够自己管控所有一切,所以只能自己折腾。
热更新的思路getJSBundleFile/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.28及以前版本:
public class MainActivity extends ReactActivity
{
@Override<br/>
protected @Nullable String getJSBundleFile()<br/>
{<br/>
String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";<br/>
File file = new File(jsBundleFile);<br/>
return file != null && file.exists() ? jsBundleFile : null;<br/>
}<br/>
}
0.29及以后版本:
public class MainApplication extends Application implements ReactApplication
{
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this)<br/>
{<br/>
@Override<br/>
protected @Nullable String getJSBundleFile()<br/>
{<br/>
String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";<br/>
File file = new File(jsBundleFile);<br/>
return file != null && file.exists() ? jsBundleFile : null;<br/>
}<br/>
}<br/>
}
com.helloworld/data/data/com.helloworld/files/index.android.bundle
封装下载方法
前面忘记介绍如何开发一个原生模块让JS调用了,这里正好借封装下载方法的机会介绍一下。
这里只是简单的实现一个下载的方法,实际项目中建议用更成熟方案。
HotUpdateModule.java
public class HotUpdateModule extends ReactContextBaseJavaModule
{
public HotUpdateModule(ReactApplicationContext reactContext) {<br/>
super(reactContext);<br/>
}<br/>
@Override<br/>
public String getName() {<br/>
return "hotupdate"; // 返回的名字就是最终模块的名字,前端调用时:NativeModules.hotupdate.xxx<br/>
}
@ReactMethod<br/>
public void download(final String url, String newFileName, final Promise promise)<br/>
{<br/>
final String savePath = getReactApplicationContext().getFilesDir() + "/" + newFileName;<br/>
new Thread(new Runnable()<br/>
{<br/>
@Override<br/>
public void run()<br/>
{<br/>
try<br/>
{<br/>
String result = SimpleDownloadUtil.download(url, savePath);<br/>
WritableMap map = Arguments.createMap();<br/>
map.putString("result", result);<br/>
promise.resolve(map);<br/>
}<br/>
catch (Exception e)<br/>
{<br/>
promise.reject("unknown error", e);<br/>
}<br/>
}<br/>
}).start();<br/>
}<br/>
}
SimpleDownloadUtil.java
public class SimpleDownloadUtil
{
/**<br/>
* 简单的下载工具类<br/>
* @param downloadUrl<br/>
* @param savePath<br/>
* @return 返回保存路径,如果下载失败,返回空<br/>
*/<br/>
public static String download(String downloadUrl, String savePath) throws Exception<br/>
{<br/>
Log.i("info", "开始下载:"+downloadUrl);<br/>
HttpURLConnection con = (HttpURLConnection) new URL(downloadUrl).openConnection();<br/>
con.setRequestMethod("GET");<br/>
con.setUseCaches(false);<br/>
con.setInstanceFollowRedirects(true);<br/>
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");<br/>
con.setRequestProperty("accept", "*/*");// 这个可以不设置<br/>
con.connect();// 连接<br/>
InputStream is = con.getInputStream();<br/>
File file = new File(savePath);<br/>
FileOutputStream fos = new FileOutputStream(file);<br/>
byte[] buf = new byte[1024];<br/>
int len = -1;<br/>
while ((len = is.read(buf)) != -1) fos.write(buf, 0, len);<br/>
is.close();<br/>
fos.close();<br/>
con.disconnect();// 断开连接<br/>
Log.i("info", "下载完毕:" + savePath);<br/>
return savePath;<br/>
}<br/>
}
TestReactPackage.java
public class TestReactPackage implements ReactPackage {
@Override<br/>
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {<br/>
List<NativeModule> modules = new ArrayList<>();<br/>
// modules.add(new TestModule(reactContext));<br/>
modules.add(new HotUpdateModule(reactContext)); // 多个模块依次添加<br/>
return modules;<br/>
}<br/>
@Override<br/>
public List<Class<? extends JavaScriptModule>> createJSModules() {<br/>
return Collections.emptyList();<br/>
}<br/>
@Override<br/>
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {<br/>
return Collections.emptyList();<br/>
}<br/>
}
MainApplicationTestReactPackage
@Override
protected List<ReactPackage> getPackages()
{
return Arrays.<ReactPackage>asList(<br/>
new MainReactPackage(),<br/>
new TestReactPackage() // 自定义的<br/>
);<br/>
}
NativeModules.hotupdate.download()NativeModules
模拟服务器
假设有一个检测是否需要更新的接口,返回如下字段:
{
"needUpdate": true, // 表示是否需要更新<br/>
"updateUrl": "http://192.168.191.1/update/bundle.zip" // 更新地址<br/>
}
为了简单起见,直接用JSON文件模拟,bundle.zip就是我们上面用命令生成的bundle文件夹压缩后的文件(如果希望用批处理方式生成zip的话可以参考我之前写的Windows下使用命令行解压和压缩zip)。
检测更新并下载
import React, { Component } from ‘react’;
import { NativeModules } from ‘react-native’;
class TestComponent extends Component
{
// 省略其它代码<br/>
componentDidMount()<br/>
{<br/>
fetch('http://192.168.191.1/update/check_update.json')<br/>
.then((response) => response.json())<br/>
.then((json) =><br/>
{<br/>
if(json.needUpdate && json.updateUrl)<br/>
{<br/>
Epg.tip('检测到省流量更新文件,开始自动下载!');<br/>
NativeModules.hotupdate.download(json.updateUrl, 'bundle.zip')<br/>
.then((e) => alert('下载成功:'+e.result+',下次重启时生效!'))<br/>
.catch((error) => alert('下载失败:'+error));<br/>
}<br/>
})<br/>
.catch((error) => alert('检测更新失败:'+error));<br/>
}<br/>
}
解压zip
由于JS本身可能需要更新,所以解压zip用JS来完成的话可能不太适合,我把它直接写在Activity里面:
@Override
protected void onCreate(Bundle savedInstanceState)
{
String root = this.getFilesDir().getAbsolutePath();<br/>
File zip = new File(root, "bundle.zip");<br/>
if(zip.exists()) // 如果检测到zip更新包,解压之<br/>
{<br/>
ZipUtil.extract(root+"/bundle.zip", root); // 这个ZipUtil是自己随便封装的<br/>
zip.delete(); // 解压之后删除zip文件<br/>
}<br/>
super.onCreate(savedInstanceState);<br/>
}
测试
一整个过程走下来感觉是有点折腾人的,虽然都比较简单,测试的时候最麻烦,因为必须生成release包之后热更新才能看到效果。
测试过程可以这样:
needUpdateneedUpdate
增量更新
图片的增量更新
前面提到了,bundle文件在哪,图片也要在哪,否则图片会找不到,但是更新包里面把所有的图片都包括进去太大了,有一种思路是:每次启动APK立即检测私有目录下是否有bundle文件,没有就从assets下复制一个,这样可以保证无论何时bundle文件都是从sd卡读取,现在要做的就是把图片也复制过去,但是图片是放在res文件夹作为资源文件存在的,怎么把res下的图片文件完整复制到sd卡,这个我还真不会,暂时也没有找到合适的方法,如果哪位知道方法还烦请告知(主要是针对非root用户,已经root的用户就好办了)。
所以目前的一个比较笨的办法是,打包时人工将所有图片丢到assets下,因为assets下的文件是可以随意复制的,缺点就是apk体积变大了,一个apk里面放了2份图片。
上述问题解决了,图片的增量更新就好办了,每次只需要把需要替换或增加的图片放到更新的zip包里面去就可以了。
bundle文件的增量更新
这是个文本文件,一般有几百kb,不作增量做全量更新问题也不大,但是还是有必要研究一下的。网上一般思路是用bsdiff对比文件,或者分离bundle,这个我没去做具体尝试,所以就不详细赘述了,有兴趣的可以看文末的参考链接。
参考相关文章
-
React报错之React hook 'useState' is called conditionally
React报错之React hook 'useState' is called conditionally
- 互联网
- 2026年04月04日
-
react高阶组件的使用
react高阶组件的使用
- 互联网
- 2026年04月04日
-
react结合antdesgin进行表单验证
react结合antdesgin进行表单验证
- 互联网
- 2026年04月04日
-
react18用不了props.history.push
react18用不了props.history.push
- 互联网
- 2026年04月04日
-
react18 生命周期
react18 生命周期
- 互联网
- 2026年04月04日
-
react 组件动态 less modifyvars修改样式
react 组件动态 less modifyvars修改样式
- 互联网
- 2026年04月04日






