RN热更新
相信用过RN的朋友们都知道 , 就算只写一行代码 , 打包出来的apk都很大, 和electron有异曲同工之妙
手里刚好有个RN项目 , 一些小的更改(没动原生)的情况下, 都要重新前往应用市场下载 , 又费流量又不方便
所以新增了关于热更的优化
由于我们的项目只有android端 , 所以以下内容全部针对android看网上很多人都是使用的CodePush , 甚至连ChartGpt都让我用CodePush , 但是根据各种因素考虑, 我不想使用CodePush , 而且根据我对RN的粗浅了解 , 要实现也没想象的复杂, 没必要使用三方库
首先,要具备基础的RN与原生通信的基础,因为核心功能还得使用java实现
其次看自己怎么设计了
原理
在RN中 , js会被打包成
index.android.bundle
,然后在java入口处, 也就是
MainApplication.java
文件中, 会调用getJsBundleFile
方法 , 来确定用于渲染的bundle文件默认是返回
null
, 使用系统包内自带的index.android.bundle
文件;通过重写该方法 , 让其 return 我们所需的目标bundle的路径 , 即可实现功能了
index.android.bundle 有两种获取方式;
- 通过打包apk, 解压缩apk, 在assets目录中 , 获取index.android.bundle (不推荐)
- 使用Metro打包生成相应的文件:
npx react-native bundle --entry-file index.js --dev false --minify true --bundle-output ./build/index.android.bundle --assets-dest ./build --platform android
注: 如果使用上述命令 ,要自己新建build文件夹 , 否则会出错, 输出文件(夹)名可以自定义, 但是下载的文件名必须和getJsBundleFile
中返回的一致
其实到这里, 都可以去自己实现了
我的实现步骤
定义了一个接口 , 用来保存需要的数据
public interface FileConstants{
// 获取JSBundle的完整路径
static String getJsBundleLocalPath(Context context){
return context.getExternalFilesDir("").getAbsoluteFile() + "/"+JS_BUNDLE_RELATIVE_PATH;
}
// 文件名
String JS_BUNDLE_NAME = "index.android.bundle";
// 文件夹名
String JS_BUNDLE_DIR_NAME = "HotUpdate";
// 相对路径
String JS_BUNDLE_RELATIVE_PATH =JS_BUNDLE_DIR_NAME+"/"+JS_BUNDLE_NAME;
}
重写getJSBundleFile
在MainApplication
中
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
// ...
@Nullable
@Override
protected String getJSBundleFile() {
// 获取jsBundle所在的文件夹
String dir = FileConstants.getJsBundleLocalPath(this.getApplication());
File file = new File(dir);
// 判断文件是否存在 , 存在就说明有新的jsBundle , 不存在就使用默认的bundle
if (file.exists()){
return dir;
}
return super.getJSBundleFile();
}
// ....
};
实现热更功能
下载JSBundle
// 定义广播
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
if (completeId == mDownloadId){
// 也没干啥 , 就是下载成功后 , 弹个消息
Toast.makeText(context,"下载成功!",Toast.LENGTH_SHORT).show();
WritableMap map = Arguments.createMap();
// 传值
map.putString("status","success");
map.putInt("progress",100);
// 发送事件, 在js中监听该事件, 获取数据
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
}
}
};
/**
* 下载JSBundle
* @param {String} url 下载地址
*/
private void downloadBundle(String url){
File file = new File(FileConstants.getJsBundleLocalPath(context));
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}else if(file.exists()){
file.delete();
}
downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
request.setDestinationUri(Uri.fromFile(file));
mDownloadId = downloadManager.enqueue(request);
// 注册广播
context.registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
实现RN方法 , 用于js调用
@ReactMethod
public void update(String url){
downloadBundle(url);
// 刷新下载进度
Timer timer = new Timer();
// 上一个时间点的下载进度 , 用于计算下载速度
final int[] last = {0};
// 定时任务, 每秒钟请求下下载状态
TimerTask task = new TimerTask() {
@SuppressLint("Range")
@Override
public void run() {
// 这玩意儿相当于给js传json
WritableMap map = Arguments.createMap();
DownloadManager.Query query = new DownloadManager.Query();
Cursor cursor = downloadManager.query(query.setFilterById(mDownloadId ));
if (cursor != null && cursor.moveToFirst()) {
// 检查下载状态
String downloadFlag = checkDownloadStatus(mDownloadId,timer);
// 已下载大小
int downloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
// 文件总大小
int total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
// 下载进度(百分比)
int pro = (downloaded * 100) / total;
// 传值
map.putString("status",downloadFlag);
map.putInt("progress",pro);
map.putInt("fileSize",total);
map.putInt("downloaded",downloaded);
map.putInt("speed",downloaded - last[0]);
last[0] = downloaded;
// 发送事件, 在js中监听该事件, 获取数据
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
}
cursor.close();
}
};
timer.schedule(task, 0, 1000);
}
检查下载状态
//检查下载状态
private String checkDownloadStatus(long downloadId,Timer timer) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId );//筛选下载任务,传入任务ID,可变参数
Cursor c = downloadManager.query(query);
boolean hasCancel = false;
String flag = "";
if (c.moveToFirst()) {
@SuppressLint("Range") int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
flag = "pause";
break;
case DownloadManager.STATUS_PENDING:
hasCancel = true;
flag = "timeout";
break;
case DownloadManager.STATUS_RUNNING:
flag = "downloading";
break;
case DownloadManager.STATUS_SUCCESSFUL:
hasCancel = true;
flag = "success";
break;
case DownloadManager.STATUS_FAILED:
hasCancel = true;
flag = "fail";
break;
}
if (timer != null && hasCancel)timer.cancel();
}
return flag;
}
完整java代码
package com.mypackage.reactnative.hotUpdate;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.onetoone.FileConstants;
import com.onetoone.MainApplication;
import com.umeng.commonsdk.statistics.common.MLog;
import java.io.File;
import java.util.Timer;
import java.util.TimerTask;
public class HotUpdate extends ReactContextBaseJavaModule {
private ReactApplicationContext context;
long mDownloadId;
DownloadManager downloadManager;
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
if (completeId == mDownloadId){
Toast.makeText(context,"下载成功!",Toast.LENGTH_SHORT).show();
}
}
};
@NonNull
@Override
public String getName() {
return "HotUpdate";
}
//检查下载状态
private String checkDownloadStatus(long downloadId,Timer timer) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId );//筛选下载任务,传入任务ID,可变参数
Cursor c = downloadManager.query(query);
boolean hasCancel = false;
String flag = "";
if (c.moveToFirst()) {
@SuppressLint("Range") int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
flag = "pause";
break;
case DownloadManager.STATUS_PENDING:
hasCancel = true;
flag = "timeout";
break;
case DownloadManager.STATUS_RUNNING:
flag = "downloading";
break;
case DownloadManager.STATUS_SUCCESSFUL:
hasCancel = true;
flag = "success";
break;
case DownloadManager.STATUS_FAILED:
hasCancel = true;
flag = "fail";
break;
}
if (timer != null && hasCancel)timer.cancel();
}
return flag;
}
public HotUpdate(ReactApplicationContext context){
super(context);
this.context = context;
}
//下载JSBundle
private void downloadBundle(){
File file = new File(FileConstants.getJsBundleLocalPath(context));
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}else if(file.exists()){
file.delete();
}
downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(FileConstants.JS_BUNDLE_DOWNLOAD_URL));
// 设置通知栏显示, 我这里不需要, 就隐藏吧
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
// 设置网络模式
// 这里得记得加入对应的网络权限
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
request.setDestinationUri(Uri.fromFile(file));
mDownloadId = downloadManager.enqueue(request);
// 对了 <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> 这个权限也得加上
context.registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@ReactMethod
public void update(){
downloadBundle();
// 刷新下载进度
Timer timer = new Timer();
// 上一个时间点的下载进度 , 用于计算下载速度
final int[] last = {0};
// 定时任务, 每秒钟请求下下载状态
TimerTask task = new TimerTask() {
@SuppressLint("Range")
@Override
public void run() {
// 这玩意儿相当于给js传json
WritableMap map = Arguments.createMap();
DownloadManager.Query query = new DownloadManager.Query();
Cursor cursor = downloadManager.query(query.setFilterById(mDownloadId ));
if (cursor != null && cursor.moveToFirst()) {
// 检查下载状态
String downloadFlag = checkDownloadStatus(mDownloadId,timer);
// 已下载大小
int downloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
// 文件总大小
int total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
// 下载进度(百分比)
int pro = (downloaded * 100) / total;
// 传值
map.putString("status",downloadFlag);
map.putInt("progress",pro);
map.putInt("fileSize",total);
map.putInt("downloaded",downloaded);
map.putInt("speed",downloaded - last[0]);
last[0] = downloaded;
// 发送事件, 在js中监听该事件, 获取数据
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
}
cursor.close();
}
};
timer.schedule(task, 0, 1000);
}
}
然后需要注册热更模块,不然没法在js中使用, 不会的参考下这篇文章吧: 8.RN js调用原生方法
接下来的任务 就是在js中做相应的逻辑处理
// 调用方法 , 执行更新
NativeModules.HotUpdate.update(下载地址);
// 监听更新事件 , 获取到对应的数据
DeviceEventEmitter.addListener("onUpdateDownload",处理方法)
// 然后重启就可以了
如何与后端交互就自己去沟通吧, 很简单的 , 就没必要讲了