# 小白学习:已有 Android 项目如何实现动态加载 RN bundle
作为小白的我学习 RNWeb 的过程中,在 Now 上看到调试 RN 时只需要输入 Bundle 名称和 module name 就可以打开 RN 页面进行调试,让我滋生了学习动态加载 bundle 展示 rn 页面的想法。在了解如何做动态加载之前,先简单了解一下 RN 加载流程。
# 一、RN 加载流程简析
在使用 RN cli 生成一个项目之后,程序的入口对应用进行注册
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);
在https://github.com/facebook/react-native/blob/master/Libraries/ReactNative/AppRegistry.js (opens new window)中查看registerComponent
源码得知,如果要注册多个 RN 应用需要确保appName
的唯一性。
registerComponent(
appKey: string,
componentProvider: ComponentProvider,
section?: boolean,
): string {
let scopedPerformanceLogger = createPerformanceLogger();
//注册过的应用
runnables[appKey] = {
componentProvider,
run: appParameters => {
renderApplication(
componentProviderInstrumentationHook(
componentProvider,
scopedPerformanceLogger,
),
appParameters.initialProps,
appParameters.rootTag,
wrapperComponentProvider && wrapperComponentProvider(appParameters),
appParameters.fabric,
showArchitectureIndicator,
scopedPerformanceLogger,
appKey === 'LogBox',
);
},
};
if (section) {
sections[appKey] = runnables[appKey];
}
return appKey;
},
上面已经简单知道了注册 RN 应用,需要传入一个appName
和具体渲染的根组件,那么就Android
而言,应用是如何启动的呢?打开 Android java 目录可以看到存在两个文件,一个是MainActivity
和MainApplication
,熟悉 Android 开发的同学应该都知道MainApplication
是一个应用启动类,而MainActivity
是应用启动入口(在 AndroidManifast.xml 中注册过);
在MainActivity
中需要返回应用的模块名称也就是在上面 RN 中注册的模块名称。
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "AwesomeProject";
}
}
而MainApplication
继承了ReactApplication
,并实现了getReactNativeHost
方法。
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
....
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
//官方描述为:自动检查和加载多个有依赖关系的so库文件;SoLoader的主要作用就是作为Java和Javascript两种程序语言之间的通信桥梁
SoLoader.init(this, /* native exopackage */ false);
}
}
ReactNativeHost
是干嘛的?观看源码得知其本身就是一个抽象类,并且具有一系列的抽象方法getJSMainModuleName
,getPackages
,getJSBundleFile
,getBundleAssetName
;这似乎和我们想要实现动态加载 js bundle 有点契合,如果我们手动实现一个ReactNativeHost
的类,复写getJSMainModuleName
,getBundleAssetName
,getJSBundleFile
将具体的值改造成远程加载的 js bundle 名称,也许就可实现远程加载。
public abstract class ReactNativeHost {
...
protected @Nullable
JSIModulePackage getJSIModulePackage() {
return null;
}
protected String getJSMainModuleName() {
return "index.android";
}
protected @Nullable String getJSBundleFile() {
return null;
}
protected @Nullable String getBundleAssetName() {
return "index.android.bundle";
}
...
}
由于我们是在已有 Android 基础上使用 RN 页面,当点击某个按钮/某个功能的情况下才会加载对应的 js 文件,这似乎在MainApplication
处写ReactNativeHost
不大合适,那么有没有可能在在一个Activity
里面返回自定义的ReactNativeHost
的呢?
答案是有的。
让我们再来看一下啊MainActivity
,他继承了 ReactActivity
,打开ReactActivity
一看,卧槽,就是一个抽象类。
/**
* Base Activity for React Native applications.
*/
public abstract class ReactActivity extends Activity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private final ReactActivityDelegate mDelegate;
protected ReactActivity() {
mDelegate = createReactActivityDelegate();
}
protected @Nullable String getMainComponentName() {
return null;
}
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
}
... //此处省略一些生命周期函数
protected final void loadApp(String appKey) {
mDelegate.loadApp(appKey);
}
}
其主要作用:
- 提供
getMainComponentName
方法的声明 - 创建
ReactActivityDelegate
实例,便于把具体的功能全委托给ReactActivityDelegate
类来处理
既然他把所以的工作都委托给了ReactActivityDelegate
那么我们可以在ReactActivityDelegate
中找到ReactNativeHost
的实例吗?
//ReactActivityDelegate源码
public class ReactActivityDelegate {
... //省略一些初始化
protected ReactRootView createRootView() {
return new ReactRootView(getContext());
}
//正是我们要找的ReactNativeHost
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
}
protected void onCreate(Bundle savedInstanceState) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
protected void loadApp(String appKey) {
if (mReactRootView != null) {
throw new IllegalStateException("Cannot loadApp while app is already running.");
}
mReactRootView = createRootView();
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
appKey,
getLaunchOptions());
getPlainActivity().setContentView(mReactRootView);
}
... //省略一些生命周期函数执行
}
在查看ReactActivityDelegate
的源码中我们发现了getReactNativeHost
方法,那么是不是意味着我们可以在 Activity 中重写ReactNativeHost
。
在ReactActivityDelegate
中我们可以看到 RN 应用加载的核心逻辑其实就两句话:创建视图并加载对应 Bundle 文件。
mReactRootView = createRootView();//ReactRootView是一个自定义View,且继承自FrameLayout
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
appKey,
getLaunchOptions());
问题来了ReactNativeHost
到时是干什么的?ReactInstanceManager
又是干什么的?查看源码得知,核心在于ReactInstanceManager
,这个类管理 CatalystInstance
的实例。他通过 ReactPackage 对外暴露一种设置 catalyst 实例的方式,并且跟踪实例的生命周期。
看了源码发现动态加载的核心在如何创建一个**CatalystInstance**
实例,其中主要包括设置 js 模块的路径,应用上下文信息等
public abstract class ReactNativeHost {
private final Application mApplication;
private @Nullable ReactInstanceManager mReactInstanceManager;
... //此处省略
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder =
ReactInstanceManager.builder()
.setApplication(mApplication)//设置应用上下文
.setJSMainModulePath(getJSMainModuleName())//设置需要加载的js bundle路径
.setUseDeveloperSupport(getUseDeveloperSupport()) //是否开启dev模式
.setRedBoxHandler(getRedBoxHandler()) //设置红盒回调,dev模式才会有红屏展示
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())// 设置 js 的执行器工厂实例,为后续加载和解析 js bundle 作准备
.setUIImplementationProvider(getUIImplementationProvider()) // 设置UI实现机制的Provider
.setJSIModulesPackage(getJSIModulePackage())//设置js模块到jsbridge中
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);//初始化生命周期
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
protected @Nullable JSIModulePackage getJSIModulePackage() {
return null;
}
protected String getJSMainModuleName() {
return "index.android";
}
protected @Nullable String getJSBundleFile() {
return null;
}
protected @Nullable String getBundleAssetName() {
return "index.android.bundle";
}
}
好啦,基础部分暂时就到这里啦,再往下写就得偏题啦。
# 二、动态加载工程实现
有了上面的解决办法,那我们开始整理一下这个工程实现的思路。
- 工程搭建
- 搭建 2 个 RN 工程,并打离线包作为 app 动态加载 RN 的资源文件;
- 搭建一个
Android
工程,用于编写加载逻辑(为了方便,就直接使用react-native
创建一个 rn 工程,我们只使用其中的android
部分) - 使用 express 或者 koa 搭建一个后端服务返回模块信息和对应的离线包;
- 编写 app 端主逻辑
- 使用 RecyclerView 呈现出一个列表(其中包括 adapter 编写,使用 okhttp 请求服务端数据)
- 点击列表中每一项向后端服务请求对应的 RN 离线包
- 下载完成离线包解压缩,并跳转到 RN 对应的 Activity
- 编写 RN 加载的 Activity
- 复写 createReactActivityDelegate 返回自定义 ReacActivity 的委托
- 自定义 ReactNativeHost 修改 js bundle 对应加载逻辑;
其主要流程如下:
# 1、工程搭建
首先让我们建立两个 rn 工程:
react-native init module_1 #第一个RN工程
react-native init module_2 #第二个RN工程
react-native init app # 主工程(为了省去自建Android项目的过程及rn相关依赖)
express mock_server # 创建一个后端服务
打离线包
# model_1工程,打离线包并且压缩成zip (zip -q -r -o module_1.zip ./module_1 非Mac的同学可以删掉这句话)
react-native bundle --entry-file index.js --bundle-output ./module_1/module_1.bundle --platform android --assets-dest ./module_1 --dev false zip -q -r -o module_1.zip ./module_1
# model_2工程
react-native bundle --entry-file index.js --bundle-output ./module_2/module_2.bundle --platform android --assets-dest ./module_2 --dev false zip -q -r -o module_2.zip ./module_2
如何打离线包?离线包的命令都是些啥?输入react-native bundle —help
就知道咯
react-native bundle [参数]
构建 js 离线包
Options:
-h, --help 输出如何使用的信息
--entry-file <path> RN入口文件的路径, 绝对路径或相对路径
--platform [string] ios 或 andorid
--transformer [string] Specify a custom transformer to be used
--dev [boolean] 如果为false, 警告会不显示并且打出的包的大小会变小
--prepack 当通过时, 打包输出将使用Prepack格式化
--bridge-config [string] 使用Prepack的一个json格式的文件__fbBatchedBridgeConfig 例如: ./bridgeconfig.json
--bundle-output <string> 打包后的文件输出目录, 例: /tmp/groups.bundle,tmp目录没有需要手动创建哦
--bundle-encoding [string] 打离线包的格式 可参考链接https://nodejs.org/api/buffer.html#buffer_buffer.
--sourcemap-output [string] 生成Source Map,但0.14之后不再自动生成source map,需要手动指定这个参数。例: /tmp/groups.map
--assets-dest [string] 打包时图片资源的存储路径
--verbose 显示打包过程
--reset-cache 移除缓存文件
--config [string] 命令行的配置文件路径
运行完成之后可以在对应工程下面查看 module_n 目录下有对应的 bundle 文件,
可能会出现的错误:解决办法(在 model_1/model_2 工程下新建一个 module_n 目录)
Loading dependency graph...info Writing bundle output to:, ./bundle/module_1.bundle
error ENOENT: no such file or directory, open './module_1/module_1.bundle'. Run CLI with --verbose flag for more details.
# 2、后端服务编写
我们使用express
创建一个 Mock 服务,主要包括模块信息的返回和对应模块 zip 文件的下载。那么我们需要将打包之后的module_1/module_2
目录压缩成 zip 各式。
const express = require("express");
const router = express.Router();
/**
* 返回对应module信息
*/
router.get("/modules", function(req, res, next) {
res.json({
data: [
{
name: "module_1"
},
{
name: "module_2"
}
]
});
});
/**
* 根据名称返回对应module的zip文件
*/
router.get("/download/:name", function(req, res, next) {
const name = req.params.name;
res.download(`static/${name}.zip`);
});
module.exports = router;
# 3、Android 工程改造
前面提到我们直接使用 rn 创建的一个 Android 工程,那么现在我们就对这个 Android 工程进行改造吧。
Step1:改造MainApplication.java
其他部分的代码干掉,值保留SoLoader
加载器,前面提到 SoLoader 的作用是加载 so 文件,作为 js 和 Java 通信的桥梁。
public class MainApplication extends Application implements ReactApplication {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
Step2: 新增okhttp
等依赖。由于需要向后端服务请求对应 zip 包,所以需要 http 请求。
//在android的app目录下的build.gradle文件新增如下依赖
implementation 'com.liulishuo.filedownloader:library:1.7.4' // 文件下载
implementation "com.squareup.okhttp3:okhttp:3.12.0" // 网络请求
implementation 'com.google.code.gson:gson:2.8.5' // json转换
implementation 'com.android.support:recyclerview-v7:28.0.0' //列表展示
Step3: 编写主逻辑
- 我们设想在 App 首页请求后端服务,查看到当前所有 RN 的 module
- 点击列表中的每一个,跳转到对应的 RN 页面。
Step4: 使用 okhttp 获取模块列表,点击下载对应 Bundle 文件
由于需要调用服务,需要在 AndroidManifest.xml 文件中声明网络获取权限,详情代码请见文末【源码】
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
ListModuleAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initView();
featchData();
}
/**
* 初始化布局视图,默认数据为空
*/
public void initView() {
recyclerView = this.findViewById(R.id.list);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new ListModuleAdapter(this, new ArrayList<ModuleItem.Bundle>());
recyclerView.setAdapter(adapter);
adapter.setOnItemClickListener(bundle -> {
// 检查是否下载过,如果已经下载过则直接打开,暂不考虑各种版本问题
String f = MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundle.name + "/" + bundle.name + ".bundle";
File file = new File((f));
if (file.exists()) {
goToRNActivity(bundle.name);
} else {
download(bundle.name);
}
});
}
/**
* 跳转到RN的展示页面
* @param bundleName
*/
public void goToRNActivity(String bundleName) {
Intent starter = new Intent(MainActivity.this, RNDynamicActivity.class);
RNDynamicActivity.bundleName = bundleName;
MainActivity.this.startActivity(starter);
}
/**
* 调用服务获取数据
*/
public void featchData() {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url(API.MODULES).method("GET", null).build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println("数据获取失败");
System.out.println(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String data = response.body().string();
ModuleItem moduleItem = new Gson().fromJson(data, ModuleItem.class);
runOnUiThread(new Runnable() {
@Override
public void run() {
//刷新列表
adapter.clearModules();
adapter.addModules(moduleItem.data);
}
});
}
});
}
/**
* 下载对应的bundle
*
* @param bundleName
*/
private void download(final String bundleName) {
System.out.println(API.DOWNLOAD + bundleName);
FileDownloader.setup(this);
FileDownloader.getImpl().create(API.DOWNLOAD + bundleName).setPath(this.getFilesDir().getAbsolutePath(), true)
.setListener(new FileDownloadListener() {
@Override
protected void started(BaseDownloadTask task) {
super.started(task);
}
@Override
protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {
}
@Override
protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {
}
@Override
protected void completed(BaseDownloadTask task) {
try {
//下载之后解压,然后打开
ZipUtils.unzip(MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundleName + ".zip", MainActivity.this.getFilesDir().getAbsolutePath());
goToRNActivity(bundleName);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void paused(BaseDownloadTask task, int soFarBytes, int totalBytes) {
}
@Override
protected void error(BaseDownloadTask task, Throwable e) {
}
@Override
protected void warn(BaseDownloadTask task) {
}
}).start();
}
}
**Step5:**自定义 RN 加载页面(记得在AndroidManifest
.xml 文件中声明)并创建ReacActivity
委托对象DispatchDelegate
//RNDynamicActivity
public class RNDynamicActivity extends ReactActivity {
public static String bundleName;
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
DispatchDelegate delegate = new DispatchDelegate(this, bundleName);
return delegate;
}
}
//DispatchDelegate
public class DispatchDelegate extends ReactActivityDelegate {
private Activity activity;
private String bundleName;
public DispatchDelegate(Activity activity, @Nullable String bundleName) {
super(activity, bundleName);
this.activity = activity;
this.bundleName = bundleName;
}
@Override
protected ReactNativeHost getReactNativeHost() {
ReactNativeHost mReactNativeHost = new ReactNativeHost(activity.getApplication()) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
//注册原生模块,这样
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Nullable
@Override
protected String getJSBundleFile() {
// 读取已经解压的bundle文件
String file = activity.getFilesDir().getAbsolutePath() + "/" + bundleName + "/" + bundleName + ".bundle";
return file;
}
@Nullable
@Override
protected String getBundleAssetName() {
return bundleName + ".bundle";
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
return mReactNativeHost;
}
}
其余详细代码请见:
MrGaoGang/luckly_learn (opens new window)
最后效果图如下:
其实真实的加载远不止如此简单,还有很多方面需要我我们考虑。比如:
- 离线包版本管理
- 离线包加载时机,比如用户在什么时间下载离线包,加载离线包,已经打开了当前离线包,下次打开是否更新?等
- 离线包缓存机制
- 权限管理
- 降级策略
- 等待
# 三、可能会遇到的问题及疑问
1、为什么页面跳转到 RNDynamicActivity 没有使用 Intent 传递数据
这就要看 Activity 中createReactActivityDelegate
和onCreate
执行顺序了,查看 ReactActivity 源码得知createReactActivityDelegate
是在 RectActivity 初始化的时候执行,所以优先于onCreate
执行。
public abstract class ReactActivity extends AppCompatActivity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private final ReactActivityDelegate mDelegate;
protected ReactActivity() {
mDelegate = createReactActivityDelegate();
}
protected @Nullable String getMainComponentName() {
return null;
}
/** Called at construction time, override if you have a custom delegate implementation. */
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
}
...
}
2、出现模块没有注册的报错
请检查工程的名称是否和打包的名称相同
3、报错:错误: 需要 class, interface 或 enum
# 四、源码
- 本文链接: https://mrgaogang.github.io/react/%E5%B7%B2%E6%9C%89Android%E9%A1%B9%E7%9B%AE%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDRN.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!