React Native列表檢視FlatList使用優化實踐指南

qiushijie發表於2019-04-02

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

相關文章