国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

React Native 中 ScrollView 性能探究

2019-11-09 15:56:32
字體:
來源:轉載
供稿:網友

1 基本使用

ScrollView 是 React Native(后面簡稱:RN) 中最常見的組件之一。理解 ScrollView 的原理,有利于寫出高性能的 RN 應用。

ScrollView 的基本使用也非常簡單,如下:

<ScrollView>    <Child1 />  <Child2 />  ...</ScrollView>  

它和 View 組件一樣,可以包含一個或者多個子組件。對子組件的布局可以是垂直或者水平的,通過屬性 horizontal=true/false 來控制。甚至還默認支持“下拉”刷新操作。另外還有一個特別贊的特性,超出屏幕的 View 會自動被移除,從而節省資源和提高繪制效率。我們來看如下一個例子:

class ScrollViewTest extends Component {  render() {    let children = [];    for (var i = 0; i < 20; i++) {      children.push(        <View key={"key_" + i} style={styles.child}>          <Text>{"T" + i}</Text>        </View>);    }    return (        <ScrollView style={styles.scrollView}>          {children}        </ScrollView>    );  }}

在 Android 上的效果如下:ScrollView-Test-Android如圖,我們在 ScrollView 中添加了 20 個子組件,但是我們的屏幕任意時刻最多只能顯示 5 個子項目。

下面我們來看實際對應的 Native 控件的情況。RN 中的 ScrollView 對應到 Native 的 RCTScrollView,自動把子組件包含在一個 ViewGroup 中(因為Android 的 ScrollView 只能有一個直接子控件),如下圖中的紅色框內:

ScrollView-Test-Android-Tree-View

注意到,我們在 JS 中添加了 20 個子組件,但是在 RCTViewGroup 中只有在屏幕上顯示的 5 個子控件,在屏幕外的組件,也會自動添加到 View 樹中,這與 Native 的 ScrollView 表現一致。

其實,RN 中的 ScrollView 有一個 removeClippedSubviews 屬性,表示如果子 View 超出可視區域,是否自動移除,雖然默認是 true。但是也需要子 View 的 overflow: 'hidden'屬性配合。所以,給子組件的 style 添加如下屬性即可。

<View key={"key_" + i} style={styles.child}>    <Text>{"T" + i}</Text></View>;const styles = StyleSheet.create({    child: {    ...    overflow: 'hidden',  },});

得到的效果是,在使用上完全沒有區別,而我們來看一下界面的 Tree View,如下圖:

ScrollView-Test-Android-Tree-View-Clipped可見,屏幕外的子 View,就被自動從 View 樹中移除了。

同時,我們來看一下 iOS 平臺上的表現,與 Android 上類似:

ScrollView-Test-iOS

這印證了我們前面的結論,RN 自動優化了 Native 平臺 ScrollView,在這個層面,我們可以說 RN 比 Native 的性能還要高。

2 性能研究

通過上面的實例,我們可以看到,ScrollView 應該是非常高效的,它使用簡單,并且還能按需構建 View 樹,高效渲染,有點類似 Native 平臺上的 ListView 了,是我心目完美 ScrollView 該有的樣子。

但是,之前看到騰訊的 TAT.ronnie 一篇文章 探索 react native 首屏渲染最佳實踐,文中提到的優化方法,主要就是針對 ScrollView 的。作者認為,在 ScrollView 中,即使不可見(例如,超出屏幕)的組件還是會繪制的。為了優化 ScrollView 的繪制性能,不可見的組件,應該在 JS 中避免添加到 ScrollView 中。

顯然,這與我們前面觀察到的結論是矛盾的。但是,作者的通過那樣處理,確實優化了顯示性能,這是怎么回事呢?為了驗證,我們也和文中一樣,使用 componentDidMount() 和 componentWillMount() 的時間差衡量顯示速度。在 Android 上,測試 ScrollView 的子組件數量分別為 10,100,1000 的時候,顯示的時間,以及 APP 所占用的內存:

子組件數量加載時間(ms)占用內存(MB)繪制時間*(ms)
1030919.714.666
100117021.915.016
1000946126.515.025

* 注,這里的繪制時間,是在 Tree View 中獲得的 Draw 時間。

從加載時間看,時間隨著子組件的數量線性增加,占用內存也有類似趨勢,說明 TAT.ronnie 的改進方法確實是有效的。另外我們也注意到,隨著子組件的數量增加,Draw 的時間并沒有明顯的變化,其實 Measure 和 Layout 時間也沒有明顯的變化。

說明 ScrollView 雖然有 removeClippedSubviews 屬性,也確實在 View Hierarchy 中去掉了不可見的 View。但是組件的加載時間消耗資源還是隨著子組件的數量成正比。

3 原因分析

來看一下 RN 中 ScrollView 的相關的源碼,主要分析 Android 平臺的代碼,iOS 類似,就不贅述了。

// ScrollView.jsvar AndroidScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyPRops);  var AndroidHorizontalScrollView = requireNativeComponent(    'AndroidHorizontalScrollView',  ScrollView,  nativeOnlyProps);var ScrollView = React.createClass({    render: function() {    var contentContainer =       <View         ...         removeClippedSubviews={this.props.removeClippedSubviews}         collapsable={false}>         {this.props.children}       </View>;     var ScrollViewClass;     if (Platform.OS === 'ios') {       ...     } else if (Platform.OS === 'android') {       if (this.props.horizontal) {         ScrollViewClass = AndroidHorizontalScrollView;       } else {         ScrollViewClass = AndroidScrollView;       }     }     // 為了簡單,忽略有下拉刷新的情況     return (      <ScrollViewClass ...>        {contentContainer}      </ScrollViewClass>    );  }});

JS 部分的代碼邏輯很簡單。首先把 ScrollView 所有子組件包裝在一個 View contentContainer 中,并繼承設置了 removeClippedSubviews 屬性。根據 ScrollView 是否是水平方向,決定是用 RCTScrollView 或者 AndroidHorizontalScrollView Native 組件來包含 contentContainer。

所以,我們先來看 RCTScrollView 本地組件對應的代碼(AndroidHorizontalScrollView 原理也類似)。JS 中的 RCTScrollView 組件由 com.facebook.react.views.scroll.ReactScrollViewManager 提供,具體的 View 的實現是 com.facebook.react.views.scroll.ReactScrollView

其中 ReactScrollViewManager 是最基礎的 ViewManager 的實現,導出了一些屬性和事件。ReactScrollView 則繼承于 android.widget.ScrollView,并實現了 ReactClippingViewGroup 接口。關于 Scroll 事件相關的代碼我們先忽略,我主要關心 View 繪制相關的代碼。主要在下面這段代碼:

@Overridepublic void updateClippingRect() {    if (!mRemoveClippedSubviews) {    return;  }  ...  View contentView = getChildAt(0);  if (contentView instanceof ReactClippingViewGroup) {    ((ReactClippingViewGroup) contentView).updateClippingRect();  }}

可見,如果不開啟 mRemoveClippedSubviews,它就和普通的 ScrollView 一樣,否者,它就會調用了它的第一個(也是唯一的一個)子 View 的 updateClippingRect() 方法。從上面的 JS 中我們可以看到,它的第一個子元素應該就是一個 View 組件,對應的 Native 的控件就是 ReactViewGroup。 ReactViewGroup 是 RN for Android 中最基礎的控件,它直接繼承于 android.view.ViewGroup

public class ReactViewGroup extends ViewGroup implements      ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView {  private boolean mRemoveClippedSubviews = false;  // 用來保存所有子 View 的數組,包括可見和不可見的  private @Nullable View[] mAllChildren = null;  private int mAllChildrenCount;  // 當前 ReactViewGroup 于父 View 相交矩陣,  // 也就是它自己在父 View 中可見區域  private @Nullable Rect mClippingRect;  ...}

在 ReactViewGroup 中實現 removeClippedSubviews 的功能也非常直接,需要更新界面 Layout 的時候,遍歷所有的子 View,看子 View 是否在 mClippingRect 區域內,如果在,就通過 addViewInLayout() 方法添加此 View,否者就通過 removeViewsInLayout() 方法移除它。

到這了,我們就可以解釋前面的矛盾了。雖然在 ScrollView 的 View Hierarchy 中,會自動移除不顯示的 View,但是實際上還是創建了所有的子 View,所以所占內存和加載時間會線性增加。

關于創建所有子 View,我這里可以多分析一下。我們知道在 Android 中,創建 View 的代價是很大的。特別是在 ScrollView 中,所有的子 View 都是同時創建的。如果 ScrollView 中子 View 的數量很多,這樣的代價累加起來,對 APP 造成的延遲和卡頓是相當可觀的。例如前面的測試中有 1000 個子組件,加載時間竟然長達 9.5 秒。我們用Method Tracing 看一下創建一個子 View 所花的時間,如下圖:

ScrollView-Demo-Method-Tracing這里只是簡單的創建一個 TextView 就消耗了大約 25ms 的時間。當然 Tracing 過程本身會拖慢 APP 運行,但是不影響我們的結論。所以 Android 中列表類的控件,都內部支持對 View 的復用,盡量避免創建 View。

通過前面的分析,我們可以得到的結論是:RN 中的 ScrollView 并不像我們想象的那樣高性能。

4 ListView

在這里提到 ListView,是因為 RN 中的 ListView 就是基于 ScrollView 的,但是有一些優化。這里簡要介紹一些 ListView 的原理。

ListView 其實是對 ScrollView 的一個封裝,對應到 Native 平臺,和 ScrollView 的表現一模一樣。但是 ListView 在顯示列表內容的時候,會根據滑動距離,逐步向 ScrollView 中添加子組件(通過調用 renderRow() 方法)。注意到 ListView 有 initialListSize 屬性,表示第一次加載的時候添加多少個子項,默認是 10,還有 pageSize 屬性,表示每次需要添加的時候,增加多少個子項,默認是 1。

通過上面的分析我們可以看到,ListView 在第一次加載的時候,不論你的列表有多大,默認最多加載 initialListSize 個子項,所以能保證啟動速度,如果還沒有充滿,或者在向下滑動過程中,再組件添加子項。這樣的操作似乎比較合理,但是注意到,整個操作中,會逐漸向 ListView 中添加子項,新出現的子項,都是通過創建新的 View,而完全沒有復用的過程。所以,如果在應用中,ListView 中的子項數量特別多,ListView 往下滑動的過程中,內存會逐漸上漲的。

值得一提的是,ListView 提供了 renderScrollComponent,可以使用其他 Scroll 組件來替換 ScrollView,并且 RecyclerViewBackedScrollView 組件來作為備選。看到這個名字我很欣喜,說明它支持子項的回收復用(Recycler)。首先,看到 iOS 的實現 RecyclerViewBackedScrollView.ios.js,其實它就是 ScrollView,并沒有實現所謂的復用,失望了一半。繼續看 Android 的實現,它實際上是對應 Native 的 com.facebook.react.views.recyclerview.AndroidRecyclerViewBackedScrollView,它繼承與 Android 的 RecyclerView。看到這里,如果使用這種方法,我直觀感覺 RN 的 ListView 性能在 Android 上表現應該會比 iOS 好。

我們繼續來看它是怎么實現回收復用的,AndroidRecyclerViewBackedScrollView 內部實現了一個 RecyclerView.Adapter,如下:

static class ReactListAdapter extends Adapter<ConcreteViewHolder> {  private final List<View> mViews = new ArrayList<>();  public void addView(View child, int index) {    mViews.add(index, child);    ...  }  public void removeViewAt(int index) {    View child = mViews.get(index);    if (child != null) {      mViews.remove(index);      ...    }  }  @Override  public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {    return new ConcreteViewHolder(new RecyclableWrapperViewGroup(parent.getContext()));  }  @Override  public void onBindViewHolder(ConcreteViewHolder holder, int position) {    RecyclableWrapperViewGroup vg = (RecyclableWrapperViewGroup) holder.itemView;    View row = mViews.get(position);    if (row.getParent() != vg) {      vg.addView(row, 0);    }  }  @Override  public void onViewRecycled(ConcreteViewHolder holder) {    super.onViewRecycled(holder);    ((RecyclableWrapperViewGroup) holder.itemView).removeAllViews();  }  }

注意到這里有一個 mViews,用來保存所有的子 View,綁定 View 的時候只是簡單用一個空的 View(RecyclableWrapperViewGroup)包了一下。這樣一來,RecyclerView 完全沒有什么起到復用的作用呀!測試一下,確實也是這樣,性能問題還是很嚴重。

這里我們也可以得到一個結論:RN 中的 ListView 也不是我們想象的 ListView 該有的性能。

5 改進方案

通過前面的分析,我們已經知道了 RN 中的 ScrollView 或者 ListView 的性能瓶頸了,同時也有了改進的思路。下面針對各種情況分析:

如果要優化首次加載速度,也就是啟動速度:可以參考 TAT.ronnie 的文章中的方法,根據實際情況,最小化 ScrollView 或者 ListView 初始子項數量;優化內存:因為 ScrollView/ListView 會保存所有子 View 在內存中,因為我們沒法刪掉子項,但是我們可以盡量減少每個子項所占的內存。例如這個項目 react-native-sglistview,它在子項不可見的時候,就把它退化成一個最基本的 View;終極解決方案:要真正達到高性能,就需要盡量少的創建 View,要想辦法真正重復利用已經創建的子項。目前只有一些想法,待我實現了,再來更新。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 阿坝| 内江市| 叶城县| 英吉沙县| 涪陵区| 阜新| 乌海市| 利辛县| 句容市| 敦化市| 马公市| 鄱阳县| 江津市| 雷州市| 南开区| 乌鲁木齐市| 南宫市| 瑞昌市| 霞浦县| 黎川县| 辰溪县| 鄂州市| 新乡市| 改则县| 莱阳市| 合水县| 浦北县| 安宁市| 门头沟区| 海林市| 沁阳市| 固安县| 京山县| 定南县| 阳原县| 宁国市| 丰台区| 易门县| 达州市| 平遥县| 曲麻莱县|