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...

相關文章
相關標籤/搜索