我的编程空间,编程开发者的网络收藏夹
学习永远不晚

React Native 的动态列表方案探索详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

React Native 的动态列表方案探索详解

背景

时至2022,精细化运营已经成为了各大App厂商的强需求,阿里的 DinamicX、Tangram 大家应该都很熟悉了,很多App厂商也自研了一些类似框架,基于DSL的动态化方案虽然有性能上的一些优势,但是毕竟不是图灵完备,一些需要逻辑动态下发的需求实现成本偏高,或由于DSL本身限制无法实现,针对这个问题我们使用RN进行了一下探索尝试, 利用我们已经相对完善的RN基建,结合客户端列表能力低成本的实现了一套的动态化能力,同时兼顾一定的性能体验。

基于 ReactNative 的动态列表方案简单来说就是将 ReactNative 容器内嵌在 RecyclerView 的 ViewHolder 中,由于页面主体框架还是由 Native 开发和渲染,所以首屏加载速度得到了保证,局部的RN实现也使页面获得动态化的能力,从而在性能、”完备逻辑执行“的动态化能力之间取得了一个平衡点,根据我们使用经验对几种动态化方案排序如下:

  • 整体性能体验排序: 纯 Native > 基于DSL动态化方案 >= ReactNative 动态列表方案 > 纯 ReactNative 页面 > H5
  • 动态能力排序: H5 = 纯 ReactNative 页面 > ReactNative 动态列表方案 > 基于DSL动态化方案 > 纯 Native
  • 实现能力排序: 纯 Native >= RN 动态列表方案 = 纯 ReactNative 页面 > H5 > 基于DSL的动态化方案

从以上排序中可以看出 ReactNative 动态列表方案整体处于中等或中等偏上的一个位置,在实现能力上远胜余基于 DSL 动态方案,和 Native 能力基本对等,可以实现一些复杂的UI交互效果,并且相比于纯 RN 实现的页面首屏速度会有非常大的优势,另外不需要对页面整体框架进行更改就能比较方便的嵌入,在开发维护成本上 RN 动态列表方案相比各种基于DSL的动态化方案会有比较明显的优势,不需要额外的开发组件管理平台,排查问题时也不用去读难懂的 dsl,最重要的是 RN 具有图灵完备的能力,所以综合来看使用 RN 内嵌到 Native RecyclerView 来实现 Native 页面部分动态化的方式算是一种性价比相对较高的方式了,值得一试。

技术方案介绍

这里从 Android 视角分享下我们这套方案实现的一些技术细节、原理以及遇到的问题。首先我们常用的一些术语:

  • moduleName 是 RN 离线包的唯一 key,相当于离线包的名字;
  • componentName 是 RN 中 registerComponent 的 component,对应一个 RN 实现的业务的执行入口;
  • 卡片指云音乐首页中每个 viewholder 内部的展示内容,展示的 UI 样式是卡片样式;
  • RN 引擎指以 RN Bridge 为主的整个 JS 离线包运行时环境。

整体方案架构如下:

从图中可以看出整体方案采用数据驱动的方式,服务端通过数据中携带的类型、component、moduleName等字段来唯一指定是否是使用 RN 来渲染,执行 RN 离线包中的哪个 component 逻辑

整体方案上有几个细节点:

  • 采用数据驱动的方式,接入页面无须关注具体展示数据,只需要将数据透传到 RN 的 JS 侧即可
  • 由于 RN 需要将离线包加载后才能执行 JS 生成客户端视图,在 RecyclerView 绑定数据时才开始加载 RN 的离线包势必会拖慢整个模块的展示,所以这里我们做了整个离线包的预加载
  • 首页列表中每个 ViewHolder 的展示元素我们叫做一个卡片,目前采取的策略是多个卡片放在一个 RN 的离线包中,通过同一个 RN 容器来分别展示,避免多个容器消耗过多的资源。

下面从数据流角度拆解整个方案,整体方案可以分为服务端数据定义和下发,容器数据透传,JS侧数据解析三个主要步骤:

  • 服务端数据定义和下发

由于是服务端接口驱动 RecyclerView 中内容展示,接口下发数据中需要有type字段标识使用RN还是Native展示,可以服用Native展示样式标记字段,由于RN中具体展示的样式和运行哪些 JS 代码直接相关,所以服务端下发的数据中需要带上对应的 moduleName 和 componentName,整体数据结构定义如下:

[    {  "type":"rn",        
                "rnInfo":{            
                "moduleName":"bizDiscovery",            
                "component":"hotSong",           
                "otherInfo":{            
                }        
        },        
         "data":{            
         "songInfo":{           
        }        
    }    
    },    
    {       
"type":"dragonball",        
"data":{            
"showInfo":{           
}        
}    
}]

获取到数据之后只需要按照 RecyclerView 正常的使用方法将数据和不同的 ViewHolder 绑定即可

  • 容器数据透传

RN 容器直接直接内嵌在 ViewHolder 中,在 viewHolder 中只需要定义承载 RN JS 渲染视图的 ViewGroup container,RN Bridge 创建好 ReactRootView 后将创建好的 ReactRootView 调用 add 方法添加到 container 中即可,数据传递是透传的方式通过 RN 的 initialProperty 传入到 JS 侧,在 JS 侧解析和使用,数据传递代码如下:

mReactRootView?.startReactApplication(reactInstanceManager, componentName, initialProperties)

这里面需要注意的点是,由于所有使用RN展示的卡片都是对应的相同的 RecyclerView type 即相同的 ViewHolder,所以在 RecyclerView 复用时可能会出现两种情况:

1. 只有一个 RN 卡片,上下滑动 RecyclerView 时发生复用,这时基本不用处理,

2. 存在两种不同类型的 RN 卡片,复用时会运行完全不同的离线包代码,这种情况会导致 JS 侧重新执行渲染逻辑生成全新的视图,上下滚动时如果每次都出现 JS 侧重新渲染,会极大的影响滑动时性能,造成滑动卡顿掉帧,针对这种问题我们对 RN 的 ReactRootView 也做了缓存,

整体架构如下:

从图中可以看到 ViewHolder 中的 container 和 RN 的 ReactRootView 是一对多的关系,RN 的 ReactRootView 在第一次初始化完成后还是挂在 RN 管理的虚拟视图树中,在 RecyclerView 滑动切换不同的展示类型时只需要从 ViewHolder 的 container 中移除不展示的ReactRootView 再重新 add 需要展示的 ReactRootView,不需要 JS 侧重新执行,重新 add ReactRootView 之后还需要将当前的数据再传入 JS 侧以适配相同样式的卡片展示不同数据的需求。这里面的原理是一般情况下我们一个 RN Bridge 只会创建一个 ReactRootView,但是查看 RN 源码,RN 其实支持一个 RN Bridge 绑定多个 RootView 的能力,代码如下:

  public void addRootNode(ReactShadowNode node) {
    mThreadAsserter.assertNow();
    int tag = node.getReactTag();
    mTagsToCSSNodes.put(tag, node);
    mRootTags.put(tag, true);
  }

一个 ReactRootView 即一棵视图树,RN在更新客户端视图时都会遍历所有的 ReactRootView,代码如下:

  protected void updateViewHierarchy() {
    ....
    try {
      for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) {
        int tag = mShadowNodeRegistry.getRootTag(i);
        ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag);
        if (cssRoot.getWidthMeasureSpec() != null && cssRoot.getHeightMeasureSpec() != null) {
          ...
          try {
            notifyOnBeforeLayoutRecursive(cssRoot);
          } finally {
            Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
          }
          calculateRootLayout(cssRoot);
          ... 
          try {
            applyUpdatesRecursive(cssRoot, 0f, 0f);
          } finally {
          }
          ...
        }
      }
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
    }
  }

所以即使使用多个 ReactRootView RN 的渲染逻辑也可以正常执行,这里一个 ReactRootView 即对应 JS 实现中的一个 Component,我们在运行 RN 业务代码会看到 startApplication 的实现在 ReactRootView 中,startApplication 传入的参数就是 Component,对应代码如下:

public class ReactRootView extends FrameLayout implements RootView, ReactRoot {
public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle initialProperties,
      @Nullable String initialUITemplate) {
      ...
  }
}

到此客户端侧的重点实现基本完成了,接下来就是JS侧。

  • JS 侧写法变化

JS 侧的对于卡片开发的写法和正常的 RN 开发基本相同,唯一的区别是需要同时注册多个 component,客户端每个业务卡片启动时只需要启动对应的 Component 即可,代码示例如下:

AppRegistry.registerComponent('hotTopic', () => EStyleTheme(HotTopic));
AppRegistry.registerComponent('musicCalendar', () => EStyleTheme(MusicCalendar));
AppRegistry.registerComponent('newSong', () => EStyleTheme(NewSong));
  • JS 和 Native 通信

至此整个渲染流程都已经介绍完成,卡片已经可以正常展示,不过既然RN具有图灵完备的能力,势必会有一些用户交互导致的UI变化,比如点击卡片上的 ”叉“ 的不感兴趣操作,点击后需要通知客户端弹出客户端的不感兴趣组件,多个卡片对应同一个 JS 引擎,JS 和 Native 的通信通道也是复用的,怎么决定由哪个卡片来弹出呢,我们的做法是在卡片第一次渲染时就使用时间戳的哈希值生成唯一的 key,将这个 key 作为 Native 侧和 JS 侧区分不同业务的唯一标识,和具体展示的业务卡片关联起来在双侧都存储起来,这样后续每次通信时双侧就可以通过 key 来确认通信的对象,确保不会导致通信混乱。

  • RN 引擎预热

在整个 RN 的执行周期中离线包加载一般也会消耗比较多的时间,所以为了尽可能的提升性能,我们还对页面卡片对应的整个离线包进行了预热,即提前将离线包加载到内存中并准备好业务逻辑的运行时环境,预热只需要创建好 ReactInstanceManager 并调用createReactContextInBackground() 即可,调用后整个离线包会被交给 JS 引擎进行预处理,代码如下:

ReactInstanceManager.builder()
                            .setApplication(ApplicationWrapper.getInstance())
                            .setJSMainModulePath("index.android") 
                            .addPackage(MainReactPackage())
                            ...
                            .build()
                            .createReactContextInBackground()

这里还需要注意的一个点是代码调试能力,采用内嵌的方式如果原来页面已经有摇一摇这种手势, RN 原生的调试菜单会无法呼出,这里需要增加额外的交互方式来解决,我们在卡片上增加了一个悬浮按钮。

到此整体框架就都已介绍完毕,在框架之外内存占用和合理的异常处理也是需要考虑的重点。

内存

在整体技术实现之外,我们另外关注的一个重点就是内存占用,我们对以RN Bridge为核心的RN容器内存占用进行了统计,使用Profiler工具获取数据如下:

 无RN容器(native/java)1 RN容器(native/java)2 RN容器(native/java)3 RN容器(native/java)5 RN容器(native/java)
红米k30pro 6G148/54.6154/56157/55.7153/56.7208/59.8
谷歌Pixel 2XL 4G137.8/60163/73176/83186/91196/101
红米k30 8G118/52143/56136/55138/56142/60

整体看来在5个以内RN容器的情况整体内存并没有增加很多,内存占用整体在可控状态,由于此方案采用了一个 RN Bridge 对应多个卡片的方式,所以相当于只新增一个Bridge,对内存影响较小,实际线上运行也没有新增 OOM 问题。

异常处理

  • 出现异常如何处理

不管是 JS 写法原因还是 ReactNative 本身的稳定性原因,总有一定概率会有异常出现,这时需要合理的逻辑处理保证功能和用户体验不会受到比较大的影响,我们当前的处理策略是异常监听还是使用 NativeExceptionHandler 来监听 SoftException 和 FatalException,异常时在统一的回调中通知上层业务(recyclerView 层),然后根据具体的业务情况,由业务层统一消除或者重建 RN 容器,保证体验不受影响或者影响较小,以云音乐首页使用场景为例目前卡片总 PV 约 1 亿,错误率不到万分之一,整体运行情况稳定,无相关用户反馈。

  • RN版本升级导致和数据不兼容如何处理

RN 使用离线包策略,为保证用户能正常获取到离线包和保证离线包能快速高效的更新,我们采取了兜底包集成、更新信息服务端接口搭车等策略,不过受限于用户的机型地区、网络状态等原因还是存在一定概率的更新不成功,对于这种情况我们将当前 RN 离线包支持的卡片信息保存在离线包的配置文件中,通过离线包获取的接口暴露给业务方,业务在运行离线包前可以根据配置信息对网络请求结果进行过滤,保证新版数据匹配旧版的离线包时不会导致异常。

未来规划

短期内我们希望将 RN 动态列表方案结合我们已有的 RN 低代码能力,实现首页运营动态搭建发布,另一方面主要在性能提升,我们目前还是使用的 RN 0.60.5 版本,JS 的执行效率和当前版本的多线程框架是我们的最大的瓶颈,之后我们会在新架构上进行更多的尝试。

以上就是React Native 的动态列表方案探索详解的详细内容,更多关于React Native 动态列表的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

React Native 的动态列表方案探索详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

探索 ASP ListView 的无限可能:打造惊艳动态列表视图

ASP ListView 控件允许开发人员轻松创建和自定义动态列表视图,本文深入探讨了 ListView 的强大功能,展示如何利用它来构建交互式、信息丰富的列表。
探索 ASP ListView 的无限可能:打造惊艳动态列表视图
2024-03-08

Python探索之静态方法和类方法的区别详解

面相对象程序设计中,类方法和静态方法是经常用到的两个术语。 逻辑上讲:类方法是只能由类名调用;静态方法可以由类名或对象名进行调用。 python staticmethod and classmethodThough classmethod
2022-06-05

深入探索C++中跨平台移动开发的解决方案

c++++可提供以下跨平台移动开发解决方案:跨平台开发框架:qt、juce、silk实战案例:使用qt开发跨平台计算器应用其他工具和技术:cmake、nativescript深入探索C++中跨平台移动开发的解决方案前言跨平台移动开发已成
深入探索C++中跨平台移动开发的解决方案
2024-05-11

动态更改Spring定时任务Cron表达式的优雅方案实例详解

spring定时器非常强大,但是有时候我们需要在不需要重启应用就可以动态的改变Cron表达式的值,下面这篇文章主要给大家介绍了关于动态更改Spring定时任务Cron表达式的优雅方案,需要的朋友可以参考下
2022-12-22

Vue3实现动态导入Excel表格数据的方法详解

在开发工作过程中,我们会遇到各种各样的表格数据导入,动态数据导入可以减少人为操作,减少出错。本文为大家介绍了Vue3实现动态导入Excel表格数据的方法,需要的可以参考一下
2022-11-16

探索阿里云ECS系列2与系列3为您的业务提供最佳的云计算解决方案

阿里云ECS系列2和系列3是阿里云的高性能计算服务,提供多种配置和规格,以满足不同的业务需求。这两款产品都具有强大的计算能力和高效的资源利用效率,旨在帮助企业和个人用户快速构建和扩展计算资源,提高业务效率。探索阿里云ECS系列2与系列3:为您的业务提供最佳的云计算解决方案在数字化转型的今天,云计算已成为企业不可或
探索阿里云ECS系列2与系列3为您的业务提供最佳的云计算解决方案
2023-12-18

查找解决缓慢下载问题的有效方法:探索pip镜像源的完整列表!

解决下载慢的问题,尝试使用pip镜像源大全!随着互联网的飞速发展,我们几乎每天都要进行各种下载操作,无论是下载软件、包或者是安装库,都离不开下载过程。然而,有时我们可能会遇到下载速度慢的问题,这不仅浪费时间,还会影响工作效率。针对这个问
查找解决缓慢下载问题的有效方法:探索pip镜像源的完整列表!
2024-01-16

编程热搜

目录