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

列表視圖在app中是很是常見的,目前React Native比較嚴重的性能問題集中在FlatList大列表等地方,如下經過js層的優化,甚至原生層的優化封裝,使性能媲美原生。java

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類似,將屏幕外的視圖組件回收,達到高性能的目的。react

用法

如下實例代碼均使用typescriptandroid

基本使用

<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之外的數據用在列表中,在此屬性中指定,不然界面極可能不會刷新ios

horizontal

設置爲 true 則變爲水平佈局模式web

inverted

翻轉滾動方向,多用於聊天列表之類反向展現數據typescript

numColumns

指定一列顯示多少個item數組

經常使用方法

scrollToEnd

滑動到視圖底部bash

scrollToIndex

滑動到指定位置app

scrollToOffset

滑動到指定像素async

上拉加載

<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層再進行一層的原生組件封裝,讓使用與其餘組件一致。

相關文章
相關標籤/搜索