React Native列表檢視FlatList使用優化實踐指南
列表檢視在app中是非常常見的,目前React Native比較嚴重的效能問題集中在FlatList大列表等地方,以下通過js層的優化,甚至原生層的優化封裝,使效能媲美原生。
FlatList
React Native 0.43版本推出FlatList替代ListView,FlatList實現繼承自VirtualizedList,底層的VirtualizedList提供更高的靈活性,但使用便捷性不如FlatList,如無特殊需求無法滿足直接使用FlatList。VirtualizedList實現繼承自ScrollView,所以FlatList繼承了VirtualizedList和ScrollView全部的props,在查閱相關文件時,如在FlatList中找不到相應的prop或者方法可以使用另外兩個元件的。React Native的FlatList與android listview、ios uitableview相似,將螢幕外的檢視元件回收,達到高效能的目的。
用法
以下例項程式碼均使用typescript
基本使用
<FlatList<number> // 資料陣列 data={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]} // key keyExtractor={(item, index) => index.toString()} // item渲染 renderItem={({item: num}) => ( <Text>{num}</Text> )} /> 複製程式碼
常用props
extraData
有除data以外的資料用在列表中,在此屬性中指定,否則介面很可能不會重新整理
horizontal
設定為 true 則變為水平佈局模式
inverted
翻轉滾動方向,多用於聊天列表之類反向展示資料
numColumns
指定一列顯示多少個item
常用方法
scrollToEnd
滑動到檢視底部
scrollToIndex
滑動到指定位置
scrollToOffset
滑動到指定畫素
上拉載入
<FlatList // 上拉回調 onEndReached={() => console.log('上拉載入')} // 滑動到最後檢視內容比例,設定為0-1,例如0.5則表示滑到最後一個檢視一半開始回撥 onEndReachedThreshold={0.1} /> 複製程式碼
下拉重新整理
<FlatList // true顯示重新整理元件 refreshing={this.state.refreshing} // 下拉回調 onRefresh=(async () => { this.setState({ refreshing: true }); await 耗時操作 this.setState({ refreshing: false }); }); /> 複製程式碼
滑動事件
onTouchStart
手指按下開始滑動,呼叫一次,用於監聽互動開始
onTouchMove
手指滑動,呼叫多次
onTouchEnd
手指鬆開,呼叫一次,開始慣性滾動,用於監聽互動結束
onMomentumScrollBegin
慣性滾動開始,呼叫一次,用於監聽滑動慣性動畫開始
onMomentumScrollEnd
慣性滾動結束,呼叫一次,用於監聽滑動慣性動畫結束
onScroll
滑動中,呼叫多次,用於監聽滑動位置
onScrollBeginDrag
開始滑動,呼叫一次,用於監聽滑動開始
onScrollEndDrag
滑動結束,呼叫一次,用於監聽滑動結束
分頁
用以開發簡單輪播檢視,分頁滑動檢視內容等
// 當前檢視索引 private index = 0; // 必須與this繫結,否則丟擲異常 private viewabilityConfig = {viewAreaCoveragePercentThreshold: 100}; handleViewableItemsChanged = (info: { viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => { // index為當前可見檢視在view的索引 this.index = info.changed[0].index!; } <FlatList // 每次滑動後一個item停留在整個檢視 pagingEnabled={true} // 可見檢視設定,1-100,50表示一半可見時回撥,100表示全部可見時回撥 viewabilityConfig={this.viewabilityConfig} // 可見檢視變更回撥 onViewableItemsChanged={this.handleViewableItemsChanged} // onViewableItemsChanged會多次回撥,監聽慣性滑動結束判斷分頁滑動結束,如需要實時判斷檢視索引顯示,則直接使用onViewableItemsChanged onMomentumScrollEnd={() => console.log('滑動至', this.index)} /> 複製程式碼
優化
removeClippedSubviews
移除在螢幕外元件,預設為true,對效能有最大的影響,不要修改為false
windowSize
保持檢視個數,即在螢幕外也不移除,預設值為11,在高耗效能元件中,可以適當設定小的值,在會快速滑動的檢視中,設定大的值如300,避免快速滑動後當前檢視還沒有渲染出現空白。
getItemLayout
獲取高度,如檢視高度固定,設定該屬性可以大大改善效能,避免了渲染過程中每一次都需要重新計算檢視高度。
getItemLayout={(data, index) => ({length: height, offset: height * index, index})}
key
合理設定key提高react對元件的複用,能很大的優化效能,在元件移出螢幕外,被回收後複用。
原生優化
在要求極高的列表檢視中,資料達上千甚至上萬,在部分情況FlatList已經無法滿足,特別是android裝置。以下介紹如何直接使用原生android RecyclerView檢視來完成高要求的列表檢視。
原生檢視程式碼
public class MyFlatListManager extends SimpleViewManager<MyFlatListManager.MyRecyclerView> { // 自定義RecyclerView public static class MyRecyclerView extends RecyclerView { // 資料列表 public List<Data> list = new ArrayList<>(); // 介面卡 public MyAdapter myAdapter; // 佈局管理器 public LinearLayoutManager mLayoutManager; public MyRecyclerView(Context context) { super(context); myAdapter = new MyAdapter(this, list); // 設定為垂直方向 mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false); setLayoutManager(mLayoutManager); // 固定高度避免重新測量,提高效能 setHasFixedSize(true); // 禁止資料變更時動畫,避免閃爍 setItemAnimator(null); setAdapter(myAdapter); } @Override public void requestLayout() { super.requestLayout(); // react native android根檢視requestLayout為空函式,避免加入新檢視無法顯示或者高度寬度不正確,手動執行測量 post(measureAndLayout); } public final Runnable measureAndLayout = new Runnable() { @Override public void run() { measure( MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)); Log.d(TAG, "measureAndLayout"); layout(getLeft(), getTop(), getRight(), getBottom()); } }; } private static class MyViewHolder extends RecyclerView.ViewHolder { public MyViewHolder(View itemView) { super(itemView); } } private static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> { private List<MyViewHolder> holders; private List<Data> list; private MyRecyclerView recyclerView; public MyAdapter(MyRecyclerView recyclerView, List<VideoInfo> list) { this.list = list; this.holders = new ArrayList<>(); this.recyclerView = recyclerView; } // 檢視建立 @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.movie_list_row, parent, false); // 手動重新設定高度,match parent itemView.getLayoutParams().height = parent.getHeight(); itemView.getLayoutParams().width = parent.getWidth(); return new MyViewHolder(itemView); } @Override public void onBindViewHolder(final MyViewHolder holder, int position) { Data data = list.get(position); //Log.i(TAG, "setTag " + position); holder.itemView.setTag(position); // 繫結檢視資料 } @Override public int getItemCount() { return list.size(); } } private static final String TAG = "MyFlatListViewManager"; @Override public String getName() { return "MyFlatListViewManager"; } @Override protected MyRecyclerView createViewInstance(final ThemedReactContext reactContext) { return new MyRecyclerView(reactContext); } @Nullable @Override public Map<String, Integer> getCommandsMap() { Map<String, Integer> commandsMap = new HashMap<>(); commandsMap.put("addData", 1); return commandsMap; } @Override public void receiveCommand(MyRecyclerView root, int commandId, @Nullable ReadableArray args) { MyAdapter myAdapter = (MyAdapter) root.getAdapter(); switch (commandId) { case 1: if (args == null) return; Log.i(TAG, "addData size: " + args.size()); Integer position = root.list.size(); for (int i = 0; i < args.size(); i++) { // 初始化值,getData為從map中獲取data的函式,自行根據結構實現 Data data = getData(args.getMap(i)); Log.i(TAG, "add data " + data); root.list.add(data); } Log.i(TAG, "addDatas old position " + position + " size " + args.size()); // 通知變更 myAdapter.notifyItemRangeInserted(position, args.size()); break; } } } 複製程式碼
需要注意的有幾個地方
- setHasFixedSize 如果檢視高度固定,設定固定高度能提高效能
- setItemAnimator 動畫可能會導致在載入圖片等的時候閃爍
- requestLayout 必須重新手動觸發測量檢視,在android中這部分機制被react native遮蔽
- onCreateViewHolder 必須手動設定itemView高度和寬度
react反模式
在原生元件和js層進行props傳遞,如資料量太大,使用props直接傳遞已經不合適,資料可能已經達到幾m甚至更大。react的props模式已經不再適合這樣的場景,在web中也是,大量的資料每一次單個數據的變更都全部重新傳遞,會導致嚴重的效能問題。在這種情況下,使用元件ref呼叫函式來一個一個新增或者一個一個移除相關陣列這些大的物件,會很好的提升效能。在android的程式碼中,不再使用prop傳遞FlatList的data,而是使用add的方法來新增,然後在js層再進行一層的原生元件封裝,讓使用與其他元件一致。