RN的bundle拆分與合并之安卓篇

在網(wǎng)上看到攜程之前拆分的一些經(jīng)驗(yàn)

先來說一組數(shù)據(jù),一個(gè)Helloorld的App,如果使用0.30 RN 官方命令react-native bundle打包出來的JSBundle文件大小大約為531KB塘辅,RN框架JavaScript本身占了530KB, zip壓縮之后也有148KB沛善。
如果只有一兩個(gè)業(yè)務(wù)使用惠桃,這點(diǎn)大小算不了什么,但是對(duì)于我們這種動(dòng)輒幾十個(gè)業(yè)務(wù)的場(chǎng)景惰爬,如果每個(gè)業(yè)務(wù)的JSBundle都需要這么大的一個(gè)RN框架本身喊暖,那將是不可接受的。
因此撕瞧,我們需要對(duì)RN官方的打包腳本做改造陵叽,將框架代碼拆分出來,讓所有業(yè)務(wù)使用一份框架代碼丛版。
開始拆分之前, 我們先以HelloWorld的RNApp為基礎(chǔ)介紹幾個(gè)背景知識(shí)巩掺。


上述是一個(gè)HelloWorld RNApp代碼的結(jié)構(gòu),基本分為3部分
頭部:各依賴模塊引用部分硼婿;
中間:入口模塊和各業(yè)務(wù)模塊定義部分锌半;
尾部:入口模塊注冊(cè)部分;

上述是HelloWorld RNApp打包之后JSBundle文件的結(jié)構(gòu)寇漫,基本分為3部分 頭部:全局定義刊殉,主要是define,require等全局模塊的定義州胳; 中間:模塊定義记焊,RN框架和業(yè)務(wù)的各個(gè)模塊定義; 尾部:引擎初始化和入口函數(shù)執(zhí)行栓撞;
__d是RN自定義的define遍膜,符合CommonJS規(guī)范,__d后面的數(shù)字是模塊的id瓤湘,是在RN打包過程中瓢颅,解析依賴關(guān)系,自增長生成的弛说。
如果所有業(yè)務(wù)代碼挽懦,都遵照一個(gè)規(guī)則:入口JS文件首先require的都是react/react-native, 則打包生成的JSBundle里面react/react-native相關(guān)的模塊id都是固定的。
拆分方案一
基于上面2點(diǎn)背景知識(shí)介紹木人,我們很容易發(fā)現(xiàn)信柿,如果將打包之后的JSBundle文件冀偶,拆分成2部分(框架部分+業(yè)務(wù)模塊部分),使用的時(shí)候合并起來渔嚷,然后去加載进鸠,即可實(shí)現(xiàn)拆分功能。
具體實(shí)現(xiàn)步驟:
創(chuàng)建一個(gè)空工程形病,入口文件只需要2行代碼客年,require react/react-native即可;
使用react-native bundle命令窒朋,打包該入口文件搀罢,生成common.js;
使用react-native bundle打包業(yè)務(wù)工程(有一點(diǎn)要保證,業(yè)務(wù)工程入口文件前面2行代碼也是require react/react-native), 生成business_all.js侥猩;
開發(fā)工具,從business_all.js里面刪除common.js的內(nèi)容抵赢,剩下的就是business.js;
App加載的時(shí)候?qū)ommon.js和business.js合并在一起欺劳,然后加載;

貌似功能完成铅鲤,可是回到Dive into React Native performance划提, 這么做還是優(yōu)化不了JSBundle的執(zhí)行時(shí)間,因?yàn)槲覀儾荒馨巡鸱珠_的2個(gè)文件分別執(zhí)行邢享,因?yàn)榧虞dcommon.js會(huì)提示找不到RNApp的入口鹏往,先執(zhí)行business.js,會(huì)提示一堆依賴的RN模塊找不到。
顯然骇塘,這種拆分方式不能滿足我們這種需要伊履。
那這個(gè)方案就完全沒有價(jià)值嗎?不是的款违,如果你做的是一個(gè)純RNApp唐瀑,native只是一個(gè)殼,里面業(yè)務(wù)全是RN開發(fā)的插爹,完全可以使用這種方式做拆分哄辣,這種方案簡單,無侵入赠尾,實(shí)現(xiàn)成本低力穗,不需要修改任何RN打包代碼和RN Runtime代碼。
拆分方案二
RN框架部分文件(common.js)大小530KB气嫁,如此大的js文件当窗,占用了絕大部分的JS執(zhí)行時(shí)間,這塊時(shí)間如果能放到后臺(tái)預(yù)先做完杉编,進(jìn)入業(yè)務(wù)也只需執(zhí)行業(yè)務(wù)頁面的幾個(gè)JS文件超全,將可以大大提升頁面加載速度咆霜,參考上面的RN性能瓶頸圖,預(yù)估可以提升100%嘶朱。
按照這個(gè)思路蛾坯,能后臺(tái)加載的JS文件, 實(shí)際上是就是一個(gè)RNApp,因此 我們?cè)O(shè)計(jì)了一個(gè)空白頁面的FakeApp疏遏,這個(gè)FakeApp做一件事情脉课,就是監(jiān)聽要顯示的真實(shí)的業(yè)務(wù)JS模塊,收到監(jiān)聽之后财异,渲染業(yè)務(wù)模塊倘零,顯示頁面。
FakeApp設(shè)計(jì)如下:


為了實(shí)現(xiàn)該拆包方案戳寸,需要改造react-native的打包命令呈驶;
基于FakeApp打common.js包的時(shí)候, 需要記錄RN各個(gè)模塊名和模塊id之間的mapping關(guān)系疫鹊;
打業(yè)務(wù)模塊包的時(shí)候袖瞻,判斷,如果已經(jīng)在mapping文件里面的模塊拆吆,不要打包到業(yè)務(wù)包中

改造頁面加載流程:
因?yàn)橐軌蚝笈_(tái)加載聋迎,所以需分離UI和JS加載引擎<iOS-RCTBridge, Android-ReactInstanceManager>;
進(jìn)入業(yè)務(wù)RN頁面時(shí)候,獲取預(yù)加載好的JS引擎枣耀,然后發(fā)送消息給FakeApp霉晕,告知該渲染的業(yè)務(wù)JS模塊;

通過后臺(tái)預(yù)加載捞奕,省去了絕大部分的JS加載時(shí)間牺堰,似乎問題已經(jīng)完美解決。
但是缝彬,如果隨著業(yè)務(wù)不斷膨脹萌焰,一個(gè)RN業(yè)務(wù)JS代碼也達(dá)到500KB,進(jìn)入這個(gè)業(yè)務(wù)頁面谷浅,500多KB JS文件讀取出來扒俯,執(zhí)行,整個(gè)JS執(zhí)行的時(shí)間瓶頸會(huì)再次出現(xiàn)一疯。
拆分方案三
正在此時(shí)撼玄,我們研究RN在Facebook App里面的使用情況,發(fā)現(xiàn)了Unbundle墩邀,簡單點(diǎn)說掌猛,就是將所有的JS模塊都拆分成獨(dú)立的文件。
下面截圖就是unbundle打包的文件格式:


entry.js就是global部分定義+RNApp入口;
UNBUNDLE文件是用于標(biāo)識(shí)這是一個(gè)unbundle包的flag荔茬;
12.js,13.js就是各個(gè)模塊废膘,文件名就是模塊id;
在業(yè)務(wù)執(zhí)行慕蔚,需要加載模塊(require)的時(shí)候丐黄,就去磁盤查找該文件,讀取孔飒、執(zhí)行灌闺。

RN里面加載模塊流程說明,以require(66666)模塊為例:
首先從__d<就是前文提到的define>的緩存列表里面查找是否有定義過模塊66666,如果有坏瞄,直接返回桂对,如果沒有走到下面第二步的nativeRequire;
nativeRequire根據(jù)模塊id鸠匀,查找文件所在路徑蕉斜,讀取文件內(nèi)容;
定義模塊缀棍,_d(66666)=eval(JS文件內(nèi)容)蛛勉,會(huì)將這個(gè)模塊ID和JS代碼執(zhí)行結(jié)果記錄在define的緩存列表里面;

打包通過react-native unbundle 命令睦柴,可以給android平臺(tái)打出這樣的unbundle包。
順便提一下毡熏,這個(gè)unbundle方案坦敌,只在android上有效,打ios平臺(tái)的unbundle包痢法,是打不出來的狱窘,在RN的打包腳本上有一行注釋,大致意思是在iOS上眾多小文件讀取财搁,文件IO效率不夠高蘸炸,android上沒這樣的問題,然后判斷如果是打iOS的unbundle包的時(shí)候尖奔,直接return了搭儒。
相對(duì)應(yīng)的,iOS開發(fā)了一個(gè)prepack的打包模式提茁,簡單點(diǎn)說淹禾,就是把所有的JS模塊打包到一個(gè)文件里面,打包成一個(gè)二進(jìn)制文件茴扁,并固定0xFB0BD1E5為文件開始铃岔,這個(gè)二進(jìn)制文件里面有個(gè)meta-table,記錄各個(gè)模塊在文件中的相對(duì)位置峭火,在加載模塊(require)的時(shí)候毁习,通過fseek智嚷,找到相應(yīng)的文件開始,讀取纺且,執(zhí)行盏道。
在Unbundle的啟發(fā)下,我們修改打包工具隆檀,開發(fā)了CRNUnbunle摇天,做了簡單的優(yōu)化,把眾多零散的JS文件做了簡單的合并恐仑。


將common部分的JS文件泉坐,合并成一個(gè)common_ios(android).js.
_crn_config記錄了這個(gè)RNApp的入口模塊ID以及其他配置信息,詳見下圖:

main_module為當(dāng)前業(yè)務(wù)模塊入口模塊ID裳仆;
module_path為業(yè)務(wù)模塊JS文件所在當(dāng)前包的相對(duì)路徑腕让;
666666=0.js,說明666666這個(gè)模塊在0.js文件里面;

做完這個(gè)拆包和加載優(yōu)化之后歧斟,我們用自己的幾個(gè)業(yè)務(wù)做了下測(cè)試纯丸,下圖是當(dāng)時(shí)的測(cè)試驗(yàn)證數(shù)據(jù)。


可以看出静袖,iOS和android基本都比官方打包方式的加載時(shí)間觉鼻,減少了50%。
這是自己單機(jī)測(cè)試的數(shù)據(jù)队橙,那上線之后坠陈,數(shù)據(jù)如何呢?
下圖捐康,是我們分析一天的數(shù)據(jù)仇矾,得出的平均值<排除掉了5s以上的異常數(shù)據(jù),后面實(shí)測(cè)下來5s以上數(shù)據(jù)極少>解总;

看到這個(gè)數(shù)據(jù)贮匕,發(fā)現(xiàn)和我們自己測(cè)試的基本一致,但是還有一個(gè)疑問花枫,加載的時(shí)間分布刻盐,是否服從正態(tài)分布,會(huì)不會(huì)很離散乌昔,快的設(shè)備很快隙疚,慢的設(shè)備很慢呢?
然后我又進(jìn)一步分析這一天的數(shù)據(jù)磕道,按照頁面加載時(shí)間區(qū)間分布統(tǒng)計(jì)供屉。

看圖上數(shù)據(jù),很明顯,iOS&Android基本一致伶丐,將近98%的用戶都能在1s內(nèi)加載完成頁面悼做,符合我們期望的正態(tài)分布,所以bundle拆分到此基本完成哗魂。


實(shí)踐

我先用bundle打包命令打一個(gè)bundle出來

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

只有一個(gè)簡單的3k左右的index.android.js,打出了一個(gè)五百多k的index.android.bundle肛走,看看里面是些什么




密密麻麻但又有規(guī)則

  • !function打頭的是公共的頭部部分
  • _d(function是JS文件,用ctrl+s搜索welcome录别,找到我們的index.android.js,原來是在第一行的_d(function,而且結(jié)尾有個(gè)參數(shù)0朽色,其余部分其實(shí)都是公共的js
  • ;require(120),是基礎(chǔ)文件的配置入口组题,require(0)則是業(yè)務(wù)的入口

基于以上葫男,能想到一個(gè)辦法:

  • 內(nèi)置一個(gè)common.js文件,里面包含了bundle文件公共部分的代碼崔列,
  • 業(yè)務(wù)代碼單獨(dú)生成一個(gè)js文件
  • 在需要展示加載某一個(gè)頁面的時(shí)梢褐,將common.js和當(dāng)前頁面需要加載的業(yè)務(wù)js合并,然后再加載

這個(gè)辦法解決了一部分問題赵讯,但加載時(shí)還是一個(gè)整體盈咳。如果common部分能重用,就能大大提升效率边翼。所以就來試試上面提到的unbundle命令

react-native unbundle --platform android --dev false --entry-file index.android.js --bundle-output build/index.android.bundle

生成的bundle只有14行了



但多了一個(gè)js-modules文件夾鱼响,里面的xx.js里面的內(nèi)容就是將之前的__d(xx)抽出來單獨(dú)放到一個(gè)文件里面,通過require(xx)加載到內(nèi)存供調(diào)用


基于unbundle命令再設(shè)計(jì)一個(gè)上面提到的fake頁面用來加載相應(yīng)的業(yè)務(wù)模塊组底,這個(gè)頁面可以預(yù)先在后臺(tái)初始化js引擎热押,將公共部分的common.js文件讀取到內(nèi)存,然后設(shè)置一個(gè)監(jiān)聽事件斤寇,通過emmit方式,當(dāng)需要加載某個(gè)頁面的的module的時(shí)候講這個(gè)頁面的module的id傳遞過來拥褂,然后通過require方法調(diào)用這個(gè)模塊娘锁。


思路差不多是這樣了,來試試看實(shí)現(xiàn)起來有沒什么坑饺鹃。

首先

我拿例子 跑了一下莫秆,瞬間明白了流程是怎么回事,有幾個(gè)關(guān)鍵:

  • DeviceEventEmitter
    前端發(fā)起監(jiān)聽悔详,后端需要用的時(shí)候調(diào)用emit觸發(fā)镊屎,通過返回模塊id,然后return React.createElement(返回的模塊ID,this.props)即可定制加載
  • 配置文件

    這個(gè)配置文件之前不是很理解為什么好多等于0.js、等于1.js,現(xiàn)在明白其實(shí)就是不同bu的入口JS,因?yàn)槎际菃雾撀酚傻男问角洋Γ贿^這個(gè)配置其實(shí)是一套打包的一個(gè)流程缝驳,不在這里做,以后研究打包工具的時(shí)候加上。

然后

我試著把這樣融入到之前的demo里用狱。

先建兩個(gè)test頁面运怖,用于測(cè)試切換。

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
} from 'react-native';

class testeight extends Component {
    render() {
        return (
            <View style={styles.container}>
                <Text style={styles.welcome}>
                    Welcome to Test 8888
                </Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    welcome: {
        fontSize: 20,
        textAlign: 'center',
        margin: 10,
    }
});

module.exports = testeight;

然后在index.android.js加入切換按鈕

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    Image,
    NativeModules,
    DeviceEventEmitter,
} from 'react-native';

export default class AwesomeProject 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 (
                <View style={styles.container}>
                    <Text style={styles.welcome}>
                        Welcome to React Native!
                    </Text>
                    <Text style={styles.instructions}>
                        To get started, edit index.android.js
                    </Text>
                    <Text style={styles.instructions}>
                        Double tap R on your keyboard to reload,{'\n'}
                        Shake or press menu button for dev menu
                    </Text>
                    <Text style={styles.instructions} onPress={() => this.showToast()}>
                        點(diǎn)我調(diào)用原生
                    </Text>
                    <Text style={styles.instructions} onPress={() => this.updateBundle()}>
                        點(diǎn)我更新bundle
                    </Text>
                    <Text style={styles.instructions} onPress={() => this.goNine()}>
                        點(diǎn)我加載頁面9999
                    </Text>
                    <Text style={styles.instructions} onPress={() => this.goEight()}>
                        點(diǎn)我加載頁面8888
                    </Text>
                    <Image 
                        source={require('./img/music_play.png')}
                        style={{width:92,height:92}}
                        />
                </View>
            );
        }
        
    }
    updateBundle () {
        NativeModules.updateBundle.check("5.0.0");
    }
    showToast () {
        //調(diào)用原生
        NativeModules.RNToastAndroid.show('from native',100);
    }
    goNine () {
        NativeModules.BundleLoad.goPage(9999);
    }
    goEight () {
        NativeModules.BundleLoad.goPage(8888);
    }
}

const 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('rnandnative', () => AwesomeProject);

然后把index.android和兩個(gè)test頁面都用unbundle打包

react-native unbundle --platform android --dev false --entry-file index.android.js --bundle-output unbundle/index.android.bundle
react-native unbundle --platform android --dev false --entry-file bundletest1.js --bundle-output unbundle/index.android.bundle1
react-native unbundle --platform android --dev false --entry-file bundletest2.js --bundle-output unbundle/index.android.bundle2

然后把index.android.bundle1夏伊、index.android.bundle2中除了_d的那句打頭的去掉,把__d(0的0改為9999摇展、8888,把文件名改為9999.js和8888.js丟到j(luò)s-modules里溺忧,這個(gè)講的估計(jì)不是很明白咏连,但去看看代碼就懂了。
然后建一個(gè)觸發(fā)emit的方法

public class RNBundleLoadModule extends ReactContextBaseJavaModule {

    private ReactApplicationContext reactApplicationContext;
    public RNBundleLoadModule(ReactApplicationContext reactApplicationContext) {
        super(reactApplicationContext);
    }
    @Override
    public String getName() {
        return "BundleLoad";
    }

    @ReactMethod
    public void goPage(final Integer pageid) {
        System.out.print("########"+pageid+"########");
        // failedCallback.invoke();
        WritableMap params = Arguments.createMap();
        params.putInt("name", pageid);
        reactApplicationContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit("test", params);
    }
}

跑起來鲁森,一切OK祟滴。

參考

https://github.com/pukaicom/reactNativeBundleBreak

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市刀森,隨后出現(xiàn)的幾起案子踱启,更是在濱河造成了極大的恐慌,老刑警劉巖研底,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埠偿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡榜晦,警方通過查閱死者的電腦和手機(jī)冠蒋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乾胶,“玉大人抖剿,你說我怎么就攤上這事∈读” “怎么了斩郎?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喻频。 經(jīng)常有香客問我缩宜,道長,這世上最難降的妖魔是什么甥温? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任锻煌,我火速辦了婚禮,結(jié)果婚禮上姻蚓,老公的妹妹穿的比我還像新娘宋梧。我一直安慰自己,他們只是感情好狰挡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布捂龄。 她就那樣靜靜地躺著释涛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跺讯。 梳的紋絲不亂的頭發(fā)上枢贿,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音刀脏,去河邊找鬼局荚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛愈污,可吹牛的內(nèi)容都是我干的耀态。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼暂雹,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼首装!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杭跪,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤仙逻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后涧尿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體系奉,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年姑廉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缺亮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡桥言,死狀恐怖萌踱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情号阿,我是刑警寧澤并鸵,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站扔涧,受9級(jí)特大地震影響能真,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扰柠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疼约。 院中可真熱鬧卤档,春花似錦、人聲如沸程剥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舔腾,卻和暖如春溪胶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稳诚。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國打工哗脖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扳还。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓才避,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氨距。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桑逝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容