安卓端react-native热更新方案

1 RN加载js文件的机制

做过安卓原生热更新的同学都知道,要实现热更新需要自己实现一个DexClassLoader来加载需要热更新的类。同理,要实现RN的热更新自然需要我们自己来控制js文件的加载逻辑。那么RN是如何加载js文件的呢?首先,我们在开发的时候会有多个js文件,但是在我们构建release包的时候,这些js文件会连同rn源代码和第三方库被打包进一个叫做index.android.bundle的js文件中,这个文件会被放到asset目录中去,然后RN应用在启动的时候就会去加载这个js文件。
release apk 中的内容
那么RN具体是如何加载这个js的呢?我们可以看到在RN的MainApplicat这个类中实现了ReactApplication这个接口,该接口如下所示:

1
2
3
4
5
6
7
public interface ReactApplication {
/**
* Get the default {@link ReactNativeHost} for this app.
*/
ReactNativeHost getReactNativeHost();
}

而在ReactNativeHost这个类中有以下两个方法,其中getJsBundleFile这个方法的注释中写到这个方法是用来返回自定义的bundle路径的。至此,我们知道了,想要加载我们自己的js文件可以通过覆写该方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected ReactInstanceManager createReactInstanceManager() {
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModuleName(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.setUIImplementationProvider(getUIImplementationProvider())
.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()));
}
return builder.build();
}
/**
* Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
* from a custom path. By default it is loaded from Android assets, from a path specified
* by {@link getBundleAssetName}.
* e.g. "file://sdcard/myapp_cache/index.android.bundle"
*/
protected @Nullable String getJSBundleFile() {
return null;
}

2 RN打包命令

我们已经知道了如何来加载自己的js文件了,那么我们的这个js文件又该如何生成呢?可以使用react-native bundle命令,该命令的详细使用如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
RNHotFix react-native bundle -h
Scanning 577 folders for symlinks in /Users/nali/workspace/react-native/RNHotFix/node_modules (5ms)
react-native bundle [options]
builds the javascript bundle for offline use
Options:
--entry-file <path> Path to the root JS file, either absolute or relative to JS root
--platform [string] Either "ios" or "android"
--transformer [string] Specify a custom transformer to be used
--dev [boolean] If false, warnings are disabled and the bundle is minified
--bundle-output <string> File name where to store the resulting bundle, ex. /tmp/groups.bundle
--bundle-encoding [string] Encoding the bundle should be written in (https://nodejs.org/api/buffer.html#buffer_buffer).
--max-workers [number] Specifies the maximum number of workers the worker-pool will spawn for transforming files. This defaults to the number of the cores available on your machine.
--sourcemap-output [string] File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map
--sourcemap-sources-root [string] Path to make sourcemap's sources entries relative to, ex. /root/dir
--sourcemap-use-absolute-path Report SourceMapURL using its full path
--assets-dest [string] Directory name where to store assets referenced in the bundle
--verbose Enables logging
--reset-cache Removes cached files
--read-global-cache Try to fetch transformed JS code from the global cache, if configured.
--config [string] Path to the CLI configuration file
-h, --help output usage information

在安卓上我们可以通过以下命令来生成我们的bundle:

1
react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

3. 热更新Demo

为了方便理解,笔者这里写了RN热更新的客户端实现和服务端小Demo,欢迎star和issue。
客户端
服务端

热更新服务端采用python的flask框架实现,服务端只提供两个接口,一个用来检测客户端是否需要更新bundle,一个用来供客户端下载最新bundle。最新的js bundle需放到latestJsBundle目录中,且名称必须为“2.3.4.3”这种格式。

4. TODO

  • 完整性校验
  • 增量更新