列表檢視在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層再進行一層的原生元件封裝,讓使用與其他元件一致。