效能更好的 React Native 無限列表
在【58部落】的業務場景下,存在較多的列表頁面。整個產品的“門面”——入口頁面,常駐在58APP下方的“發現”tab,所以要求有較高的使用者體驗。作為一個初中期的社群產品,很多功能還不夠完善和穩定,因此要求能較快的功能迭代。兼具體驗和快速迭代的要求,在58APP中,我們的選擇是以 React Native 來進行頁面的開發。
圖1 - 門面頁面(我們稱為部落一級頁,此處為廣告 :-) )
該頁面是由多個Tab組成,每個tab基本上都是無限下拉的列表。在 React Native 中,可以用作列表的元件,常見的有:
-
SectionList
當然還有官方支援的高效能的簡單列表元件:
-
FlatList
但即使是 React Native 官方支援的效能最好 FlatList
元件,在Android的一些機型上的表現也差強人意,特別是使用超過兩年的Android手機,基本上就是到非常卡的狀態了。
所以,今天介紹下在Android上表現更好的、效能更優的 React Native 列表元件:
-
RecyclerListView
。
RecyclerListView 是什麼
RecyclerListView 是一個高效能的列表(listview)元件,同時支援 React Native 和 Web ,並且可用於複雜的列表。RecyclerListView 元件的實現靈感,來自於 Android RecyclerView
原生元件及iOS UICollectionView
原生元件。
為什麼需要RecyclerListView
我們知道,React Native 的其他列表元件如 ListView
,會一次性建立所有的列表單元格—— cell
。如果列表資料比較多,則會建立很多的檢視物件,而檢視物件是非常消耗記憶體的。所以, ListView
元件,對於我們業務中的這種無限列表,基本上是不可以用的。
對於React Native 官方提供的高效能的列表元件 FlatList
, 前文提到,在Android裝置上的表現,並不是十分友好。它的實現原理,是將列表中不在可視區域內的檢視,進行回收,然後根據頁面的滾動,不斷的渲染出現在可視區域內的檢視。這裡需要注意的是, FlatList
是將不可見的檢視回收,從記憶體中清除了,下次需要的時候,再重新建立。 這就要求裝置在滾動的時候,能快速的創建出需要的檢視,才能讓列表流暢的展現在使用者面前 。而問題也就出現在這裡,Android裝置因為老化等原因,計算力等跟不上,加之React Native 本身 JS 層與 Native 層之間互動的一些問題(這裡不做深入追究),導致建立檢視的速度達不到使列表流暢滾動的要求。
那怎樣來解決這樣的問題呢?
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
一個最簡單的示例(點選底部閱讀原文)
為了進行 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:
-
onScroll: 列表滾動時觸發;
-
onEndReached: 列表觸底時觸發;
-
onEndReachedThreshold: 列表距離底部多大距離時觸發, 這裡是具體到底部的畫素值,與FlatList幾屏的數值是有區別的 ;
-
onVisibleIndexesChanged: 可見元素,滾動時實時觸發;
-
renderFooter: 渲染列表footer。
實際業務怎麼處理
在我們的業務場景中,在列表中包含5類 cell
:
-
普通帖子
-
置頂banner
-
推薦部落
-
推薦話題
-
通知公告
後期應該還會增加其他的型別。後4類基本從 dim.height
上來講,是不會根據內容變化的,所以還比較簡單,定義固定的 type
即可。
對於“普通帖子”這個型別來講,就相對來說比較複雜了,示例其中一種情況如下圖:
圖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測試的幀率結果:

圖3 - FlatList 滾動幀率圖4 - RecyclerListView#1 滾動幀率

圖5 - RecyclerListView#2 滾動幀率
圖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/recyclerlistview/tree/master/docs/guides/performance