React Native按需加载 手Q狼人杀探索之路

转自 腾讯Bugly

| 导语 最近特别火的狼人杀和最近特别火的React Native会擦出什么样的火花呢?本文和您一同探讨RN性能优化的现实场景。

项目简介:

狼人杀游戏是多人实时性游戏,对流畅度等性能都有要求。作为大型游戏,无论从代码规模和迭代速度来看,手Q的安装包和版本迭代速度都无法用native来承载这样的游戏。从而React Native成为了比较好的选择。

手Q React Native 简介

在手Q目前使用的React Native 版本是0.15版本。下面的数据分析都是基于手QRN0.15版本进行的分析数据。

问题分析

开发过React Native的同学,大体都对白屏界面有所了解。作为RN原生自带功能,基本上每个使用RN的业务都在优化这一阶段。通过对狼人杀的测试来看,首次从RN启动到渲染,耗时基本有1.7s左右。而这些耗时数据还是在iPhone6s中测试得出,可想低端局的情况可能会更加糟糕。

分析性能

工欲善其事必先利其器,要分析其耗时。还得从源头着手,根据常规做法,都会将React Native打包的js拆分成Base Bundle和业务Bundle。从上图,RN  加载流程来看,加载BaseBundle与业务Bundle的耗时是可以有优化空间的。

优化的方案和大多数人的思路一样,只需在业务启动前预加载BaseBundle与业务Bundle即可达到优化时间的效果。

目前所遇到的瓶颈

在优化的开始,我们可能一直把精力放在BaseBundle中,认为BaseBundle是RN的公共库,体积肯定不小。但是从数据来看,我们的狼人杀业务Bundle已经是1.8MB(纯js代码,不包括资源文件)而BaseBundle只有918KB,已经是两倍的体量。现在还只是狼人杀业务的初期,随着业务的快速迭代,业务Bundle只会更快的增加。而过大的业务Bundle所导致的加载时间也会加长。

可能有同学会说,这不是有预加载嘛。我承认,预加载确实解决了绝大部分业务Bundle的加载耗时。但是,并不是每次预加载都可以刚刚好预加载好业务Bundle。虽然业务Bundle加载耗时变长,预加载好的几率就会慢慢变低。

而这不是最关键的行为,最关键的是内存的消耗,我们来看一张图

从上图就可以看出,仅仅是BaseBundle,仅仅只是在内存中展开,还没有到运行。这个时候内存消耗已经达到了6MB。而整个狼人杀RN渲染起来,则消耗了20MB以上的内存。而这还没有包括业务使用的内存。在手Q中,内存的消耗是巨大的,而留给狼人杀使用的内存其实已经很少了。从这里可以看出,内存的优化好像更加迫在眉睫。

React Native 按需加载

React Native的思路是在业务运行之前,将所有js代码在JavaScriptContext中展开。这个逻辑本身没有什么问题。但是,我们需要改造成按需加载。按需加载的本质就是将不是关键路径的业务RN拆分开,变成插件中的插件。当业务触发到此逻辑的时候,再去将js代码动态展开。达到动态执行的目的。

而我们想要达成按需加载的效果,可能会面临着三个挑战。

1.js在动态运行的时候,代码注入的问题。

2.js模块与模块之间相互引用的问题。

3.打包工具改造的问题。我们来依次看下这三个问题。

动态注入

1.从JS层面分析,想要达到JS代码的动态注入。必须要和运行的JS在相同运用域下面。我们通过分析打包后的JS代码得知,必须要在__d(verboseName + 模块名称)作用域下面。

2.从native层面分析,想要达到JS代码的动态注入。则必须要拿到JavaScriptCore中的JSContext。

- (void)enqueueApplicationScript:(NSData *)script                             url:(NSURL *)url                      onComplete:(RCTJavaScriptCompleteBlock)onComplete{  RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");  RCTProfileBeginFlowEvent();  [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {    RCTProfileEndFlowEvent();    RCTAssertJSThread();

if(scriptLoadError) {      onComplete(scriptLoadError);

return;    }    RCTProfileBeginEvent(0, @"FetchApplicationScriptCallbacks", nil);    [_javaScriptExecutor executeJSCall:@"BatchedBridge"method:@"flushedQueue"arguments:@[]                              callback:^(id json, NSError *error)     {       RCTProfileEndEvent(0, @"js_call,init", @{         @"json": RCTNullIfNil(json),         @"error": RCTNullIfNil(error),       });       [self handleBuffer:json batchEnded:YES];       onComplete(error);     }];  }];}

而上述函数则是比较关键的执行函数,需将此函数从RN内核中暴露出来。

模块相互引用

如果要实现按需加载,则主逻辑JS中包含的其他插件JS代码,则不能在主逻辑JS展开的时候运行。我们想要实现这样的效果,则有两个方案可以实施(二选一即可)。

1.跟进JS动态执行的原理,我们可以将主业务JS A中引用插件 B的实现函数使用空方法d(verboseName + 业务名{空}) 代替。然后等到运行时,再注入相同的方法(d(verboseName + 业务名{真实方法}) )。等业务触发了插件B逻辑的时候,真正运行的是刚刚注入的B真实方法。

2.懒require

我们平常的业务代码基本是这样引入另外一个模块的

importGameWait from'../gameWait/gameWait';

importNetOperation from'../netOperation/NetOperation';

importGameNight from'../gameOperation/GameNight';

importGameDay from'../gameOperation/GameDay';

importGameState from'../gameState/GameState';

import{GameStateEnum} from'../gameState/GameEnum';

最终打包工具会把他打包成这样的

var _gameWaitGameWait = require('react-

native/Werewolf.zip.dir/module/gameWait/gameWait.js');

var _gameWaitGameWait2 = _interopRequireDefault(_gameWaitGameWait);

var _netOperationNetOperation = require(

'react-native/Werewolf.zip.dir/module/netOperation/NetOperation.js');

var _netOperationNetOperation2 = _

interopRequireDefault(_netOperationNetOperation);

var _gameOperationGameNight = require(

'react-native/Werewolf.zip.dir/module/gameOperation/GameNight.js');

var _gameOperationGameNight2 = _

interopRequireDefault(_gameOperationGameNight);

var _gameOperationGameDay = require(

'react-native/Werewolf.zip.dir/module/gameOperation/GameDay.js');

var _gameOperationGameDay2 = _

interopRequireDefault(_gameOperationGameDay);

而这些在业务函数体中,会在编译的时候去找寻此文件是否存在。而这样会报错。

正确的做法是在业务逻辑中,再去require其模块。

if(this.state.nowGameStateEnum === GameStateEnum.game_start) {        var GameWait = require('../gameWait/gameWait');

this._changeToDay();

return(                     );}

在打包工具中展示则是这样的效果。

if(this.state.nowGameStateEnum === _gameStateGameEnum.GameStateEnum.game_start) {      var GameWait = require('react-native/Werewolf.zip.dir/module/gameWait/gameWait.js');

this._changeToDay();

return(         _React2.default.createElement(GameWait, {         onClosePage:this._onCloseWait.bind(this)      }));}

这样就实现了require的懒加载。实现了先运行主业务,再动态运行插件业务。

打包工具改造

resolve(ReactPackager.createClientFor(options).then(client => {      log('Created ReactPackager');

returnclient.buildBundle(requestOpts)        .then(outputBundle => {          log('Closing client');          client.close();

returnoutputBundle;        })        .then(outputBundle => deleteBaseBundle(outputBundle))        .then(outputBundle => processBundle(outputBundle, !args.dev))        .then(outputBundle => saveBundleAndMap(          outputBundle,          args.platform,          args['bundle-output'],          args['bundle-encoding'],          args['sourcemap-output'],          args['assets-dest']        ));    }));

打包工具的改造,重要的是将业务Bundle拆分成不同的插件。这个可以仿照以前BaseBundle与业务Bundle拆分的做法。

按需加载小结

RN按需加载,只是一个思路。当业务逐渐庞大的时候,相信大家都会面临这个问题。不过,安卓则比较幸运一点。RN有一个原生的unbundle命令可以将业务Bundle以每个业务一个js文件。不过unbundle命令不能打出iOS平台的,解释是因为iOS上面对小文件有IO性能的瓶颈。不过,这里我就没有亲自测试过了。不过个人感觉,真正做到按需加载,就得根据业务做不同的打包,不易过大,也不易过小。平衡才是王道。

后续

大家从上文耗时表可以了解到,预加载和按需加载,只是优化了启动耗时的一部分。而RN在执行RunApplication到RNComponent展示出,中间还有800ms的耗时。这部分目前来看,不管是狼人杀大型业务的启动,还是demo业务的启动,都会有这800ms的耗时,应该与业务大小无关。从时间表来看,是js在大量绘制ReactNativeBaseComponent。所以,这部分应该也有优化的空间。后续有进展再和大家分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容