登录 立即注册
安币:

安卓巴士 - 安卓开发 - Android开发 - 安卓 - 移动互联网门户

演讲实录——React-native技术在腾讯课堂中的实践及优化 [复制链接]

2019-5-13 10:10
九霄逆鳞 阅读:103 评论:0 赞:1
Tag:  

本文来自2019安卓巴士开发者大会现场实录,由于录入匆忙,内容可能存在偏差,欢迎大家扫描文末二维码查看现场实录视频和下载大会完整PPT。演讲人:王华杰

大家好!我叫王华杰,来自腾讯在线教育部,主要做腾讯课堂app的开发工作,这次分享的题目是RN技术在腾讯课堂中的实践和优化。


首先我们看一下RN在腾讯课堂的使用情况。腾讯课堂App首页共有4个TAG,其中“首页”和“分类”两个页面是采用RN技术开发,一开始腾讯课堂首页TAG是native代码实现,后来才换成RN技术来实现,那么为什么腾讯课堂会在首页这种重要场景选择用RN技术来实现呢?



首先我们来看一下移动开发中的技术选型,在RN出来之前,移动开发基本有两种技术方案,一种是H5方案,在app中嵌入H5的页面,一种是直接采用native代码写的页面,这两种方案各有优点和缺点。H5实现的页面呢,他具有开发效率非常高,而且是跨平台支持的,H5写一套页面可以同时跑在Android和iOS上,还有重要的优点就是可动态发布,不需要跟随app的版本,但是他的缺点就是,体验相对差了点,性能也比原生的页面差,调用原生能力也比较差。而native技术开发的页面呢,开发效率比较低,而且跨平台比较难,基本是android和iOS各写一套代码。动态更新也很困难,一般都是要通过发版本来解决,而且发版本后新版本的普及也需要很长时间的。但是采用native技术开发的页面也有他的好处,首先他体验好,性能高,调用原生能力也非常强。



那么有没有一种解决方案能够把两者的优势综合在一起呢?答案是有的,那就是RN。RN是采用javascript语言来写具有原生体验app一种手段,他的基本原理就是,用js来描述UI逻辑,渲染则是使用Native的view来渲染上屏,这样他就综合了H5和native的优点,具有开发效率高,可跨平台和动态发布,而且高性能体验好。腾讯课堂是怎么引入RN的呢。



腾讯课堂对RN有一个很大的拓展和改造,原生的RN针对的业务场景比较单一,一般bundle是打包成一个包,稳定性和性能不够好。启动速度相对native有比较大的差距,官方的组件API比较少,整体不够优秀。我们在原生基础上做了拓展和改造,首先是对多业务场景做了优化,还有它的稳定性,我们解决了大部分稳定性问题,而且提高了页面的加载速度。功能上支持了业务的分包加载,还支持bundle发布管理,还有动态更新,还定制了很多高性能的组件和API。



整体架构,主要分三部分:工具部分包括代码的和打包分包工具。Native能力部分我们会提供很多高性能的API和组件,API有存储、下载等。发布部分有发布系统,会做一个离线包的管理。还有差异更新,出现异常的监控和降级。还有性能监控。


腾讯课堂中除了有RN技术实现的页面,也有很多采用H5技术实现的页面,但是有些native能力需要同时向RN和H5提供,由于两种方案的技术差异,会造成两种提供方式,就是我们需要为h5写一套native-api,同时也为RN写一套native api。H5中的,js调用native能力的时候通过h5的bridge调用native-api, RN中则是通过rn的bridge来来源native-api,那么怎么把同一份native-api同时提供给H5和RN呢?



先看一下H5的bridge实现,我们打开一个web页面时,在开发者选项里可以看到有很多网络请求,而这些请求webview都是能拦截到的,那么我们基于这种方式来实现调用native的能力,就是js发起一个网络请求,h5bridge作为协议头,后面带有module,method这些参数,webview判断到这个请求是h5bridge协议的,就把这个请求拦截到并解析协议参数然后去调用native的api。



那么RN的bridge呢,在RN中带调用native的通道已经是有了,像UIManager.createView这些都是调用native的api,目前RN支持的参数类型有:整形,浮点型,字符串,数组,map,function,但是对function的支持有一些局限性:数组和map里面不支持function类型,function仅能在参数列表的末尾,最多两个(就是成功和失败的回调),而且function只能执行1次。



那么提供给H5和RN的接口怎么统一呢?假如js需要调用native的存储接口,js调用native时,H5调用走h5brige, RN中,我们写了个NativeBridge的RN module他,通过这个module来调用。这个NaitiveBridge的调用会转换为h5的h5bridge://Storage/get/…或者RN的NativeBridgeModule.call(“Storage”, “get”,  …),就是把模块名字,方法名字和参数传到native进行分发到对应的native-api里处理



传入一个大参数一个map, 有key和一个success的回调,这样api格式很像小程序的api, 比较直观优美。但是这种调用在原生RN上是不能支持的,因为RN的调用不能再map里嵌套函数



那么腾讯课堂怎么支持这个功能的呢?首先参数转换成:NativeModules.NativeBridge.call(“Storage”, “get”,  “[…]”),“Storage”作为模块名,get作为方法名传入,还有一个为参数列表的字符串。参数怎么转换呢?我们队参数列表进行转成json字符串,对函数类型做了特殊处理就是把function用一个自增的id替换,并把这个id和function对象关联起来,最终的调用会转化为NativeModules.NativeBridge.call("Storage", "get",  "[{"key": "key", "success": 1}]"),回调的时候只需要把funcId传回来就可以了,根据funcId就可以找到对应的function对象,然后执行。在native这个module怎么定义呢?1、继承ExportedModule,并把Module注册到h5或者RN中bridge中,2、使用Exported注解导出方法,3、这个模块方法即可同时暴露给H5与RN。经过这种方式的统一,我们解决了RN bridge的局限性,我们可以再object和array中潜入function,可以对function进行多次回调。 



接下来讲一下分包和模块化的加载。首先我们来看一下腾讯课堂的首页加载耗时分布,从图中可以看出 js inti和bundle加载时耗时比较多,因为腾讯整个的bundle接近2M,所以加载比较耗时,因为它有很多业务,所以它的bundle会比较大。其中RN框架至少有500多K。多业务场景下bundle冗余,比如说我加载首页,那么没必要把其他页面的js也加载进来的,基础bundle基本不更新,业务bundle体积小,易动态更新,所有分包还是有必要的。



主要的思路就是把bundle中的业务代码和框架代码分离。这样就可以在app启动时先预加载框架的js代码,打开页面时再加载对应页面的js代码,提高页面的加载速度。



先看一下, RN目前的打包方式,RN支持有两种打包方式,普通的bundle打包方式,就是把所有的js模块输出到一个js bundle文件里面,加载时完整加载整个bundle文件,还有一种打包方式就是unbudle, unbundle又分两种,一种是assetde unbunlde,一种是asset file的bunblde, 每个模块是一个文本文件,打包的产物中有几百个文件,这个是android的unbundle打包方式,因为ios在大量小文件时有io瓶颈,所有ios 采用的indexd-file的unbundle打包方式, 将所有模块输出到一个带索引表的二进制文件,文件头中记录各个模块在文件中的相对位置,加载时按模块加载。



使用普通bundle打包之后Bundle文件的结构如下,也主要包含3部分


头部:全局定义,主要是define,require等全局模块的定义

中间:模块定义,RN框架和业务的各个模块定义

尾部:引擎初始化和入口函数执行。



业界主要有两种分包方案,一种是基于RN bundle打包方式,将bundle文件拆分成2部分(框架部分+业务模块部分),目前主流拆包方式,按需加载粒度:业务包。另一种是基于RN unbundle assets的打包方式,将bundle文件拆分成各模块部分,可实现按需加载, 按需加载粒度:模块


腾讯课堂中RN的分包方案是怎么样的呢?基于RN unbundle 带索引表的打包方式,将bundle文件拆分成2部分(框架部分+业务模块部分),可实现按需加载,按需加载粒度:模块。



基于unbundle index-file方案进行分包。分包前的bundle结构,包含:模块配置表,启动代码,和很多模块。模块配置表中会记录每个模块在文件中的偏移量和大小。那么怎么分包呢,分包就是把bundle文件中业务模块抽离出来成为业务bundle,剩下的为主bundle, 主bundle的模块配置表记录了主bundle的模块信息和启动代码,业务bundle的模块配置表记录业务模块的信息。这里需要注意的一点是,由于分包后可以单独打包,模块id不能采用原来的自增id, 主要是为了避免id冲突,所以需要修改id的生成方式,比如直接用模块的路径作为id。



分包后的bundle怎么加载呢?首先加载所有bundle的模块配置表,这样就支持每个模块在哪个文件的哪个位置了,接下来加载启动代码运行,运行过程中会有require加载模块,如果这个模块没有加载,会走到nativeRequire,根据moduleid从模块配置表里找到对应的模块在文件中的位置然后加载对应的模块。



经过分包之后,腾讯课堂的首页从983kb能够降到578kb左右。课堂的分类从983kb降到400多kb。首页可以从1.2秒减少到800毫秒。经过分包,对bundle的加载速度是有很大提升的。



接下来看一下发布和动态更新。代码在git仓库中管理,打包系统把业务代码打包后上传到AK离线发布系统,根据前面发布的版本,生成一个差量包。App在检查更新的时候会带上当前bundle的版本号,发布系统会对版本号进行匹配,找到对应的差量包给app更新,匹配不到差量包就使用全量包更新。



如何做到及时刷新?进入闪屏时会检查是否有更新,为了不耽误闪屏跳转,最多只等检查更新1s中,如果有更新的话,把等待时间延长到3s中,下载更新包,然后闪屏跳转。然后看看本地的bundle文件是否已经更新了,如果更新了,就重新创建RNContext如果没有更新,则直接加载RN页面,在app切到后台时,会触发检查更新,当然这里会有频率限制。



对于异常处理和降级是怎么处理的。目前腾讯课堂一开始发现很多crash,基本上是发生在RN源码里面,解决不了会做try-catch,会做bundle的版本回退,或者降级到h5的页面。bundle加载过程中的Crash,我们会把这个异常try-catch,再进行处理。如果是业务加载过程中出现的JS异常的话,我们都可以做上报。有一些native模块的报错。还有比较常见的一个是JNI层的so加载报错,主要是安卓的so路径可能不对,可以从APK里面重新取它的so然后加载。还有React View层报错, 通过try-catch,可能会有一些功能上的不正常。可以判断出当前用户的异常次数,如果异常短期之内超过3次之后会直接降到H5。如果发现某个机型有异常,我们可以对某个机型进行降级到H5。



经过异常处理,腾讯课堂的Crash率从0.1%降到0.01%,已经非常低了。


讲一下未来的探索。因为RN现在的性能还是有待提高的,像一些高性能List和启动速度都是可以提高的。现在RN在做框架的重构,性能上有很大的提高。稳定性,RN的款方Crash率比较高。目前腾讯课堂首页有H5版本,有RN版本,两套代码,我们希望以后只需要一套代码就可以支持RN和H5。目前Flutter采用dart语言开发,对于习惯了js开发的前端开发者来说还不够友好,所以可以探索RN的Flutter模式。



谢谢大家!




现场PPT分享:

      关注【安卓巴士Android开发者门户】公众号,后台回复“420”获取讲师完整PPT。


大会现场视频小程序:



欢迎前往安卓巴士博客区投稿,技术成长于分享

期待巴友留言,共同探讨学习


分享到:
我来说两句
facelist
您需要登录后才可以评论 登录 | 立即注册
所有评论(0)

站长推荐

通过邮件订阅最新安卓weekly信息
上一条 /4 下一条

下载安卓巴士客户端

全国最大的安卓开发者社区

广告投放| 广东互联网违法和不良信息举报中心|中国互联网举报中心|下载客户端|申请友链|手机版|站点统计|安卓巴士 ( 粤ICP备15117877号 )

返回顶部