关于react-native:React-Native-无限列表的优化与实践

10次阅读

共计 5738 个字符,预计需要花费 15 分钟才能阅读完成。

导语
本文介绍了在应用 React Native 开发过程中,如何对有限列表组件进行技术选型,如何应用 RecyclerListView 组件对有限列表进行性能优化,如何解决有限列表与标签页搭配应用时的内存优化与手势重叠的问题,心愿对大家有所启发。

背景
对于分类信息流状态的产品,用户通过左右滑动切换分类,通过一直上滑来浏览更多的信息。

用标签页 (Tabs) 实现切换分类,用有限列表 (List) 实现上滑浏览
手势上滑,页面向上滚动,展现更多列表项 (List Item)
手势左滑,页面向左滚动,展现左边的列表(蓝色)

因为 React Native(RN)能够用较低的老本,同时满足用户体验、疾速迭代,和跨 App 开发上线的要求。所以,对于分类信息流状态的产品技术选型应用的是 RN。在应用 RN 开发首页的过程中,咱们填过很多坑,心愿这些填坑教训,对读者有借鉴意义。
第一,RN 官网提供的有限列表(ListView/FlatList)性能太差,始终被业内吐槽。通过实际比照,咱们抉择了内存管理效率更优的第三方组件——RecyclerListView。

第二,RecyclerListView 须要晓得每个列表项的高度,能力正确渲染。如果,列表项高度不确定,怎么解决?

第三,标签页和有限列表组合应用时,会遇到一些问题。首先,标签页中有多个有限列表,怎么无效治理内存?其次,标签页能够左右滑动,有限列表中也有左右滚动的内容组件,二者手势区域重叠时,如何指定组件优先解决?

列表的技术选型

  1. ListView

在实际开发分类信息流状态的产品过程中,咱们开始尝试过应用 RN,版本是 0.28。过后,有限列表用的是官网提供的 ListView。ListView 的列表项始终不会被销毁,这会导致内存一直减少,导致卡顿。前 100 条信息滚动十分晦涩,200 条时就开始卡顿,到 1000 条时就根本就滑不动了。过后,也没有特地好的解决方案,只能在产品上进行斗争,将有限列表降级为无限列表。

  1. FlatList
    FlatList 是在 RN 0.43 版本新增的,领有内存回收的性能,能够用来实现有限列表。咱们第一工夫就跟进了,把 RN 版本进行降级。尽管,FlatList 能够实现有限列表,但体验上总归还是有所欠缺的。FlatList 在 iOS 体现很晦涩,但在 Android 某些机型上会有略有卡顿。
  2. RecyclerListView
    在实际开发中,咱们技术选型还尝试采纳了 RecyclerListView。RecyclerListView 实现了内存的复用,性能也是更好。无论是 iOS 还是 Android 都体现的很晦涩。

晦涩度比照
掂量晦涩度的要害指标是帧率,帧率越高越晦涩,越低越卡顿。咱们用 RecyclerListView 和 FlatList 别离实现了雷同性能的有限列表,在 Android 手机中进行了测试,滚动帧率如下。

滚动帧率比照(以 Android OPPO R9 为例)

实现原理比照
ListView、FlatList、RecyclerListView 都是 RN 的列表组件,为什么它们之间性能差距这么大?咱们对其实现原理进行了一些钻研。

  1. ListView
    ListView 的实现思路比较简单,当用户上滑加载新的列表内容时,会一直地新增列表项。每次新增,都会导致内存减少,减少到肯定水平后,可应用的内存空间有余,页面就会呈现卡顿。
  2. FlatList
    FlatList 取了个巧,既然用户只能看到手机屏幕里的内容,那么只用将用户看到的(可视区域)和行将看到的(凑近可视区域)局部渲染进去就行了。而用户看不到的中央 (远离可视区域),就删掉,用空白元素占位就行。这样,空白区域的内存就失去了开释。
    要实现有限加载,必须要思考如何高效利用内存。FlatList“删除一个,新增一个”是一个思路。RecyclerListView“构造相似,改改再用”是另一个思路。
  3. RecyclerListView
    RecyclerListView 假如列表项的品种可枚举的。所有列表项能够分为若干类,比方,一张图片的图文布局是一类,两张图片的图文布局是一类,只有布局类似就是同一类列表项。开发者,须要对类型进行当时的申明。
    const types = {
    ONE_IMAGE: ‘ONE_IMAGE’, // 一张图片的图文布局
    TWO_IMAGE: ‘TWO_IMAGE’ // 两张图片的图文布局
    }

如果,用户行将看见的列表项,和用户看不见的列表项,类型一样。就把用户看不见的列表,批改成用户行将看到的列表项。批改不波及到组件的整体构造,只波及组件的属性参数,通常包含,文本、图片地址,还有展现的地位。
{/ 把用户看不见的列表项 /}
<View style={{position: ‘absolute’, top: disappeared}}>

<Text> 一行文本 </Text>
<Image source={{uri: '1.png'}}/>

<View>
{/ 批改成用户行将看见的列表项 /}
<View style={{position: ‘absolute’, top: visible}}>

<Text> 一行文本~~</Text>
<Image source={{uri: '2.png'}}/>

<View></View>

从三者原理上比照,咱们能够发现,在内存应用效率方面,内存复用的 RecyclerListView 比内存回收的 FlatList 更好,FlatList 又比内存不回收的 ListView 更好。

原理比照
手势上滑,页面向上滚动,加载更多列表项(深绿色)

RecyclerListView 的实际
RecyclerListView 复用列表项的地位是须要常常变动的,因而用的是相对定位 position: absolute 布局,而不是从上往下的 flex 布局。应用了相对定位,就须要晓得列表项的地位(top)。为了使用者的不便,RecyclerListView 让开发者传入所有列表项的高度(height),外部主动推断出其地位(top)。

  1. 高度确定的列表项
    在最简略例子中,所有列表项的高度都是已知的。只需将将高度、类型数据,和 Server 的数据进行合并,就能够失去 RecyclerListView 的状态(state)。
    const types = {
    ONE_IMAGE: ‘ONE_IMAGE’, // 一张图片的图文布局
    TWO_IMAGE: ‘TWO_IMAGE’ // 两张图片的图文布局
    }

// server data
const serverData = [

{img: ['xx'], text: '' },
{img: ['xx', 'xx'], text: '' },
{img: ['xx', 'xx'], text: '' },
{img: ['xx'], text: '' },

]

// RecyclerListView state
const list = serverData.map(item => {

switch (item.img.length) {
    case 1:
        // 高度确定,为 100px
        return {...item, height: 100, type: types.ONE_IMAGE,}
    case 2:
        return {...item, height: 100, type: types.TWO_IMAGE,}
    default:
        return null
}

})

  1. 高度不确定的列表项
    并不是所有列表项的高度,都是确定的。比方,上文下图的列表项,尽管图片高度是确定的,然而文本高度是由 Server 传过来的文本长度决定的。文字可能一行,可能两行,可能多行,文字有几行是不确定的,因而列表项的高度也不确定。那么,应该如何应用 RecyclerListView 组件呢?

2.1 Native 异步获取高度
Native 端,实际上有提前计算文本高度的 API —— fontMetrics。将 Native fontMetrics API 裸露给 JS,JS 不就具备了提前计算高度的能力了。此时,RecyclerListView 须要的 state 计算方法如下,其值是 promise 类型。
const list = serverData.map(async item => {

switch (item.img.length) {
    case 1:
        return {...item, height: await fontMetrics(item.text), type: types.ONE_IMAGE, }
    case 2:
        return {...item, height: await fontMetrics(item.text), type: types.TWO_IMAGE, }
    default:
        return null
}

})

每次调用 fontMetrics,都须要 oc/java 与 js 进行一次异步通信。而异步通信是十分耗时的,该计划会明显增加渲染耗时。此外,新增 fontMetrics 接口的计划,依赖 Native 发版,只能在新版本中应用,老版本用不了。因而,咱们没有采纳。
2.2 地位修改
开启 RecyclerListView 的 forceNonDeterministicRendering=true 属性后,会主动进行布局地位纠正。其原理是,开发者当时估算出列表项的高度,RecyclerListView 先按估算高度把视图渲染进去。当视图渲染进去后,通过 onLayout 获取列表项真正的高度,再通过动画将视图位移到正确的地位。

地位修改
该计划,在预计高度偏差小的场景下很实用,但在估算偏差大的场景下,会明察看到显著的重叠和位移的景象。那么,有没有一种估算偏差小,耗时又短的办法呢?
2.3 JS 估算高度
大部分状况下,列表项高度不确定都是由文本长度的不确定导致的。因而,只有能大抵估算文本的高度就行。
1 个 17px 字号 20px 行高的汉字,渲染进去的宽度为 17px,高度为 20px。如果,容器宽度足够宽,文字不折行,30 个的汉字,渲染进去的宽度为 30 17px = 510px,高度仍旧为 20px。如果,容器宽度只有 414px,那么显然会折成 2 行,此时文字高度为 2 20px =40px。其通用公式为:
行数 = Math.ceil(文字不折行宽度 / 容器宽度)
文字高度 = 行数 * 行高
实际上,字符类型不仅有汉字,还有小写字母、大写字母、数字、空格等,此外,渲染字号也各有不同。因而,最终的文本行数算法也更为简单。咱们通过多种真机测试,得出了 17px 下的各类字符类型的平局渲染宽度,比方大写字母 11px,小写字母 8.6px 等等,算法摘要如下:
/**

  • @param str 字符串文本
  • @param fontSize 字号
  • @returns 不折行宽度
    */
    function getStrWidth(str, fontSize) {
    const scale = fontSize / 17;
    const capitalWidth = 11 * scale; // 大写字母
    const lowerWidth = 8.6 * scale; // 小写字母
    const spaceWidth = 4 * scale; // 空格
    const numberWidth = 9.9 * scale; // 数字
    const chineseWidth = 17.3 * scale; // 中文和其余

    const width = Array.from(str).reduce(

      (sum, char) =>
          sum +
          getCharWidth(char, {
              capitalWidth,
              lowerWidth,
              spaceWidth,
              numberWidth,
              chineseWidth,
          }),
      0,

    );

    return Math.floor(width / fontSize) * fontSize;
    }

/**

  • @param string 字符串文本
  • @param fontSize 字体大小
  • @param width 渲染容器宽度
  • @returns 行数
    */
    function getNumberOfLines(string, fontSize, width) {
    return Math.ceil(getStrWidth(string, fontSize) / width);
    }
    上述纯 js 估算文字行数的算法,实测的准确率在 90% 左右,估算耗时为毫秒级别,可能很好的满足咱们需要。
    2.4 JS 估算高度 + 地位修改
    因而,咱们的最终计划为,通过 JS 估算出文本行数,并得出文本高度,再进一步地推断出列表项的布局高度。并开启 forceNonDeterministicRendering=true,在估算有偏差时,主动动画修改列表项的地位。

    标签页中的有限列表
    对于分类信息流状态的产品,有的会蕴含多样化标签,每个标签都有特定的内容,其中大部分标签页是有限列表。如果,所有标签页的内容都同时存在,内存得不开释,也会导致性能问题。
  1. 内存回收
    沿用下面的解决列表内存的思路,咱们能够抉择内存回收,或内存复用思路。内存复用的前提是,复用内容的构造雷同,只有数据有变动。理论业务中,产品曾经将类似内容进行了分类,每个标签页各有各的特点,很难复用。因而,对于标签页而言,内存回收是更好的抉择。
    整体思路是,可视区域内的标签页必定要显示进去。最近在可视区域的显示过的内容,依据状况进行保留。远离可视区的内容,须要销毁。

销毁远离可视区的标签页

  1. 手势重叠的解决
    标签页 TabView 1.0 应用的是 RN 自带的手势零碎,独自的左右滑动切换的标签页,自带的手势零碎运行良好。如果可视区中,既有能够左右滑动切换的标签页,又有能够左右滚动的内容区域。用户向左滚动手势重叠区域时,是标签页响应滚动,还是内容区域响应,还是同时响应呢?

手势重叠区域,向左滚动,谁响应?
因为 RN 的手势辨认,是同时在 oc/java 渲染主线程和 js 线程中同时进行的,这种奇怪的解决形式,使得手势很难失去精准的解决。这导致 TabView 1.0 不能很好的解决手势重叠的业务场景。
在 TabView 2.0 中,集成了新的手势零碎 React Native Gesture Handler。新的手势零碎,是申明式的,由纯 oc/java 渲染主线程解决的手势零碎。咱们能够在 JS 代码中,对手势的响应形式进行提前申明,让标签页期待(waitFor) 内容区域的手势响应。也就是说,重叠的区域手势,只作用于内容区域。

总结
本文介绍了咱们在应用 RN 开发分类信息流状态的产品的有限列表中,遇到的一些常见问题,以及如何进行技术考量、优化和抉择的。心愿能对大家有借鉴意义。

正文完
 0