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 6G 148/54.6 154/56 157/55.7 153/56.7 208/59.8
谷歌Pixel 2XL 4G 137.8/60 163/73 176/83 186/91 196/101
红米k30 8G 118/52 143/56 136/55 138/56 142/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 动态列表的资料请关注我们其它相关文章!

(0)

相关推荐

  • 使用react-native-image-viewer实现大图预览

    目录 react-native-image-viewer大图预览 先看一个实现的效果 实例 下面是一个简单的实例代码 Props react-native-image-viewer大图预览 在移动开发中,特别是涉及到图片的应用开发中,经常会遇到图片预览等功能,并且预览支持图片的放大和缩小,在Android原生开发中可以使用PhotoViewPager库实现,如果在React Native中,可以选择使用react-native-image-viewer. 先看一个实现的效果 实例 使用前需要先安

  • react-native 实现渐变色背景过程

    目录 react-native 渐变色背景 react-native学习记录 滚动条 轮播图示例 渐变色 ScrollableTabView默认页面 ScrollableTabView背景颜色 ScrollableTabView选中颜色 ScrollableTabView未选中颜色 react-native 渐变色背景 使用react-native-linear-gradient插件 1.安装 npm react-native-linear-gradient 2.链接 react-native

  • react-native 父函数组件调用类子组件的方法(实例详解)

    react-native 父函数组件调用类子组件的方法,代码如下所示: import React, {Component} from 'react'; import {Text, View, TouchableOpacity} from 'react-native'; // 父 let child onRefbbb = (ref) => { child = ref } clickccc = () => { child.myName() } const Parent =()=> { ret

  • 使用react-native-doc-viewer实现文档预览

    目录 react-native-doc-viewer文档预览 具体步骤如下 react-native初体验的总结 一.前提知识点 二.开发前后相关的一些配置 三.开发项目中用到的知识 四.ui库 五.项目搭建 六.根据需要安装对应依赖 七.开发和调试 react-native-doc-viewer文档预览 react-native项目要求实现word,excel,pdf,mp4,png等格式附件图片的在线预览,最终选用react-native-doc-viewer实现, 具体步骤如下 1.安装r

  • 详解React Native项目中启用Hermes引擎

    目录 引言 一.启用 Hermes 引擎 1.1 Android 1.2 iOS 二.Hermes 引擎使用 2.1 检查 Hermes 引擎是否启用 2.2 绑定Hermes 2.3 使用DevTools在Hermes上调试JS 引言 目前,最新版本的React Native(0.70.0及以上版本)已经默认开启了Hermes引擎.而Hermes则是专门针对React Native应用而优化的全新JavaScript引擎,启用Hermes引擎可以优化启动时间,减少内存占用以及空间占用. 一.启

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

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

  • React学习笔记之列表渲染示例详解

    前言 本文主要给大家介绍了关于React列表渲染的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 示例详解: 列表渲染也很简单,利用map方法返回一个新的渲染列表即可,例如: const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li>{number}</li> ); ReactDOM.render( <ul>{listItems}<

  • React Native学习教程之自定义NavigationBar详解

    前言 在刚开始学习React Native的时候,版本还是0.20,问题一大堆,Navigation这个问题更是很多,首先,是NavigationBar的问题,NavigationIOS有NavigationBar,Navigation却需要自定义一个,最后,我想了想,还是自定义一个view,岂不更好,现在新公司不用RN,我正好有点时间,就把自定义的NavigationBar分享给大家.好了少废话,上代码: 示例代码 // NavigationBar 导航条的自定义封装 // create by

  • React Native之prop-types进行属性确认详解

    属性确认的作用 使用 React Native 创建的组件是可以复用的,所以我们开发的组件可能会给项目组其他同事使用.但别人可能对这个组件不熟悉,常常会忘记使用某些属性,或者某些属性传递的数据类型有误. 因此我们可以在开发 React Native 自定义组件时,可以通过属性确认来声明这个组件需要哪些属性.这样,如果在调用这个自定义组件时没有提供相应的属性,则会在手机与调试工具中弹出警告信息,告知开发者该组件需要哪些属性. React Native已经升级到0.51.0了,版本升级很快,但是对老

  • React Native中Mobx的使用方法详解

    前言 从今天开始我们来搞搞状态管理可否,这几天没怎么写博客,因为被病魔战胜了,tmd,突然的降温让我不知所措,大家最近注意安全,毕竟年底了,查的严,呸,大家注意保暖 特别声明:写该文只是写一下用MobX的思路,有很多地方我会直接放官网链接,因为官网已经写的够详细了 首先来个比较简单的,MobX. 引用官网上的一句话: MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming - TF

  • React Native自定义组件与输出方法详解

    简介 如果你看的这一篇,需要你对ReactNative的开发有一定的了解,此文讲的是在ReactNative提供的组件不能满足需求,或者native用于较成熟的组件想要输出,那么就需要用到自定义组件了. 通过该文,我们也可以对native和JS交互方式进行初步了解,关于输出方法内部实现,我们下一篇再剖. Native module native module就是实现了RCTBridgeModule协议的OC类.RCT就是ReaCT的缩写. 具体步骤如下 引入#import <React/RCTB

  • react 路由权限动态菜单方案配置react-router-auth-plus

    目录 正文 如何使用 1. 配置路由 2. 在应用的最外层渲染路由 权限说明 动态菜单 正文 在 react 中做路由权限管理,一直是比较麻烦的事,不像 vue 中有进入路由前拦截的功能.在摸鱼时间撸了一个傻瓜式配置的路由权限 library (基于 react-router v6). react-router v6 文档地址 react-router-auth-plus github 地址 如何使用 1. 配置路由 import { AuthRouterObject } from "react

  • react 组件实现无缝轮播示例详解

    目录 正文 无缝轮播 实现思路 构思使用时代码结构 Carousel组件 CarouselItem组件 完善组件 完成小圆点 正文 需求是做一个无缝轮播图,我说这不是有很多现成的轮子吗?后来了解到他有一个特殊的需求,他要求小圆点需要在轮播图外面,因为现在大部分插件都是将小圆点写在轮播图内部的,这对于不了解插件内部结构的小伙伴确实不知道如何修改. 很久没有写插件的我准备写一个插件(react) 无缝轮播 无缝轮播从最后一张到第一张的过程中不会原路返回,它就像轮子似的,从结束到开始是无缝连接的,非常

  • 利用IntersectionObserver实现动态渲染的示例详解

    目录 前言 实现 懒加载组件 长列表组件示意 测试 前言 开发表格时,希望支持可视后的动态加载.在查找资料做了一些尝试后,最终使用IntersectionObserver,相对方便地实现了该功能 IntersectionObserver诞生已经有几年了,所以它的兼容性目前已经达到可以使用的程度了.具体兼容程度以及详细API可参考CDN 实现 懒加载组件 核心就是利用了IntersectionObserver的能力,封装了LazyContainer组件,该组件的children,只有在视口中可见时

  • C++ 中继承与动态内存分配的详解

    C++ 中继承与动态内存分配的详解 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性.如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧.下面来看看这两种情况: 一.派生类不使用new 派生类是否需要为显示定义析构函数,复制构造函数和赋值操作符呢? 不需要! 首先,来看是否需要析构函数,如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数.实际上,派生类的默认构造

随机推荐