Android ReactNative bundle拆分合成方案

目前大多数的APP 对于React Native 都是一个尝试阶段,用混合开发的方式,在应用中用React Native 去实现个别或则是几个页面。

首先看一下在一个App中嵌入RN页面的主要的类图以及相互之间的关系

ͼƬ 1.png

BaseReactActivity用于加载业务js bundle 文件 YourReactModule和YourReactPackage 实现Native Android代码和 React Native页面通信。

这三个类的详细的类图 如下图所示。

ͼƬx1.png

这里主要讲一下BaseActivity的实现

 protected void iniReactRootView() {  
     ReactInstanceManager.Builder builder = ReactInstanceManager._builder_()  
             .setApplication(getApplication())  
             .setJSMainModuleName(TextUtils._isEmpty_(getMainModuleName()) ? _JS_MAIN_BUNDLE_NAME_ : getMainModuleName())//bundle的名字             .setUseDeveloperSupport(BuildConfig._DEBUG_)//支持debug 摇一摇 reload页面             .addPackage(new MainReactPackage())//添加RN提供的原生模块             .setInitialLifecycleState(LifecycleState._BEFORE_CREATE_);     String jsBundleFile = getJSBundleFile();     File file = null;  
     if (!TextUtils._isEmpty_(jsBundleFile)) {  
         file = new File(jsBundleFile);     }     if (file != null && file.exists()) {  
         builder.setJSBundleFile(getJSBundleFile());//从手机的本地加载文件         Log._i_(_TAG_, "load bundle from local cache");     } else {  
         String bundleAssetName = getBundleAssetName();         builder.setBundleAssetName(TextUtils._isEmpty_(bundleAssetName) ? _JS_BUNDLE_LOCAL_FILE_ : bundleAssetName);//从assets文件下读取加载         Log._i_(_TAG_, "load bundle from asset");     }     if (getPackages() != null) {  
         builder.addPackage(getPackages());//添加自定义的通信模块     }     mReactInstanceManager \= builder.build();     mReactRootView.startReactApplication(mReactInstanceManager, getJsModuleName(), null);     mDoubleTapReloadRecognizer \= new DoubleTapReloadRecognizer();  }  abstract protected String getJsModuleName();  
   
 abstract protected ReactPackage getPackages();  _/**  
  *_ _与__modlue__对应的__js__文件的名称_  _*  
  * **@return**  */_ abstract protected String getMainModuleName();  _/**  
  *_ _从本地__sd__卡读取__bundle__文件_  _*  
  * **@return**  */_ abstract protected String getJSBundleFile();  _/**  
  * assets_ _中自带的_ _bundle__名称_  _*  
  * **@return**  */_ abstract protected String getBundleAssetName();

上面的代码 是ReactNative的初始化,流程 包括 设置Context,加载的bundle文件的路径,自定义的通信模块以及相关的配置。

主要关注一下 setJSBundleFile()这个方法,这个方法非常的重要,通过这个方法RN 可以从手机的sd卡读取文件并且加载显示,这是热跟新实现的基础。举个例子,我们可以将最新的RNbundle文件下载的本地 然后替换掉老的版本,在页面初始化的时候 加载最新的bundle,这样就实现了无需发版 就可以更新页面。

当然这不是今天要讲的重点。 以上所说的都是一些RN的基础知识,当我们将RN运用到实际的项目中的时候发现了很多问题。其中最大的一个问题就是页面加载速度缓慢,bundle文件过于臃肿。

接下来就探讨一下如何解决这个问题。

首先我们可以通过 React Native的打包命令 打包一个最基础的显示 helloworld的index.android.js。

打包命令:react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output app/src/main/assets/index.android.bundle  

index.android.js的源码:

import React, { Component } from 'react';  
 import{  
   AppRegistry,  
   View,  
   Text,  
   DeviceEventEmitter,  
 } from 'react-native';  var TestModule = React.createClass({  
   render: function() {     return (  
       <View style={styles.container}>  
         <Text style={styles.welcome}>  
          hello world  
          </Text>  
       </View>  
     );  
   }  
 });  var styles = StyleSheet.create({  
   container: {  
     flex: 1,  
     justifyContent: 'center',  
     alignItems: 'center',  
     backgroundColor: '#F5FCFF',  
   },  
   welcome: {  
     fontSize: 20,  
     textAlign: 'center',  
     margin: 10,  
   },  
   instructions: {  
     textAlign: 'center',  
     color: '#333333',  
     marginBottom: 5,  
   },  
 });  
   
 AppRegistry.registerComponent('TestModule', () => TestModule);

的打出的业务bundle文件如下图所示

ͼƬy1.png仅仅只是一个普通的helloworld文件打开之后就是密密麻麻的大概有400行,然后找个bundle文件的大小将近有530k,其实仅仅看这个一个文件是看不什么东西的,当你尝试着多打几个bundle包,你会惊奇的发现,打出的bundle包里有绝大部分的内容都是相同的,只有这一行

__d(0,function(e,t,n,r){var l=t(12),o=babelHelpers.interopRequi…….不同

而仔细的观察你会发现 这一行其实就是把你的index.android.js文件进行了简单的压缩和转换,代表的就是当前业务bundle 的代码。如图中蓝圈里标示的。

于是如下图中的四个圈:

红圈 公共的头部部分。

篮圈 js业务代码

绿圈 公共的js方法

橙圈 业务的入口

有了以上的分析以后,我们至少解决了一个问题,那就是 ReactNative 业务bundle臃肿的问题,

使用Reactnative bundle打包后将公共的部分抽离出来,生成一个Common.js,即上图中的红圈绿圈橙圈部分  将业务bundle的生成一个单独的不module.js文件即上图中的绿圈部分。在需要加载相应的ReactNative页面的时候 将 Common.js和业务的module.js生成完整的bundle.js存储到本地,然后通过geJsbundleFile()方法从本地加载。

可参考demo 其、github地址 :https://github.com/pukaicom/ReactNativeBsdiff

dem中用到了bsdiff增量合成方法。该方法的实现参考:https://my.oschina.net/liucundong/blog/160436

这只是解决了部分问题,但是并没有解决ReactNative 页面加载缓慢的问题,通过上面的分析可以知道,如果按照合成的bundle 的方案,在加载每一个RN页面的时候其实 重复加载了大量的文件内容,读取文件到内存是一个耗费时间的过程,如果每个页面都重复读取的话,效率和用户体验明显是不好的,那能不能避免重复读取重复的文件呢,当然是可以的。

可以将公共的部分预先读取到Activity,然后在需要加载某个页面的时候,通过ReactNative的 RCTDeviceEventEmitter机制,发送消息到当前的RN页面,然后通过require方法 加载需要展现的modle的js文件 然后展示。我们先看一下文件的目录结构

ͼƬ z1.png

666.js 和777.js代表的是业务的id,里面的内容如下:

ͼƬ z2.png 

其实就是前面提到的 __d(0,function………………..方法

 只不过 将里面的内容改成了和文件名一样的数字,666.js改为了__d(666,function。。。。777.js改为了__d(777,function…… 这一步很重要,因为一会儿要通过这个id在主页面mainReact.android.js中通过require(id)方法 将该部分的业务bundle读取到内存。

看一下MainReact.android.js的代码:

import React, { Component } from 'react';  
 import{  
   AppRegistry,  
   View,  
   Text,  
   DeviceEventEmitter,  
 } from 'react-native';  
   
 class startComponent extends Component{  
    constructor(props){  
    super(props);    this.state = {  
    content:null,showModule:false    };  
    DeviceEventEmitter.addListener("test", (result) => {  
       let mainComponent = require(result.name);       this.setState({  
       content:mainComponent,  
       showModule:true       })  
    });  
    }  
    render(){  
       let _content = null;       if(this.state.content){  
        _content = React.createElement(this.state.content,this.props);        return _content;  
       }else{       return (<Text>I am the MainPage</Text>)  
       }  
    }  
 }  
 AppRegistry.registerComponent('mainRNModule', () => startComponent);

通DeviceEventEmitter 监听页面跳转的信号,将当前需要加载的页面id放到result.name中,然后通过require获取当前的component然后 通过render展示在当前的页面上。

在Native 原生中发送 Emitter消息的代码如下

public void gotoMainPage() {     //发送事件     WritableMap params = Arguments._createMap_();     params.putInt("name", 666);     reactApplicationContext             .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)  
             .emit("test", params); }

当需要切换当前activity展示的业务bundle页面时 直接通过 emit发送消息到主页面,主页面接收到需要展示的bundle页面的id时通过require将该文件读取到内存并且展示。

需要注意的是由于每次展示的其实是 mainReact.android.js 页面。所以只需要在

改文件中添加这句话即可。

AppRegistry.registerComponent('mainRNModule', () => startComponent);  

其它的业务bundle文件则 只需要将当前文件定义为可以应用的一个component即可:在文件的末尾 将AppRegistry………替换为下面的代码。

module.exports = FamilyAddressComponent;  

具体的参见demo:https://github.com/pukaicom/reactNativeBundleBreak

相关的引用:

     http://reactnative.cn/docs/0.30/integration-with-existing-apps.html#content

     https://my.oschina.net/liucundong/blog/160436