在【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 是一個高性能的列表(listview)組件,同時支持 React Native 和 Web ,而且可用於複雜的列表。RecyclerListView 組件的實現靈感,來自於 Android RecyclerView
原生組件及iOS UICollectionView
原生組件。react-native
咱們知道,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 的使用比較簡單,相對於 FlatList 經過getItemLayout
來優化佈局須要提供offset
——相對於FlatList組件對頂部的一個偏移值來講,RecyclerListView 只須要知道對應cell
的高度值便可。對於複雜列表來講,RecyclerListView 的這種方式,大大優於FlatList使用方式。
一個 RecyclerListView 組件必要的 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
設置cell
的dim.width
和dim.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:
在咱們的業務場景中,在列表中包含5類cell
:
後期應該還會增長其餘的類型。後4類基本從dim.height
上來說,是不會根據內容變化的,因此還比較簡單,定義固定的type
便可。
對於「普通帖子」這個類型來說,就相對來講比較複雜了,示例其中一種狀況以下圖:
圖2 - 普通帖子的常見樣式
其中有兩部分是固定有的:
其餘部分就是根據帖子內容,有,無或者幾種形態變化了,如帖子內容可展現爲一行或者兩行,帖子中的圖片分爲一圖、二圖、三圖模式等等。
因此這裏就出現了一個上述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測試的幀率結果:
圖3 - FlatList 滾動幀率
圖4 - RecyclerListView#1 滾動幀率
圖5 - RecyclerListView#2 滾動幀率
圖6 - 幀率對比
經過幀率對比能夠看出,RecyclerListView的滾動幀率是遠大於FlatList的。FlatList在滾動時幀率波動比較嚴重,上手體驗會發現比較卡頓且較多白屏現象。相對來講,RecyclerListView 的幀率變化相對穩定,基本都能維持到 35fps 以上,平均值在46fps 左右。
RecyclerListView#1 和 RecyclerListView#2, 總體幀率差距不是很明顯,在該機型上得不出很正確的結論,就目前的狀況來看,這種結果卻是咱們做爲開發者但願看到的結果。由於相對應的,對數據進行拆分不只爲增長數據量,而且從開發體驗上來講,也會增長較大成本,開發體驗並很差。
RecyclerListView#1 和 RecyclerListView#2 的比對,還須要更多的設備去驗證。
cell recycling
, 因此 cell
內部不能保留狀態,若是須要數據變化,必定要在外部進行存儲,如用redux等cell
)刪除會存在必定問題,特別是對於數據須要進行拆分的列表其餘開發建議參見 RecyclerListView Performance: https://github.com/Flipkart/r...