招商证券react-native热更新优化实践
Posted 前端之巅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了招商证券react-native热更新优化实践相关的知识,希望对你有一定的参考价值。
-
减小业务打包生成的 jsbundle 体积,进而减小接入的 APP 的体积以及子工程热更新时下载体积,提升用户体验; -
实现基础 jsbundle 和业务 jsbundle 的分离,多个业务共用一个基础包,使业务 jsbundle 更纯粹,提高开发效率; 优化子工程的 jsbundle 加载,实现基础包预加载,减小加载业务 bundle 时的白屏时间。
-
0.52~0.55 版本如何改造官方 metro 打包工具,使 react-native bundle 命令支持基础包和业务包的拆分; -
在原 react-native 应用的 APP(之后称之为主工程)中,如何加入另一个 react-native 应用的子工程; 主工程原生层如何预加载基础包,在进入对应的子工程
viewController/Activity
的时候加载业务包。
注: 因为 Facebook 对 react-native 0.56 以上的 metro 拆包已经趋于完善并开放了
createModuleIdFactory
方法的接口,如果你的 react-native 版本在 0.56 以上,那么第一点你只需关注相关方法实现即可,第 2、3 点是基于 APP 原生的思路及实现,理论上在任意版本的 react-native 都适用。
本文章适合以下人群阅读:熟悉 react-native 加载原理、打包流程,同时希望了解 0.50 以上 RN 异步加载 jsbundle 过程,同时了解原生开发的开发人员。
不适用以下人群:react-native 初学者,不了解 APP 原生开发或 objective-c/Java 语言的开发人员。
在进行如何进行业务拆包之前,我们先对 react-native 的 jsbundle 做一个简单的认识。
react-native bundle --platform ios --dev false --entry-file index.js --bundle-output __test__/example/index.ios.jsbundle --assets-dest __test__/example/
index.ios.jsbundle
和相应的 assets 文件夹,
index.ios.jsbundle
内容大致如下:
var
__DEV__ = false,
__BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
process = this.process || {};
process.env=process.env || {};
process.env.NODE_ENV = "production";
!(function(r) {
"use strict";
r.__r = o,
r.__d = function(r,i,n) {
if(null != e[i])
return;
e[i] = {
dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}
}
},
r.__c = n;
....
})();
__d(function(r,o,t,i,n){t.exports=r.ErrorUtils},18,[]);
...
...
__d(function(c,e,t,a,n){var r=e(n[0]);r(r.S,'Object',{create:e(n[1])})},506,[431,460]);
require(46);
require(11);
-
var 声明的全局变量,对当前运行环境的定义和 Process 进程环境相关信息; -
(function() { })() 闭包中定义的代码块,其中定义了对 define(d) 、require(r)、clear(__c) 的支持,以及 module(react-native 及第三方 dependences 依赖的 module)的加载逻辑; -
__d 定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,最后以 require 方式引入使用; require 定义的代码块,找到
__d
定义的代码块并执行,其中require
中的数字即为__d
定义行中最后出现的那个数字。
可以看到,如果每个业务都单独打一个完整包,那么每个包都会包含第一部分和第二部分,因此我们的目标也就是把第一部分和第二部分提取出来,放到基础包common.jsbundle
中,之后业务包共用此基础包。
-
使用 Google 的 diff-match-patch 工具计算业务 patch,在集成到主工程并需要加载业务页面时,再合成一个完整的 jsbundle 进行加载; 扩展 react-native 官方打包工具 metro,将打包后的业务代码提取出来,下面将对这两种方法进行介绍。
diff-match-patch
提供了多语言版本,包括 Java、Objective-c、Python 及 JavaScript 等,因此很容易用在跨平台的 react-native 应用中。首先我们在没有业务代码的 RN 框架进行打包,得到
common.ios.jsbundle。
接下来再添加业务代码,如在一个业务页面添加代码
<Text>Hello World</Text>
,打包,得到业务包
business.ios.bundle。
接下来我们写一段脚本,将我们添加的这段代码通过 diff-match-patch 提取出来,以补丁 (patch) 的形式作为业务拆包:
// split.js
const DiffMatchPatch = require('diff-match-patch');
const fs = require('fs');
const dmp = new DiffMatchPatch();
fs.readFile(`${process.cwd()}/common.ios.jsbundle`, 'utf8', (e, data) => {
let commonData = data;
fs.readFile(`${process.cwd()}/business.ios.jsbundle`, 'utf8', (e, data) => {
let businessData = data;
const patch = dmp.patch_make(commonData, businessData); // 生成补丁
const patchText = dmp.patch_toText(patch); // 生成补丁文本
fs.writeFileSync(`${process.cwd()}/diff.ios.patch`, patchText);
});
});
diff.ios.patch
,内容如下:
@@ -761599,32 +761599,83 @@
ent(s.Text,null,
+%22Hello World%22),o.default.createElement(s.Text,null,
this.state.param
可以看到,diff-match-patch
生成的 patch 记录了业务包和公共包的差异部分,差异部分以特殊的符号记录了对应位置、字符等信息。
业务包体积方面,原business.ios.jsbundle
大小为 1.2MB,经过diff-match-patch
处理得到的diff-mch_apply
仅为 121B。在子应用整合到主应用时,业务只需提供一个.patch
文件,主工程再根据需要使用diff-match-patch
的 patch_apply 方法将diff.ios.jsbundle
和common.ios.js
重新合成一个完整的business.ios.jsbundle
即可。
diff-match-patch
拆业务包得到
xxx.patch
的方案看起来很完美,但是我们使用拆包方案时没有采取这个方案,因为
diff-match-patch
在实际的业务包处理过程中存在以下大坑:
-
diff-match-patch
得到的 patch 是记录业务包和基础包的差异,并且是没有经过压缩的文本记录。除了记录单纯的业务代码差异外,还加入了额外的补丁信息。实际业务开发过程中,差异部分是很多的,因此一个原 1000 行的业务 jsbundle 经过diff-match-patch
处理,得到的diff.patch
文件能达到 2000 多行!原业务包大小为 3.6MB,补丁包大小居然达到 7.2MB!这大大违背了我们拆包的初衷。 diff-match-patch
的patch_apply
方法在 Android 中存在严重的效率问题,性能一般的机型,光是合并刚才上面 Hello world 的例子拆出来的仅为 121B 的diff.patch
,就花了 3200ms 左右!如果补丁文件再大一些,合并效率会更低,因此完整的业务拆包diff-match-patch
并不适用。
metro
是 react-native 的官方打包工具及 dev server 工具,在执行
react-native bundle
或在 RN 项目下执行
npm start
时,实际是调用 metro-builder 来进行加载打包任务的,类似于 Web 开发常用的 Webpack。针对拆包工作,0.56 之后版本的 RN 依赖的 metro 已经趋于完善,开放了
--config <path / to / config>
参数来配置提供自定义文件,通过这个配置选项,我们就可以通过两个关键方法配置来修改打包配置,这两个方法分别是:
createModuleIdFactory、
processModuleFilter
。
-
createModuleIdFactory
负责固定 module 的 ID。在上文的jsbundle 解析
中,__d
中定义的各个 module 后都有一个数字表示,并在最后的 require 方法中进行调用(如 require(41)),这其中的数字就是createModuleIdFactory
方法生成的。如果添加了 module,那么 ID 会重新生成。如果要做一个基础包,那么公共 module 的 ID 必须是固定的,因此 0.56+ 版本的 RN 可以通过此方法的接口来将 module 的 ID 固定,0.52~0.55 的 RN 依赖的 metro 也用到了这个方法,只是没暴露出来,因此可以通过修改源码的方式来实现 0.56+ 版本相同的效果。 processModuleFilter
负责过滤掉基础包的内容模块,0.56 以下版本没有此方法,因此需要我们自己来实现这个过滤方法。
createModuleIdFactory
的实现,在
metro/src/lib/createModuleIdFactory.js
看到该方法,很简单:
// createModuleIdFactory.js
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== 'number') {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
function createModuleIdFactory() {
if (!process.env.__PROD__) { // debug 模式
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== 'number') {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
} else { // 生产打包使用具体包路径代替 id,方便拆包处理
// 定义项目根目录路径
const projectRootPath = `${process.cwd()}`;
// path 为模块路径名称
return path => {
let moduleName = '';
if(path.indexOf('node_modules\\react-native\\Libraries\\') > 0) {
moduleName = path.substr(path.lastIndexOf('\\') + 1);
} else if(path.indexOf(projectRootPath)==0){
moduleName = path.substr(projectRootPath.length + 1);
}
moduleName = moduleName.replace('.js', '');
moduleName = moduleName.replace('.png', '');
moduleName = moduleName.replace('.jpg', '');
moduleName = moduleName.replace(/\\/g, '_'); // 适配 Windows 平台路径问题
moduleName = moduleName.replace(/\//g, '_'); // 适配 macos 平台路径问题
return moduleName;
};
}
}
module.exports = createModuleIdFactory;
这里我加了一个process.env.__PROD__
的判断,即在开发模式下依旧使用原模式(因为我发现全局改的话开发模式会运行失败)。在打包的时候注入一个__PROD__
环境变量才会进入自定义固定 ID 那段方法。在这段代码中,依据模块路径 path,在其基础上进行自定义路径名称,并作为 Id 返回,使用模块的路径作为 ID,保证返回的 ID 是绝对固定的。
// base.js
require('react-native');
require('react');
... 这里可以引入更多的第三方模块及自己的公共模块// index.js
class App extends Component {
render() {
return (
<View>
<Text>
hello world
</Text>
</View>
)
}
}
AppRegistry.registerComponent("business", () => App);
注:1. base.js 为基础模块入口,index.js 为业务模块入口;2.base.js 没有
AppRegistry.registerComponent
入口,业务模块必须要有这个入口,这个一定要注意,这个是网上很多类似拆包文章没有说清楚的事!!!
接下来就是通过react-native bundle
命令来进行打包了,需要两个不同的命令,区别在于打包入口文件参数(--entry-file)不一样:
react-native bundle --platform ios --dev false --entry-file base.js --bundle-output __test__/example/common.ios.jsbundle --assets-dest __test__/ios/
react-native bundle --platform ios --dev false --entry-file index.js --bundle-output __test__/example/business.ios.jsbundle --assets-dest __test__/ios/
打完包打开 jsbundle 看下require
及__d
对应的 module ID,看下是不是都变成模块路径了,通过这个也可以直观明了地看到 jsbundle 每一行对应的模块。
comm
命令,命令如下:
comm -2 -3 ./__test__/ios/business.ios.jsbundle ./__test__/ios/common.ios.jsbundle > ./__test__/ios/diff.ios.jsbundle
选项 -2 表示不显示在第二个文件中出现的内容,-3 表示不显示同时在两个文件中都出现的内容,通过这个组合就可以生成业务模块独有的代码。在实际实践中,我发现求出来的diff.jsbundle
依旧比较大。观察diff.jsbundle
中的内容,发现diff.jsbundle
和common.jsbundle
中依旧有相同的内容!通过查这个comm
命令的使用方法得知,这个命令对有序文本的过滤效果是比较好的,但是面对我们杂乱无章的 jsbundle 文本,过滤效果就会出现 bug,因此 comm 命令并不能求解出最优diff.jsbundle
。
common.jsbundle
和
business.jsbundle
都是纯文本文件,我们已经把各个模块的 ID 固定,而且每个模块的定义 (也就是 jsbundle 中的 __d) 都是作为文本的固定行排列,两者相同的行肯定是一模一样的,那么就很好办了。只要对
business.jsbundle
进行逐行扫描,判断每一行是否在
common.jsbundle
出现过。如没有出现过,那么肯定就是业务独有的内容,再将这些差异逐行写入
diff.jsbundle
,这样既保证了内容的唯一性,也保证了模块定义的顺序,在后续的异步加载 jsbundle 时不至于出现业务模块定义错乱的问题!实现的 node.js 代码如下 (ps:编程语言和求差算法可以进一步优化):
// diff.js: 找出 common 和 business 的不同行
var fs = require('fs');
var readline = require('readline');
function readFileToArr(fReadName,callback){
var fRead = fs.createReadStream(fReadName);
var objReadline = readline.createInterface({
input:fRead
});
var arr = new Array();
objReadline.on('line',function (line) {
arr.push(line);
//console.log('line:'+ line);
});
objReadline.on('close',function () {
// console.log(arr);
callback(arr);
});
}
var argvs = process.argv.splice(2);
var commonFile = argvs[0]; // common.ios.jsbundle
var businessFile = argvs[1]; // business.ios.jsbundle
var diffOut = argvs[2]; // diff.ios.jsbundle
readFileToArr(commonFile, function (c_data) {
var diff = [];
var commonArrs = c_data;
readFileToArr(businessFile, function (b_data) {
var businessArrs = b_data;
for (let i = 0; i < businessArrs.length; i++) {
if (commonArrs.indexOf(businessArrs[i]) === -1) { // business 中独有的行
diff.push(businessArrs[i]);
}
}
// console.log(diff.length);
var newContent = diff.join('\n');
fs.writeFileSync(diffOut, newContent); // 生成 diff.ios.jsbundle
});
});
node ./__test__/diff.js ./__test__/ios/common.ios.jsbundle ./__test__/ios/business.ios.jsbundle ./__test__/ios/diff.ios.jsbundle
至此,我们的基础包base.jsbundle
和业务包diff.jsbundle
就已经完成了,只要交付给主工程应用,主工程预集成基础包base.jsbundle
,并在启动时预加载基础包,在进入不同业务时按需加载diff.jsbundle
,接下来的工作就是如何在 native 端以此加载这两个 jsbundle 了。
MainActivity.java
和
MainApplication.java
,
MainActivity.java
为原生层应用程序的入口文件,
MainApplication.java
为整体应用程序的初始化入口文件,
MainActivity.java
内容如下:
public class MainActivity extends ReactActivity {
/**
* 返回 index.js 下 AppRegistry.registerComponent() 方法传入的 appKey,
* 用来渲染视图。
*/
@Override
protected String getMainComponentName() {
return "RN_bundle_split";
}
}
MainActivity.java
继承了ReactActivity
类,通过传入的 appKey 来渲染对应的 react-native 界面。
MainApplication.java
内容如下:
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
MainApplication.java
主要完成三件事:
-
实现 ReactApplication 接口,重写 getReactNativeHost
方法,返回ReactNativeHos
t 实例; -
定义并初始化 ReactNativeHost
,实现 getUseDeveloperSupport、getPackages、getJSMainModuleName 方法,完成初始化设置; 在
onCreate
生命周期方法中,调用 SoLoader 的 init 方法,启动 C++ 层逻辑代码的初始化加载。
其中,ReactNativeHost
可以当做是一个 react-native 运行的环境,如果需要通过加载新的 jsbundle 来加入新的业务页面,那么也就需要一个新的 ReactNativeHost,同时也需要一个新的 reactActivity 来渲染业务界面。
MainActivity.java
和
MainApplication.java
是怎么联系起来从而将 react-native 项目完整地跑起来呢?前面有提到,MainActivity 继承了 ReactActivity 类,因此一个 react-native 界面的具体实现就是从
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();
}
/**
* 返回从 JavaScript 注册的主要组件的名称,用于组件的渲染。
* e.g. "RN_bundle_split"
*/
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);
}
/**
* 获取 ReactNativeHost 实例
*/
protected final ReactNativeHost getReactNativeHost() {
return mDelegate.getReactNativeHost();
}
/**
* 获取 ReactInstanceManager 实例
*/
protected final ReactInstanceManager getReactInstanceManager() {
return mDelegate.getReactInstanceManager();
}
/**
* 加载 JSBundle
*/
protected final void loadApp(String appKey) {
mDelegate.loadApp(appKey);
}
}
-
继承 Activity,实现 DefaultHardwareBackBtnHandler、PermissionAwareActivity 两个接口,重写其中的返回事件,及请求权限的方法。 -
构造函数中调用 createReactActivityDelegate 方法,传入 this、和 getMainComponentName 方法返回值,创建 ReactActivityDelegate 实例。 -
重写 Activity 生命周期方法,调用 delegate 实例的对应生命周期方法。 -
定义获取 ReactNativeHost、ReactInstanceManager 实例方法。 定义 loadApp 方法。
很明显,ReactActivity 中采用了委托的方式,将所有行为全权交给了 ReactActivityDelegate 去处理。好处也很明显,降低代码耦合,提升了可扩展性。也就是ReactActivityDelegate
将MainActivity.java
和MainApplication.java
的行为关联了起来。
-
MainActivity.java
定义 appKey,指定界面的渲染入口; -
MainApplication.java
实例化ReactNativeHost
及其他一些 react-native 的环境配置项; ReactActivity
中实例化ReactActivityDelegate,
将MainActivity.java
和MainApplication.java
的行为关联起来,最后在ReactActivityDelegate
中调用createRootView
渲染视图。
在接入多个 jsbundle 时,我们就要依据这些 react-native 的加载原理来进行接入。
ReactNativeHost。
我们新建一个
MyReactApplication
类来初始化
ReactNativeHost
,同时提供
getReactNativeHost
供接下来的自定义
delegate
调用:
// MyReactApplication.java
public class MyReactApplication extends Application implements ReactApplication {
public static MyReactApplication mInstance;
private WeakReference<Application> appReference;
public static ReactNativeHost mReactNativeHost;
private MyReactApplication() {
}
public static MyReactApplication getInstance() {
if (mInstance == null) {
synchronized (MyReactApplication.class) {
if (mInstance == null) {
mInstance = new MyReactApplication();
}
}
}
return mInstance;
}
// 初始化 ReactNativeHost
public void init(Application application) {
appReference = new WeakReference<>(application);
mReactNativeHost = new ReactNativeHost(application) {
@Override
protected String getBundleAssetName() {
// 指向业务包
return "business.jsbundle";
}
@Override
public boolean getUseDeveloperSupport() {
return false;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};
}
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}
MyReactApplication
这个类,需要对这个类初始化,可以在
MainApplication
的
onCreate
中进行初始化:
public class MainApplication extends Application implements ReactApplication {
...
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
// MyReactApplication 初始化
MyReactApplication.getInstance().init(this);
}
}
ReactActivityDelegate
来服务于新的 react-native 环境。下面是自定义实现的
MyReactActivityDelegate
,
getReactNativeHost
方法获取的是
MyReactApplication
的
ReactNativeHost
,其他方法直接使用
ReactActivityDelegate
类中的私有方法:
public class MyReactActivityDelegate extends ReactActivityDelegate {
...
@Override
public ReactNativeHost getReactNativeHost() {
return MyReactApplication.getInstance().getReactNativeHost();
}
...
}
SubActivity
来渲染新的 jsbundle 界面了。
SubActivity
代码很简单,需要返回 appKey,需要注意的是继承
ReactActivity
类,还要覆盖
ReactActivityDelegate
:
public class SubActivity extends ReactActivity {
@Nullable
@Override
protected String getMainComponentName() {
return "RN_bundle_split_business";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
// 使用 MyReactActivityDelegate 覆盖原 ReactActivityDelegate
return new MyReactActivityDelegate(this, getMainComponentName());
}
}
至此,一个可以正常加载 jsbundle 的Activity
已经完成,使用 native 方法startActivity
跳转即可。
MyReactApplication
初始化
ReactNativeHost
的时候,我们看到,最后用到的 jsbundle 只有一个
business.jsbundle
,我们拆包得到的是
common.jsbundle
和
business.jsbundle
两个包。那怎么实现先加载
common.jsbundle,
再加载
business.jsbundle
呢?我们再来回顾下子应用
SubActivity
从初始化到渲染的界面流程:
-
MyReactApplication
在需要加载Activity
的时候初始化,此时也将ReactNativeHost
初始化; SubActivity
初始化MyReactActivityDelegate
,在MyReactActivityDelegate
初始化过程中通过startReactApplication
执行createReactContextInBackground()
方法实现 ReactContext 的创建及 Bundle 的加载逻辑,最终将视图绑定,完成渲染。
因此,RN 加载 js 代码、绑定视图的逻辑可以分开异步执行。利用这个特性,就可以将加载基础包代码、加载业务包代码以及最后绑定视图分步执行,我们在刚才SubActivity
的基础上进行改造。
SubActivity
,我们将这个 Activity 继承普通的
Activity
类:
public class SubActivity extends Activity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
...
}
MyReactApplication
中的
ReactNativeHost
初始化的包改成
common.jsbundle:
...
public void init(Application application) {
appReference = new WeakReference<>(application);
mReactNativeHost = new ReactNativeHost(application) {
@Override
protected String getBundleAssetName() {
return "common.jsbundle";
}
@Override
public boolean getUseDeveloperSupport() {
return false;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};
}
...
MyReactApplication
和
MyReactActivityDelegate
放在
SubActivity.java
的
onCreate
中进行初始化,同时执行
createReactContextInBackground()
完成
ReactContext
的初始化,监听基础包初始化完成,核心代码如下:
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("rndebug", "初始化 MyReactActivityDelegate");
mDelegate = new MyReactActivityDelegate(this, "RN_bundle_split_business");
Log.d("rndebug", "初始化 MyReactApplication");
MyReactApplication.getInstance().init(MainApplication.mainApplication);
final ReactInstanceManager manager = getReactNativeHost().getReactInstanceManager();
manager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext context) {
Log.d("rndebug", "基础包加载完毕");
loadScript(); // 加载业务包
initView(); // 加载视图
manager.removeReactInstanceEventListener(this);
}
});
getReactNativeHost().getReactInstanceManager().createReactContextInBackground();
}
loadScript()
加载业务包,这个方法是在 (1) 初始化的
ReactContext
的基础上,通过调用
CatalystInstance
实例的
loadScriptFromAssets()
方法完成对业务 jsBundle 文件的加载,核心代码如下:
public static void loadScriptFromAsset(Context context,
CatalystInstance instance,
String assetName,boolean loadSynchronously) {
String source = assetName;
if(!assetName.startsWith("assets://")) {
source = "assets://" + assetName;
}
((CatalystInstanceImpl)instance).loadScriptFromAssets(context.getAssets(), source,loadSynchronously);
}
initView()
方法即完成视图加载:
protected void initView(){
mDelegate.onCreate(null);
}
mDelegate.onCreate()
内执行的代码如下:
// MyReactActivityDelegate.java
...
@Override
public void onCreate(Bundle savedInstanceState) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
@Override
public 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);
}
...
这样业务 rn 界面SubActivity
就完成了 jsbundle 的异步加载,也可以根据不同的业务包按需加载。
从上面的实践来看,Android 下 react-native 基于 JavaScript 的设计还是很灵活的,通过createReactContextInBackground()
的调用实现了基础包的预先加载,通过loadScriptFromAssets()
的调用加载业务包,在不使用反射的情况下实现了业务模块的异步加载,降低了反射所带来的性能影响。
Application
、
Activity
、
Delegate
等诸多概念不同,react-native 在 iOS 下的加载简单得多,启动核心文件就一个
AppDelegate.m。
这个类似于 Android 的
Application
,即在应用首次打开的时候会执行里面的内容,
AppDelegate.m
的内容也很简单,主要是 RN 的初始化和视图加载:
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
// 初始化 react-native rootview
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"RN_bundle_split"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
// 将 react-native view 加载到 viewController 中
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
AppDelegate.m
就是获取 jsbundle 的本地路径或线上路径,然后使用RCTRootView
初始化一个视图,初始化传入 jsbundle 的路径和 appkey,最后再将视图添加到 iOS 的viewController
中,这样 iOS 端整个 RN 的加载渲染就完成了。
基于 iOS 的启动流程,如果要接入多个 jsbundle,只需要新创建一个viewController
,再按照AppDelegate.m
的RootView
初始化方法加载不同的 jsbundle 路径和 appKey 就可以轻松实现。
AppDelegate
中,react-native 初始化视图是使用了
RCTRootView
类提供的
initWithBundleURL
方法来加载一个完整的 jsbundle。事实上,查看
RCTRootView
类的源码,发现这个类还提供了
initWithBridge
方法来初始化 RN 视图,react-native bridge 在 iOS 的概念类似 Android 的
reactContext。
因此,先使用
RCTBridge
类加载基础包创建一个 bridge:
#import "RCTBridge.h"
NSURL *jsCodeLocation = [NSURL URLWithString:[[NSBundle mainBundle] pathForResource:@"common.jsbundle" ofType:nil]];
bridge = [[RCTBridge alloc] initWithBundleURL:jsCodeLocation
moduleProvider:nil launchOptions:launchOptions];
接下来就是在这个 bridge 的基础上运行业务包内的代码,从而得到完整的 bridge。
RCTBridge
的
executeSourceCode
方法,该方法传入的内容为 js 代码并执行,但这个方法并没有开放给开发者使用,因此,我们需要手动将这个方法暴露出来,项目根目录下新创建一个
RCTBridge.h
:
#import <Foundation/Foundation.h>
@interface RCTBridge (RnLoadJS)
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;
@end
#import "RCTBridge.h"
,读取业务包的内容并进行加载:
NSLog(@"subapp: 业务包加载开始");
NSString * busJsCodeLocation = [[NSBundle mainBundle] pathForResource:@"business.ios.jsbundle" ofType:nil];
NSData * sourceBus = [NSData dataWithContentsOfFile:busJsCodeLocation options:NSDataReadingMappedIfSafe error:nil];
[(RCTBridge *)bridge.batchedBridge executeSourceCode:sourceBus sync:YES];
NSLog(@"subapp: 业务包加载结束");
RCTRootView
类里的
initWithBridge
将分别加载了基础包和业务包的 bridge 传入,加上 appKey 初始化一个
RCTRootView
,再添加到
viewController
的
view
即可。
RCTRootView * root = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"RN_bundle_split_business" initialProperties:initialProperties];
self.view = root;
静态资源目前的优化控制在文件级别,通过 diff 命令可计算出相邻版本的差异包。
因此不管是 jsbundle 还是静态资源的热更新,目前的优化方案依然是寻求局部最优解,差异包的分发仅仅针对版本号为上一版本的客户端,而面向更老版本的客户端,我们依然采用全量包下发的方案。这是一个工程化的整体考量,首先针对全版本做差异化分发势必大大增加版本发布工作量与复杂度。另外,随着业务子应用逐步趋于稳定,新版本覆盖率往往很高,因此为少数老版本做差异化分发,代价较大。
以上是关于招商证券react-native热更新优化实践的主要内容,如果未能解决你的问题,请参考以下文章