RecyclerListView使用说明及与FlatList性能对比

前言

在【58部落】的业务场景下,存在较多的列表页面。整个产品的“门面”——入口页面,常驻在58APP下方的“发现”tab,因此要求有较高的用户体验。做为一个初中期的社区产品,不少功能还不够完善和稳定,所以要求能较快的功能迭代。兼具体验和快速迭代的要求,在58APP中,咱们的选择是以 React Native 来进行页面的开发。javascript

门面页面

图1 - 门面页面(咱们称为部落一级页,此处为广告 :-) )java

该页面是由多个Tab组成,每一个tab基本上都是无限下拉的列表。在 React Native 中,能够用做列表的组件,常见的有:react

  • ListView
  • SectionList

固然还有官方支持的高性能的简单列表组件:git

  • FlatList

但即便是 React Native 官方支持的性能最好FlatList组件,在Android的一些机型上的表现也差强人意,特别是使用超过两年的Android手机,基本上就是到很是卡的状态了。github

因此,今天介绍下在Android上表现更好的、性能更优的 React Native 列表组件:redux

  • RecyclerListView

RecyclerListView 是什么

RecyclerListView 是一个高性能的列表(listview)组件,同时支持 React Native 和 Web ,而且可用于复杂的列表。RecyclerListView 组件的实现灵感,来自于 Android RecyclerView原生组件及iOS UICollectionView原生组件。react-native

为何须要RecyclerListView

咱们知道,React Native 的其余列表组件如ListView,会一次性建立全部的列表单元格——cell。若是列表数据比较多,则会建立不少的视图对象,而视图对象是很是消耗内存的。因此,ListView组件,对于咱们业务中的这种无限列表,基本上是不能够用的。缓存

对于React Native 官方提供的高性能的列表组件FlatList, 前文提到,在Android设备上的表现,并非十分友好。它的实现原理,是将列表中不在可视区域内的视图,进行回收,而后根据页面的滚动,不断的渲染出如今可视区域内的视图。这里须要注意的是,FlatList是将不可见的视图回收,从内存中清除了,下次须要的时候,再从新建立。这就要求设备在滚动的时候,能快速的建立出须要的视图,才能让列表流畅的展示在用户面前。而问题也就出如今这里,Android设备由于老化等缘由,计算力等跟不上,加之React Native 自己 JS 层与 Native 层之间交互的一些问题(这里不作深刻追究),致使建立视图的速度达不到使列表流畅滚动的要求。ide

那怎样来解决这样的问题呢?布局

RecyclerListView 受到 Android RecyclerView 和 iOS UICollectionView 的启发,进行两方面的优化:

  • 仅建立可见区域的视图,这步与FlatList是一致的。
  • cell recycling,重用单元格,这个作法是FlatList缺少的。

对于程序来讲,视图对象的建立是很是昂贵的,而且伴随着内存的消耗。意味着若是不断的建立视图,在列表滚动的过程当中,内存占用量会不断增长。FlatList中将不可见的视图从内存中移除,这是一个比较好的优化手段,但同时也会致使大量的视图从新建立以及垃圾回收。

RecyclerListView 经过对不可见视图对象进行缓存及重复利用,一方面不会建立大量的视图对象,另外一方面也不须要频繁的建立视图对象和垃圾回收。

基于这样的理论,因此RecyclerListView的性能是会优于FlatList的,实际结果会从下面的实践中得知。

RecyclerListView怎么使用

RecyclerListView 的使用比较简单,相对于 FlatList 经过getItemLayout来优化布局须要提供offset——相对于FlatList组件对顶部的一个偏移值来讲,RecyclerListView 只须要知道对应cell的高度值便可。对于复杂列表来讲,RecyclerListView 的这种方式,大大优于FlatList使用方式。

一个 RecyclerListView 组件必要的 props 有 :

  • dataProvider
  • layoutProvider
  • rowRenderer

查看完整的props

一个最简单的示例
/***
 Use this component inside your React Native Application.
 A scrollable list with different item type
 */
import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";

const ViewTypes = {
    FULL: 0,
    HALF_LEFT: 1,
    HALF_RIGHT: 2
};

let containerCount = 0;

class CellContainer extends React.Component {
    constructor(args) {
        super(args);
        this._containerId = containerCount++;
    }
    render() {
        return (
            <View {...this.props}>
                {this.props.children}
                <Text>Cell Id: {this._containerId}</Text>
            </View>
        );
    }
}

/***
 * To test out just copy this component and render in you root component
 */
export default class RecycleTestComponent extends React.Component {
    constructor(args) {
        super(args);

        let { width } = Dimensions.get("window");

        //Create the data provider and provide method which takes in two rows of data and return if those two are different or not.
        //THIS IS VERY IMPORTANT, FORGET PERFORMANCE IF THIS IS MESSED UP
        let dataProvider = new DataProvider((r1, r2) => {
            return r1 !== r2;
        });

        //Create the layout provider
        //First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid
        //Second: Given a type and object set the exact height and width for that type on given object, if you're using non deterministic rendering provide close estimates
        //If you need data based check you can access your data provider here
        //You'll need data in most cases, we don't provide it by default to enable things like data virtualization in the future
        //NOTE: For complex lists LayoutProvider will also be complex it would then make sense to move it to a different file
        this._layoutProvider = new LayoutProvider(
            index => {
                if (index % 3 === 0) {
                    return ViewTypes.FULL;
                } else if (index % 3 === 1) {
                    return ViewTypes.HALF_LEFT;
                } else {
                    return ViewTypes.HALF_RIGHT;
                }
            },
            (type, dim) => {
                switch (type) {
                    case ViewTypes.HALF_LEFT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.HALF_RIGHT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.FULL:
                        dim.width = width;
                        dim.height = 140;
                        break;
                    default:
                        dim.width = 0;
                        dim.height = 0;
                }
            }
        );

        this._rowRenderer = this._rowRenderer.bind(this);

        //Since component should always render once data has changed, make data provider part of the state
        this.state = {
            dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
        };
    }

    _generateArray(n) {
        let arr = new Array(n);
        for (let i = 0; i < n; i++) {
            arr[i] = i;
        }
        return arr;
    }

    //Given type and data return the view component
    _rowRenderer(type, data) {
        //You can return any view here, CellContainer has no special significance
        switch (type) {
            case ViewTypes.HALF_LEFT:
                return (
                    <CellContainer style={styles.containerGridLeft}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            case ViewTypes.HALF_RIGHT:
                return (
                    <CellContainer style={styles.containerGridRight}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            case ViewTypes.FULL:
                return (
                    <CellContainer style={styles.container}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            default:
                return null;
        }
    }

    render() {
        return (
            <RecyclerListView 
                layoutProvider={this._layoutProvider} 
                dataProvider={this.state.dataProvider} 
                rowRenderer={this._rowRenderer} 
            />
        )
    }
}
const styles = {
    container: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#00a1f1"
    },
    containerGridLeft: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#ffbb00"
    },
    containerGridRight: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#7cbb00"
    }
};

为了进行cell-recycling,RecyclerListView要求对每一个cell(一般也叫Item)定义一个type,根据type设置celldim.widthdim.height

this._layoutProvider = new LayoutProvider(
            index => {
                if (index % 3 === 0) {
                    return ViewTypes.FULL;
                } 
                ...
            },
            (type, dim) => {
                switch (type) {
                    case ViewTypes.HALF_LEFT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    ...
                }
            }
        );

rowRenderer负责渲染一个cell,一样是根据type来进行渲染:

_rowRenderer(type, data) {
    switch (type) {
        case ViewTypes.HALF_LEFT:
            return (
                <CellContainer style={styles.containerGridLeft}>
                    <Text>Data: {data}</Text>
                </CellContainer>
            );
        ...
      }
}

固然在咱们的实际业务场景中不可能这么简单,页面滚动须要进行一些处理啊,滚动到最底部须要加载下一页等等都是最多见的业务场景,RecyclerListView这些也都支持得比较好,如下是一些常见的 props:

  • onScroll: 列表滚动时触发;
  • onEndReached: 列表触底时触发;
  • onEndReachedThreshold: 列表距离底部多大距离时触发,这里是具体到底部的像素值,与FlatList几屏的数值是有区别的;
  • onVisibleIndexesChanged: 可见元素,滚动时实时触发;
  • renderFooter: 渲染列表footer。
实际业务怎么处理

在咱们的业务场景中,在列表中包含5类cell:

  • 普通帖子
  • 置顶banner
  • 推荐部落
  • 推荐话题
  • 通知公告

后期应该还会增长其余的类型。后4类基本从dim.height上来说,是不会根据内容变化的,因此还比较简单,定义固定的type便可。

对于“普通帖子”这个类型来说,就相对来讲比较复杂了,示例其中一种状况以下图:

cell

图2 - 普通帖子的常见样式

其中有两部分是固定有的:

  • header:发帖者信息等
  • footer: 帖子回复,点赞等数据

其余部分就是根据帖子内容,有,无或者几种形态变化了,如帖子内容可展现为一行或者两行,帖子中的图片分为一图、二图、三图模式等等。

因此这里就出现了一个上述demo中无法解决的问题,“普通帖子”这种类型,咱们单单定义一个type,不进行其余处理,会存在一些问题。解决这个问题,在咱们的业务中,测试了两种方式:

  • 1.仅定义为一个type,记为RecyclerListView#1
    经过其内容,计算出每一个cell的高度,并存储到原始数据中,在layoutProvider中获取。

    this._layoutProvider = new LayoutProvider(
        index => {
            ...
        },
        (type, dim, index) => {
        // 注意这里的第三个参数
        // 好比原始数据存在 this.data 中
        if(type==='card'){
            dim.height = this.data[index].height ;
        } 
        ...
    })
  • 2.将“普通帖子”,拆分红多个组成部分,记为RecyclerListView#2

    // 如一条帖子的数据是这样的
    const data = {
        title:'标题',
        context:'内容',
        pics:['https://pic1.58cdn.com.cn/1.png'] ,// 图片
        user:{} ,// 用户信息
        replynum:300 // 回复信息
        hotAnswers:[]
        ...
    }

    根据展现规则,把用户信息等拆成一条,做为header这种type,把title拆成一条,做为title这种type,一个图片拆成一种type,两个图片的又拆成另外一种type......,这样,每一个type就基本上比较单纯,type的高度值也基本能固定了。

从理论上来说,第二种方式心梗应该是会优于第一种方式(具体回顾RecyclerListView的实现方式及原理)。

性能对比

如下是用OPPO R9测试的帧率结果:

FlatList

图3 - FlatList 滚动帧率

RecyclerListView#1

图4 - RecyclerListView#1 滚动帧率

RecyclerListView#2

图5 - RecyclerListView#2 滚动帧率

mixin

图6 - 帧率对比

经过帧率对比能够看出,RecyclerListView的滚动帧率是远大于FlatList的。FlatList在滚动时帧率波动比较严重,上手体验会发现比较卡顿且较多白屏现象。相对来讲,RecyclerListView 的帧率变化相对稳定,基本都能维持到 35fps 以上,平均值在46fps 左右。

RecyclerListView#1 和 RecyclerListView#2, 总体帧率差距不是很明显,在该机型上得不出很正确的结论,就目前的状况来看,这种结果却是咱们做为开发者但愿看到的结果。由于相对应的,对数据进行拆分不只为增长数据量,而且从开发体验上来讲,也会增长较大成本,开发体验并很差。

RecyclerListView#1 和 RecyclerListView#2 的比对,还须要更多的设备去验证。

5. 开发建议和场景限制

  • 列表能简单,尽可能简单
  • 数据项能不拆,尽可能不拆;拆是个大坑
  • 由于cell recycling, 因此 cell内部不能保留状态,若是须要数据变化,必定要在外部进行存储,如用redux等
  • 列表项(cell)删除会存在必定问题,特别是对于数据须要进行拆分的列表

其余开发建议参见 RecyclerListView Performance: https://github.com/Flipkart/r...

相关文章
相关标签/搜索