一、RN在携程业务使用现状
2019年,携程开始在线上使用RN框架,并结合自身的业场景,对RN框架进行了开发和改造,研发了CRN框架(以下简称CRN)。2021年,CRN成为携程主流的开发框架。集团内有20+个App接入CRN框架,其中核心的App都已接入。携程旅行App中,200+个业务Bundle在线上运行,业务页面数量超过2000个,超过80%的业务使用CRN。
二、技术选型(为什么选择CRN)
从新技术的选择到落地的实践上看,业务对技术的要求往往是以下几个方面:
1)功能全,全量业务都能快速的适配上线
2)性能好,用户体验多端一致
3)成本低,复用现有在其他平台的运行的代码
为了满足业务需求,鸿蒙的实现技术上我们选择了CRN,主要考虑:
1)基建成熟度高:有配套研发/测试/发布/运营监控系统,内部交流活跃,知识沉淀深
2)业务适配成本小:业务不需要重新再开发一遍,可以使用现有的业务代码
3)开发能快速上手:业务开发还是使用原有的技术进行开发,在鸿蒙上运行
4)产品迭代效率:支持每个周期的产品迭代,快速在鸿蒙系统的手机上线
三、CRN适配实践
3.1 版本升级
线上携程旅行App使用的React Native(RN)版本是0.70.1,而鸿蒙RN版本是0.72.5。因此,适配鸿蒙的第一步是将RN版本从0.70.1升级到0.72.5。
版本升级包含了如下几个方面:
3.1.1 RN版本差异分析
我们对比RN 0.70.1 和 0.72.5 框架库的差异,整体改动点不多。为了降低业务方升级成本,我们在框架底层对废弃的组件和API变更做了兼容,尽可能减少业务使用方的改动。
3.1.2 CRN框架改造
CRN框架覆盖了文档、工具、开发框架、发布、监控、排障全链路。对应框架的改造也从这几个方面进行。
1)在文档方面,我们编写了详细的业务升级文档,列出业务方需要关注的点和常见问题。
2)在工具方面,提供了一键式CLI升级工具,只需在业务工程执行一行升级命令,即可完成工程升级改造。
3)在开发框架方面,改造涉及点比较多,包括:
- 对Native运行时升级,升级RN 0.72.5 核心库,合并对官方RN库的自定义改动点。
- 对JS打包工具升级,支持现有的拆包逻辑,合并对官方RN库的自定义改动点。
- 梳理使用到的社区三方库,统一三方库版本升级至鸿蒙RN三方库要求版本。
- 对Hermes引擎进行升级,合并自定义改动点。
- 对RN自定义组件和API进行新架构改造。
4)在发布方面,对现有的CRN发布系统进行改造,支持选择鸿蒙平台进行单独发布。发布的产物下发和线上IOS/Android进行隔离,保证测试上线阶段,不影响已经上架的IOS/Android应用。
待后续鸿蒙应用稳定,再支持一键同时发布IOS/Android/HarmonyOS Next平台。
又考虑到业务场景存在一套代码,跨RN版本发布。发布系统改造,支持了发布时根据发布单选择的RN版本,自动选择依赖配置进行打包发布。提升业务发布效率。
5)在监控方面,实现鸿蒙端的监控数据上报,接入到现有的监控系统,方便线上监控。
6)在排障方面,实现鸿蒙端的异常数据上报,接入现有排障系统,方便线上排障。
3.1.3 业务工程改造
1)业务方按照提供升级文档和工具进行具体业务工程改造。
2)升级改造后,进行本地开发环境测试,发现问题,解决问题。
3)本地测试通过后,进行打包发布,进入集成测试阶段。
在升级过程中,工作量最大的部分是“RN自定义组件和API实现新架构改造”。
这里先介绍下RN新架构。RN新架构是指从0.68版本开始后的架构。主要包括:
- Turo Modules 模块系统,替换老架构中的Native Modules,用于JS到Native的API同步调用。
- Farbic 组件系统,替换老架构中Native Component,支持同步渲染。
由于鸿蒙RN只支持新架构,所以需要将RN自定义组件和API实现进行新架构改造。在携程旅行App中,我们使用有100+的自定义组件和API。这部分的改造工作量非常大,建议在做适配时优先处理这部分工作。
3.2 差异化工作
在RN版本升级到0.72.5后,开始鸿蒙端特有的适配。
鸿蒙RN框架特点:
- 已实现了官方RN大部分组件、API
- 已实现社区常用的三方库
- 自定义组件和API需要应用开发自行实现
差异化工作:
1)自定义组件和API实现
- 100+自定义组件和API,基于鸿蒙原生开发实现,再封装提供给RN调用
- 按优先级分阶段实现这些自定义组件和API,保持上层JS接口不变
2)RN工程改造
- 添加react-native-harmony和react-native-harmony-cli依赖库
- 适配Platform.OS,Platform.select等API
- 实现xxx.harmony.js文件,逻辑与IOS保持一致
- 升级三方库版本,如react-native-gesture-handler,从1.X版本升级到2.X版本
- 三方库版本升级后,对不兼容的地方做适配
3.3 原生组件开发
携程CRN框架经过近8年的迭代,业务线非常复杂,自定义的组件、turboModule有100多个。
在鸿蒙中适配CRN,首先面临的工作就是将这些自定义组件、turboModule在鸿蒙原生端用ArkTS重新实现。
我们面临以下几个挑战:
1)工作量
这些组件经过了近8年的迭代,开发负责人可能几经易手。有些复杂组件,如信息流组件、自定义地图、日历组件、多媒体组件等,逻辑异常复杂,经过跟原开发负责人、产品等初步讨论,工作量都超过单人一个半月。而我们面临的是100多个组件、turboModule的重实现。
2)HarmonyOS Next逐步完善,与Android、iOS在某些特性上有差异
开发过程中发现了很多HarmonyOS Next功能不完善、存在若干Bug的地方,毕竟是一个新系统,我们与华为同学紧密合作,一一解决了问题,这个过程见证了鸿蒙系统的愈发成熟。
出于安全考虑,鸿蒙系统有一些新特性,比如选取图片视频进行编辑的场景,在Android、iOS中,申请用户权限之后便可以拿到整个系统相册的图片视频,这确实可能存在一些安全隐患。鸿蒙在最开始就切割了这一操作,即使App经用户同意申请了读相册权限,也无法拿到系统主相册的图片视频,本意是让App直接跳到系统相册选取图片之后返回,只提供当次选中的图片信息给App,从而彻底断绝了App侵犯用户隐私的可能。
但我们的多媒体场景比较复杂,用户选取图片、视频后会跳入编辑页,且可以重回相册页选择其他图片,也就是说我们的图片视频选择页与编辑页存在联动,鸿蒙提供的这种跳入系统相册的方式显示无法满足我们的需求。
后续经过讨论,鸿蒙提供了相册Picker的方案,将系统相册页封装为组件提供给开发者,我们的图片视频选择页可以内嵌相册Picker,从而解决了联动的问题。但这个需求从开始评审、开发、测试到最终实现,花费了几个月的时间。
3)RN组件C化
在接入RN的过程中,发现鸿蒙中RN关键性能指标与Android、iOS有差距,华为鸿蒙RN团队为了解决性能问题,提出了组件C化的方案。
简单来讲,就是将ArkTS实现的组件用C-Api重新实现一遍,华为方面给出的要求是容器结点(RN代码中存在标签<></>嵌套的组件)需强制C化。虽然携程中这种必须C化的组件并不多,但也带来了非常多的适配工作。具体可参考下篇-组件C化。
部分组件图如下:
Fabric、TurboModule
最开始,我们在实现相关Fabric、TurboModule的时候,鸿蒙RN框架还没有提供Spec文件CodeGen工具,全靠手写。不过现在已经提供了相关工具,具体操作步骤可以参考相关文档。
Spec文件生成之后,剩下的工作就是相关组件、TurboModule的功能桥接实现,逻辑较为简单,实现相关功能就好。
需要注意的是:
- RN代码中存在标签<></>嵌套的组件被视为容器结点,此类型组件需使用C-API实现。
- 可以通过this.ctx获取RNOHContext,进而获取RNInstance,从而获取一系列RN端JS传入的信息,如View宽高、style等,也可执行发送事件、接收事件、获取TurboModule进行其他操作等等。
- 在RNInstanceImpl构造函数中有一个arkTsComponentNames字段,可以传入所有我们自定义叶子结点Fabric组件的名称,用于在RNOH SDK内部进行指令分发优化。实现ArkTS端Fabric组件后,需要将Fabric组件的名称加入此列表中。
- 假设存在实现过于复杂或者其他原因无法C化的容器组件,RNOH SDK内部指令优化代码需修改(这也意味着RNOH SDK需重新打包编译),关键代码见下文‘性能优化-5.3 RN 指令精简章节。
3.4 组件C化
经过与华为的详细沟通,RN代码中存在标签<></>嵌套的组件被视为容器结点,此类型组件需强制C化。
也就是此类型的组件:
<RNComponent>
<Text/>
<Image/>
</RNComponent>
RNComponent算为容器结点
经确认,携程端存在四个需强制C化的容器结点组件,分别为:
名称 | 描述 |
SwipeoutView | 可滑动组件 |
ScrollView | 滚动组件 |
CustomScrollView | 自定义列表组件 |
CRNModal | modal容器 |
简而言之,需要把ArkTS端实现的组件用C-Api再次实现。
3.4.1 CRNModal C化
CRNModal 在开发测试过程中一步步探索了实现方案,经过多轮测试、方案讨论调整,最终确定了C化方案。
方案一:尝试使用系统Modal实现这个组件
后续测试过程中发现系统Modal的实现方案为系统Dialog,层级很高,携程业务线会出现这样一种场景,RN页面打开Modal后点击跳转一个其他页面,新打开的页面会出现在Modal的下方,不符合需求,方案淘汰。
方案二:尝试通过新跳转一个透明页面的方式实现modal
测试发现新跳转一个页面后,RN的点击事件分发出现问题,无法响应任何事件。且我们App的路由方案为Navigation,经与华为方面沟通,Navigation C化难度非常巨大,短时间不可行。此方案淘汰。
方案三:尝试在RN JS端创建modal
JS端创建一个style为position: 'absolute', zIndex: 999的容器,层级提高,显示在其他组件上方来实现modal。测试发现调用显示Modal的地方很多,可能会在一个嵌套很深的层级中,如果在这里尝试展示这个zIndex: 999的容器,还是会有被遮挡的情况,最终此方案也被淘汰。
方案四:C++层进行插入
经过内部讨论,这个modal应该展示在整个RN页面层级的最上方,这在Android、iOS中都很好实现,但鸿蒙是一个声明式的语言,无法拿到页面实例,无法拿到父组件,也就无法进行插入。
但研究RNOH SDK之后发现,C化后的RNInstance实例在C++端持有一个XComponentSurface,所有RN页面对应的Native组件都被添加显示在这里,而XComponentSurface可以获取rootView实例ComponentInstance,这是一个根控件,将这个根控件强转为ViewComponentInstance之后,在ViewComponentInstance.cpp代码中可以类似Android,获取childCount,通过index添加child等等,进而可以实现在RN页面层级最上方添加modal,最终也是依据此方案,实现了CRNModal组件。
流程如下:
3.4.2 开发注意事项
1)在CAPI instance中声明的Node节点,必须在全局声明,否则会导致node节点不能收到node_event等消息;
2)设置node属性构建ArkUI_AttributeItem的时候,如果设置的值是一个ArkUI_NumberValue类型,需要指定size,这个size的计算必须除去类型的长度,如下:
ArkUI\_NumberValue value\[] = {{.i32 = alignItem}};
ArkUI\_AttributeItem item = {value, sizeof(value) / sizeof(ArkUI\_NumberValue)};
3)animateTo执行动画,在组件析构之后还是会回调,需要控制好生命周期避免crash;
4)设置Stack背景,导致子组件布局错误,是因为Stack被作为同级组件从而导致子组件的postion参数异常,需要手动处理好position问题;
5)可以通过以下方式在C++层调用arkTS方法,获取相关数据:
方案1:在ArkTS里实现一个TurboModule方法,然后通过rnInstance->getTurboModule<XXTurboModule>获取对应的TurboModule,调用方法,获取返回值。但此方案涉及C++与ArkTS的跨端调用,性能会差一些,优点是实现简单。
方案2:通过ArkTSBridge,添加一个ArkTS方法的桥,然后就可以在C++里直接调用这个ArkTS方法。具体实现可以参考NapiBridget.ArkTSBridgeHandler里任意方法。此方案性能好,但实现起来稍微麻烦一点。
6)可以通过以下Api获取设备的高宽
auto displayMetrics = ArkTSBridge::getInstance()->getDisplayMetrics();
displayMetrics.screenPhysicalPixels.width / displayMetrics.screenPhysicalPixels.scale //直接获取到是px单位,需要进行转换,也可以自行修改TurboModle的初始化值:
四、遇到的问题和解决办法
在升级适配过程中,我们遇到了一些RN新架构问题,还有一些鸿蒙RN特有的问题。
RN 新架构问题:
- IOS Animated.timing 设置 useNativeDriver:true 后,内嵌按钮无法点击
- IOS TouchableOpacity 内嵌 Aminated.View ,Aminated.View 开启动画变更位置后,无法点击
- IOS Image样式设置 borderRadius 显示不全
- IOS minimumFontScale maxFontSizeMultiplier 不生效
- Aminated.View 内嵌Modal组件,内部TouchableOpacity点击不响应
- FlatList、ScrollView stickyHeaderIndices 吸顶功能多次滑动后失效
- Aminated.View 、Animated.ScrollView、layoutAnimation 动画卡顿
- 样式中使用了zIndex属性层级可能不生效,尝试添加 position:relative属性后生效
- 组件需要设置默认高宽,不然布局展示可能发生截断
由于动画、样式、性能影响较大,最终决定在RN 0.72.5版本(iOS/Andriod)中只使用Turbo Modules,不开启Fabric模式,来规避掉这些问题。
但鸿蒙RN只支持新架构,新架构存在问题有些在鸿蒙端同样存在。我们和华为伙伴紧密沟通来处理这些问题。对于无法规避问题只能业务侧做兼容处理。
鸿蒙RN特有的问题:
问题:RN Modal弹窗显示时,再打开一个H5页面会显示在Modal下面
解决办法:实现一个View层级的CRNModal替代RN Modal
问题:绝对定位中添加top:“auto”导致元素不显示
解决办法:去除top:“auto”设置
问题:zIndex:-1元素不显示
解决办法:在最外层的View添加collapsable={false}属性
问题:position:absolute样式漂移
解决办法:在外层的View添加collapsable={true}属性
问题:react-native-harmony/metro.config 和现有的自定义metro配置冲突
解决办法:提取react-native-harmony/metro.config中harmony平台相关处理,合并到自定义metro插件中
五、性能优化
华为内部对鸿蒙系统寄予厚望,为了追求更好的用户体验,希望鸿蒙APP核心业务场景性能指标达成业内最佳水平。对携程来说,大多数业务页面都是RN,RN技术栈对性能指标非常敏感,很小的性能优化或劣化,都会大幅影响用户体验。
5.1 CRN预加载
默认情况下,我们会在页面的生命周期中去加载rn_bundle,因为页面已经进入生命周期开始展示了,加载bundle又会有一定的耗时,这种情况下,就会产生白屏现象。
携程也存在某些页面依赖接口数据且接口返回比较慢的情况,比如机票列表页,在进入页面白屏之后又会有长时间的骨架屏,用户体验差。
经过调研,携程端基于系统的FrameNode能力,实现了CRN预加载方案,解决了上述问题。
5.1.1 FrameNode
鸿蒙中FrameNode是一个非常强大的能力,不光是各大厂商在用,官方ArkUI中也大量使用了FrameNode进行性能优化。
它的特点用一句话可以描述:后台离屏渲染,前台上树展示。
利用这个特点,可以实现组件渲染与页面展示的完全分离。也就是说组件的创建渲染不再依赖页面的生命周期,这样我们就可以做很多事情了。
但正因为FrameNode组件会在后台真实渲染,它使用起来会有一定的风险,后台渲染的组件可能会影响前台行为,比如改变状态栏颜色、弹Toast、弹Dialog等,这些都需要人为进行规避。
在RNSDK中我们对Toast、Dialog、状态栏等行为的TurboModule调用,根据页面状态进行了拦截,页面不可见时,上述这些TurboModule的调用都不会生效,而且会记录最后一次拦截的行为及参数,在页面变为可见时,恢复最后一次被拦截的行为。
基于FrameNode能力,实现了鸿蒙中CRN预加载的1.0和2.0方案,下文会详细介绍这两个方案。
另外需要注意的一点是,携程在RN中使用FrameNode过程中,遇过一个困扰许久的问题:使用FrameNode加载RN页面时,在某些比较复杂的页面,会发生非常严重的JS阻塞现象,用户的点击、返回等操作行为被页面渲染指令阻塞,迟迟得不到响应,极度影响用户体验。
经过与华为方面的联合排查,发现是因为RNInstance初始化时传入的参数:disableCnotallow=true导致。此参数会关闭React18一个性能优化的功能:微任务指令批量提交,从而导致在JS代码setTimeout中进行setState时,指令立即提交,总指令数大幅增加,进而大幅影响RN指令处理效率。
大家如果也会在项目中用到FrameNode进行RN页面的性能优化,在初始化RNInstance时,disableConcurrentRoot参数一定要传false。
5.1.2 CRN分包
要理解我们CRN的预加载方案,首先要了解我们的分包逻辑,具体可参考文章:《近万字长文详述携程大规模应用RN的工程化实践》。
总体而言,将业务bundle的加载分为两部分: rn_common & rn_business。其中rn_common包含完整的基础框架能力,rn_business则是具体的业务逻辑代码。通过nativeRequire的方式,分行加载rn_business中的业务代码,然后在加载了rn_common的空白页面上进行渲染。
可以发现,CRN这种分包模式完美契合FrameNode,我们使用FrameNode预渲染一个加载了rn_common的空白页面,这个空白页面不会渲染UI元素且具备完整的框架能力,不会有任何影响前台页面的行为,等到页面真正展示时,才去加载业务代码rn_business,进行UI渲染,从而完美规避FrameNode的使用风险。
也正是基于此,我们实现了CRN预加载1.0的方案。
5.1.3 CRN预加载1.0
预加载1.0方案:
- 在前置页面通过FrameNode预加载一个RNSurface,利用这个RNSurface去加载rn_common,完成后可以理解为后台存在了一个具备所有框架能力的空白页面。
- 用户点击跳转RN页面时,添加一个用户几乎不可感知的延时去加载rn_business。
- 充分利用这个跳转延时 + 页面创建 + 页面切换的动画时间去加载业务bundle、渲染等。
- 业务Bundle加载完成后,动态替换业务自定义的intialProps
- 做到了rn bundle加载、 渲染与页面生命周期的完全隔离。
- 目前预加载1.0方案在全业务默认使用,基本解决了RN页面首帧白屏问题。
在携程的某些业务线中,页面UI依赖网络接口数据,且受外部接口影响,响应较慢。这时候,打开页面会有较长时间的骨架屏loading,也非常影响用户体验。
如果在前置页面中,我们可以大概率猜到用户下一步跳入的目标页面,那是不是可以利用FrameNode将目标页面提前加载,且根据前置页面的参数进行动态刷新,这样用户真正跳入目标页面的时候,就可以直接上屏,达到秒开的效果。
基于此,我们实现了CRN预加载2.0。
5.1.4 CRN预加载2.0
预加载2.0方案:
- 在前置页面通过FrameNode预加载了一个真实的RN页面,完成了加载rn_commom、rn_business、接口请求、渲染等一系列流程。
- 前置页面中影响下一个页面关键参数发生改变时,发消息给后台预加载的的RN页面,RN页面接收到事件,拿到关键参数后进行网络请求,得到数据后对页面进行刷新。
- 用户点击跳转到目标页面时,直接将后台已经预渲染好的页面上屏展示。
- 因为页面已经在后台被真实渲染,有影响前置页面的风险,虽然我们在RN SDK层面已经做了一层拦截,但这种拦截不可能cover所有场景,所有接入了预加载2.0方案的业务都必须在上线前经过完整回归测试。
- 目前,我们在机票列表页及火车票详情页使用了预加载2.0方案。
对比视频:
性能优化关闭:
性能优化开启:
5.2 RN TurboModule运行在Worker线程
前段时间我们在RN JS端对TurboModule调用加了一个埋点,统计TurboModule方法调用的耗时,后续也是根据这个埋点生成了一个报表,发现在鸿蒙中,TurboModule同步方法调用耗时比Android、iOS耗时长10倍,某些方法甚至慢100倍。
经过分析,在Android、iOS中TurboModule都是运行在单独的子线程中,而在鸿蒙中,TurboModule都运行在主线程,主线程要承载一些别的任务比如页面渲染、用户操作行为响应等,这些行为会导致鸿蒙中TurboModule的调用被阻塞,耗时就长。比如下图的Trace,如果TurboModule在UI线程运行,那就可能会被阻塞,阻塞的这段时间,js线程只能等待,而这段等待是毫无意义的。
前段时间,鸿蒙RN SDK也是加入了TurboModule运行在Worker线程这个能力。RNInstance在创建时会同步创建一个worker线程,专门用于TurboModule运行。我们要做的是对工程中TurboModule代码进行适配改造,使之可以运行到worker线程中。
整个适配过程也存在一系列的问题。
首先,鸿蒙的ArkTS衍生自TS语言,基于Actor线程模型,内存不共享,线程间数据通信非常麻烦。
为了解决线程间通信流程繁琐的问题,鸿蒙提供了Sendable注解,可以理解为被这个注解修饰的对象会在共享内存创建。但Sendable存在一个问题,Sendable对象的成员变量只能是Sendable对象或其他特定的数据类型,也就是说我们如果对一个对象进行Sendable改造,就必须对他的所有成员变量进行Sendable改造,也需要对成员变量的成员变量进行Sendable改造,那这个改造过程就存在指数级扩散的问题。
另外,Sendable注解提供的时候,我们大部分代码都已经完成了,在这种成熟的大型项目中再重新进行Sendable改造的成本非常高,大家各自App如果还没开始或者刚开始开发,一定要考虑Sendable适配的问题,比如数据类型默认使用collection下属map、array,class默认添加Sendable注解等。
目前,我们适配完成了7个TurboModule,其他TurboModule做Sendable适配的成本非常高,正在逐步进行中。
5.3 RN 指令精简
在鸿蒙中,RN支持两套组件:C-API实现的组件以及原生ArkTS实现的组件。
C-API实现的组件性能更好,华为的支持力度更大,出于性能考虑,大多数厂商使用RN时,都会选择C-API实现的组件。
C-API组件的Create、Insert、Update、Remove等指令不再需要传递给ArkTS侧。仅仅几个自定义的ArkTS组件需要将指令传递给ArkTS侧。如下图中绿色节点的指令。
鸿蒙RNOH SDK中有默认算法,可以保留叶子节点ArkTS组件的指令,如果项目内没有ArkTS容器组件,RNInstance初始化时添加配置arkTsComponentNames就好。
但在携程的业务中存在AdatpterMap这个容器组件,依赖系统花瓣地图,这个地图C化难度巨大,所以我们的AdatpterMap暂时也只能由ArkTS实现。
这就需要我们自己设计算法。在保证性能的情况下,完整保留AdatpterMap及其子组件的相关指令。
以下是关键代码:
RNOHSDK/src/main/cpp/RNOH/MountingManagerCAPI.cpp::getValidMutations:
facebook::react::ShadowViewMutationList MountingManagerCAPI::getValidMutations(
facebook::react::ShadowViewMutationList const& mutations) {
...
//需要特殊处理,保留容器组件及其子组件所有指令的容器组件名称
std::unordered_set<std::string> whiteListArkTsComponentNames = {
"AdapterMap", "AdapterMapMarkersContainer", "AdapterMapMarker"};
//第一次遍历:只遍历create,从前到后找到混合组件名称,只保存tag
for (auto mutation : mutations) {
if (mutation.type == facebook::react::ShadowViewMutation::Create) {
...
//特殊保留地图容器组件tag
if (whiteListArkTsComponentNames
.count(newChild.componentName)) {
arkTsComponentTags.push(newChild.tag);
}
}
}
if (!arkTsComponentTags.empty()) {
// 第二次遍历:只遍历insert,找到混合组件tag和它的子组件的tag。采用广度遍历方式,这里也只保存tag
for (auto mutation : mutations) {
if (mutation.type == facebook::react::ShadowViewMutation::Insert) {
...
//保存地图容器组件tag和它的子组件的tag
}
}
}
//第三次遍历:根据2中齐全的tag,重新过滤所有指令,保留这些tag的create、insert、update、remove指令。
for (auto mutation : mutations) {
...
//根据组件Tag,保留需要传递给ArkTS的所有指令
}
return validMutations;
}
再来看下成果,测试RN页面中,算法过滤的不需要传递到ArkTS侧的指令数超过99.9%。
页面 | 优化前指令数 | 优化后指令数 |
酒店首页 | 3092 | 11 |
酒店套餐 | 2181 | 0 |
机票+酒店 | 496 | 2 |
酒店 | 3838 | 3 |
美食/购物 | 1542 | 3 |
5.4 分帧渲染
分帧渲染主要用在App的启动优化中。首页宫格存在两屏,二屏在刚开始是不可见的,可以在首页加载完毕之后再去加载宫格二屏。
分帧渲染可以监听到帧渲染的回调,这样就可以对页面元素的加载优先级进行定制,将重要的元素优先加载,不重要或者不可见的元素后续加载。进而提升页面的性能。
关键代码:
private myDisplaySync?: displaySync.DisplaySync
updateStage() {
if (this.stages == 0) {
this.myDisplaySync = displaySync.create();
this.myDisplaySync.start();
this.myDisplaySync.on('frame', (frameInfo: displaySync.IntervalInfo) => {
this.updateStage();
});
}
this.stages++;
if (this.stages == 3) {
this.myDisplaySync?.stop();
}
}
...
build() {
Column() {
Scroll(this.scroller) {
Row() {
//默认加载宫格首屏
if (this.stages > 0){
this.genFirstCell(0)
}
//三个渲染帧之后,加载宫格二屏
if (this.stages > 2){
this.genFirstCell(1)
}
}
...
}
接入分帧渲染,控制宫格二屏的渲染时机后,首页的启动耗时减少了20ms。
5.5 后续性能优化
华为鸿蒙RN团队规划有一个性能优化的feature,在这里简单介绍下。
5.5.1 更换RN JS执行引擎:JSVM(基于V8)
JSVM相较hermers,预计可以提升20%的JS解析性能。前段时间华为提供了一个rn sdk,我们新建了一个分支验证了一下这个JSVM,js的加载速度确实比hermes要快一些。但RN产物jsbundle的加载比hermes要慢,这意味着页面的首屏性能会受到一定影响。这是我们非常关注的一个性能指标,问题得到解决之后,我们也会进行切换。
六、成果和未来规划
经过4个月鸿蒙版本的开发和适配,2024年6月18日携程在鸿蒙应用商店上架了首个全业务全场景的携程旅行鸿蒙版应用。业务方在Android/iOS上的一套CRN代码,只需经过简单的适配,就能正常在鸿蒙系统上运行,甚至有些业务不需要修改,现有的代码直接在鸿蒙系统上能完整的跑完业务流程。
未来,我们还会在以下两个方面持续对鸿蒙CRN框架进行优化:
用户体验
用户体验和性能一直是我们关注的重点,CRN在鸿蒙系统上还有很大的优化空间。我们会持续在性能上继续打磨和提升。
技术布局
为了追求高效率、低成本的研发模式,未来携程业务开发会大量使用一码多端的框架xTaro。后续xTaro会支持鸿蒙系统,真正实现让业务的一套代码能在多端多平台多应用场景上全矩阵运行。
鸿蒙生态的发展是一个持续且快速的过程。随着鸿蒙系统的不断迭代升级和生态的逐步完善,我们会持续为用户提供更加智能、安全、便捷的一站式的旅行应用。