av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

ReactNative中ScrollView性能探究

1 基本使用

平頂山網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站設(shè)計(jì)等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。成都創(chuàng)新互聯(lián)公司成立于2013年到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專(zhuān)注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)公司。

ScrollView 是 React Native(后面簡(jiǎn)稱(chēng):RN) 中最常見(jiàn)的組件之一。理解 ScrollView 的原理,有利于寫(xiě)出高性能的 RN 應(yīng)用。

ScrollView 的基本使用也非常簡(jiǎn)單,如下:

 
 
 
 
  1.   
  2.   
  3.   
  4.   ...
  5.  

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

 
 
 
 
  1. class ScrollViewTest extends Component {
  2.  
  3.   render() {
  4.     let children = [];
  5.  
  6.     for (var i = 0; i < 20; i++) {
  7.       children.push(
  8.         
  9.           {"T" + i}
  10.         );
  11.     }
  12.     return (
  13.         
  14.           {children}
  15.         
  16.     );
  17.   }

在 Android 上的效果如下:

如圖,我們?cè)?ScrollView 中添加了 20 個(gè)子組件,但是我們的屏幕任意時(shí)刻最多只能顯示 5 個(gè)子項(xiàng)目。

下面我們來(lái)看實(shí)際對(duì)應(yīng)的 Native 控件的情況。RN 中的 ScrollView 對(duì)應(yīng)到 Native 的 RCTScrollView,自動(dòng)把子組件包含在一個(gè) ViewGroup 中(因?yàn)锳ndroid 的 ScrollView 只能有一個(gè)直接子控件),如下圖中的紅色框內(nèi):

注意到,我們?cè)?JS 中添加了 20 個(gè)子組件,但是在 RCTViewGroup 中只有在屏幕上顯示的 5 個(gè)子控件,在屏幕外的組件,也會(huì)自動(dòng)添加到 View 樹(shù)中,這與 Native 的 ScrollView 表現(xiàn)一致。

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

 
 
 
 
  1.   
  2.   {"T" + i}
  3. ;
  4.  
  5. const styles = StyleSheet.create({  
  6.   child: {
  7.     ...
  8.     overflow: 'hidden',
  9.   },
  10. }); 

得到的效果是,在使用上完全沒(méi)有區(qū)別,而我們來(lái)看一下界面的 Tree View,如下圖:

可見(jiàn),屏幕外的子 View,就被自動(dòng)從 View 樹(shù)中移除了。

同時(shí),我們來(lái)看一下 iOS 平臺(tái)上的表現(xiàn),與 Android 上類(lèi)似:

這印證了我們前面的結(jié)論,RN 自動(dòng)優(yōu)化了 Native 平臺(tái) ScrollView,在這個(gè)層面,我們可以說(shuō) RN 比 Native 的性能還要高。

2 性能研究

通過(guò)上面的實(shí)例,我們可以看到,ScrollView 應(yīng)該是非常高效的,它使用簡(jiǎn)單,并且還能按需構(gòu)建 View 樹(shù),高效渲染,有點(diǎn)類(lèi)似 Native 平臺(tái)上的 ListView 了,是我心目完美 ScrollView 該有的樣子。

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

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

子組件數(shù)量 加載時(shí)間(ms) 占用內(nèi)存(MB) 繪制時(shí)間*(ms)
10 309 19.7 14.666
100 1170 21.9 15.016
1000 9461 26.5 15.025

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

從加載時(shí)間看,時(shí)間隨著子組件的數(shù)量線性增加,占用內(nèi)存也有類(lèi)似趨勢(shì),說(shuō)明 TAT.ronnie 的改進(jìn)方法確實(shí)是有效的。另外我們也注意到,隨著子組件的數(shù)量增加,Draw 的時(shí)間并沒(méi)有明顯的變化,其實(shí) Measure 和 Layout 時(shí)間也沒(méi)有明顯的變化。

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

3 原因分析

來(lái)看一下 RN 中 ScrollView 的相關(guān)的源碼,主要分析 Android 平臺(tái)的代碼,iOS 類(lèi)似,就不贅述了。

 
 
 
 
  1. // ScrollView.js
  2. var AndroidScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps);  
  3. var AndroidHorizontalScrollView = requireNativeComponent(  
  4.   'AndroidHorizontalScrollView',
  5.   ScrollView,
  6.   nativeOnlyProps
  7. );
  8.  
  9. var ScrollView = React.createClass({  
  10.   render: function() {
  11.     var contentContainer =
  12.        
  13.          ...
  14.          removeClippedSubviews={this.props.removeClippedSubviews}
  15.          collapsable={false}>
  16.          {this.props.children}
  17.        ;
  18.  
  19.      var ScrollViewClass;
  20.      if (Platform.OS === 'ios') {
  21.        ...
  22.      } else if (Platform.OS === 'android') {
  23.        if (this.props.horizontal) {
  24.          ScrollViewClass = AndroidHorizontalScrollView;
  25.        } else {
  26.          ScrollViewClass = AndroidScrollView;
  27.        }
  28.      }
  29.  
  30.      // 為了簡(jiǎn)單,忽略有下拉刷新的情況
  31.      return (
  32.       
  33.         {contentContainer}
  34.       
  35.     );
  36.   }
  37. }); 

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

所以,我們先來(lái)看 RCTScrollView 本地組件對(duì)應(yīng)的代碼(AndroidHorizontalScrollView 原理也類(lèi)似)。JS 中的 RCTScrollView 組件由 com.facebook.react.views.scroll.ReactScrollViewManager 提供,具體的 View 的實(shí)現(xiàn)是 com.facebook.react.views.scroll.ReactScrollView。

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

 
 
 
 
  1. @Override
  2. public void updateClippingRect() {  
  3.   if (!mRemoveClippedSubviews) {
  4.     return;
  5.   }
  6.   ...
  7.   View contentView = getChildAt(0);
  8.   if (contentView instanceof ReactClippingViewGroup) {
  9.     ((ReactClippingViewGroup) contentView).updateClippingRect();
  10.   }

可見(jiàn),如果不開(kāi)啟 mRemoveClippedSubviews,它就和普通的 ScrollView 一樣,否者,它就會(huì)調(diào)用了它的第一個(gè)(也是唯一的一個(gè))子 View 的 updateClippingRect() 方法。從上面的 JS 中我們可以看到,它的第一個(gè)子元素應(yīng)該就是一個(gè) View 組件,對(duì)應(yīng)的 Native 的控件就是 ReactViewGroup。 ReactViewGroup 是 RN for Android 中最基礎(chǔ)的控件,它直接繼承于 android.view.ViewGroup:

 
 
 
 
  1. public class ReactViewGroup extends ViewGroup implements  
  2.     ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView {
  3.   private boolean mRemoveClippedSubviews = false;
  4.   // 用來(lái)保存所有子 View 的數(shù)組,包括可見(jiàn)和不可見(jiàn)的
  5.   private @Nullable View[] mAllChildren = null;
  6.   private int mAllChildrenCount;
  7.   // 當(dāng)前 ReactViewGroup 于父 View 相交矩陣,
  8.   // 也就是它自己在父 View 中可見(jiàn)區(qū)域
  9.   private @Nullable Rect mClippingRect;
  10.   ...

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

到這了,我們就可以解釋前面的矛盾了。雖然在 ScrollView 的 View Hierarchy 中,會(huì)自動(dòng)移除不顯示的 View,但是實(shí)際上還是創(chuàng)建了所有的子 View,所以所占內(nèi)存和加載時(shí)間會(huì)線性增加。

關(guān)于創(chuàng)建所有子 View,我這里可以多分析一下。我們知道在 Android 中,創(chuàng)建 View 的代價(jià)是很大的。特別是在 ScrollView 中,所有的子 View 都是同時(shí)創(chuàng)建的。如果 ScrollView 中子 View 的數(shù)量很多,這樣的代價(jià)累加起來(lái),對(duì) APP 造成的延遲和卡頓是相當(dāng)可觀的。例如前面的測(cè)試中有 1000 個(gè)子組件,加載時(shí)間竟然長(zhǎng)達(dá) 9.5 秒。我們用Method Tracing 看一下創(chuàng)建一個(gè)子 View 所花的時(shí)間,如下圖:

這里只是簡(jiǎn)單的創(chuàng)建一個(gè) TextView 就消耗了大約 25ms 的時(shí)間。當(dāng)然 Tracing 過(guò)程本身會(huì)拖慢 APP 運(yùn)行,但是不影響我們的結(jié)論。所以 Android 中列表類(lèi)的控件,都內(nèi)部支持對(duì) View 的復(fù)用,盡量避免創(chuàng)建 View。

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

4 ListView

在這里提到 ListView,是因?yàn)?RN 中的 ListView 就是基于 ScrollView 的,但是有一些優(yōu)化。這里簡(jiǎn)要介紹一些 ListView 的原理。

ListView 其實(shí)是對(duì) ScrollView 的一個(gè)封裝,對(duì)應(yīng)到 Native 平臺(tái),和 ScrollView 的表現(xiàn)一模一樣。但是 ListView 在顯示列表內(nèi)容的時(shí)候,會(huì)根據(jù)滑動(dòng)距離,逐步向 ScrollView 中添加子組件(通過(guò)調(diào)用 renderRow() 方法)。注意到 ListView 有 initialListSize 屬性,表示第一次加載的時(shí)候添加多少個(gè)子項(xiàng),默認(rèn)是 10,還有 pageSize 屬性,表示每次需要添加的時(shí)候,增加多少個(gè)子項(xiàng),默認(rèn)是 1。

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

值得一提的是,ListView 提供了 renderScrollComponent,可以使用其他 Scroll 組件來(lái)替換 ScrollView,并且 RecyclerViewBackedScrollView 組件來(lái)作為備選??吹竭@個(gè)名字我很欣喜,說(shuō)明它支持子項(xiàng)的回收復(fù)用(Recycler)。首先,看到 iOS 的實(shí)現(xiàn) RecyclerViewBackedScrollView.ios.js,其實(shí)它就是 ScrollView,并沒(méi)有實(shí)現(xiàn)所謂的復(fù)用,失望了一半。繼續(xù)看 Android 的實(shí)現(xiàn),它實(shí)際上是對(duì)應(yīng) Native 的 com.facebook.react.views.recyclerview.AndroidRecyclerViewBackedScrollView,它繼承與 Android 的 RecyclerView??吹竭@里,如果使用這種方法,我直觀感覺(jué) RN 的 ListView 性能在 Android 上表現(xiàn)應(yīng)該會(huì)比 iOS 好。

我們繼續(xù)來(lái)看它是怎么實(shí)現(xiàn)回收復(fù)用的,AndroidRecyclerViewBackedScrollView 內(nèi)部實(shí)現(xiàn)了一個(gè) RecyclerView.Adapter,如下:

 
 
 
 
  1. static class ReactListAdapter extends Adapter {
  2.  
  3.   private final List mViews = new ArrayList<>();
  4.  
  5.   public void addView(View child, int index) {
  6.     mViews.add(index, child);
  7.     ...
  8.   }
  9.  
  10.   public void removeViewAt(int index) {
  11.     View child = mViews.get(index);
  12.     if (child != null) {
  13.       mViews.remove(index);
  14.       ...
  15.     }
  16.   }
  17.  
  18.   @Override
  19.   public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  20.     return new ConcreteViewHolder(new RecyclableWrapperViewGroup(parent.getContext()));
  21.   }
  22.  
  23.   @Override
  24.   public void onBindViewHolder(ConcreteViewHolder holder, int position) {
  25.     RecyclableWrapperViewGroup vg = (RecyclableWrapperViewGroup) holder.itemView;
  26.     View row = mViews.get(position);
  27.     if (row.getParent() != vg) {
  28.       vg.addView(row, 0);
  29.     }
  30.   }
  31.  
  32.   @Override
  33.   public void onViewRecycled(ConcreteViewHolder holder) {
  34.     super.onViewRecycled(holder);
  35.     ((RecyclableWrapperViewGroup) holder.itemView).removeAllViews();
  36.   }  

注意到這里有一個(gè) mViews,用來(lái)保存所有的子 View,綁定 View 的時(shí)候只是簡(jiǎn)單用一個(gè)空的 View(RecyclableWrapperViewGroup)包了一下。這樣一來(lái),RecyclerView 完全沒(méi)有什么起到復(fù)用的作用呀!測(cè)試一下,確實(shí)也是這樣,性能問(wèn)題還是很?chē)?yán)重。

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

5 改進(jìn)方案

通過(guò)前面的分析,我們已經(jīng)知道了 RN 中的 ScrollView 或者 ListView 的性能瓶頸了,同時(shí)也有了改進(jìn)的思路。下面針對(duì)各種情況分析:

  1. 如果要優(yōu)化首次加載速度,也就是啟動(dòng)速度:可以參考 TAT.ronnie 的文章中的方法,根據(jù)實(shí)際情況,最小化 ScrollView 或者 ListView 初始子項(xiàng)數(shù)量;
  2. 優(yōu)化內(nèi)存:因?yàn)?ScrollView/ListView 會(huì)保存所有子 View 在內(nèi)存中,因?yàn)槲覀儧](méi)法刪掉子項(xiàng),但是我們可以盡量減少每個(gè)子項(xiàng)所占的內(nèi)存。例如這個(gè)項(xiàng)目 react-native-sglistview,它在子項(xiàng)不可見(jiàn)的時(shí)候,就把它退化成一個(gè)最基本的 View;
  3. 終極解決方案:要真正達(dá)到高性能,就需要盡量少的創(chuàng)建 View,要想辦法真正重復(fù)利用已經(jīng)創(chuàng)建的子項(xiàng)。目前只有一些想法,待我實(shí)現(xiàn)了,再來(lái)更新。

新聞名稱(chēng):ReactNative中ScrollView性能探究
本文地址:http://uogjgqi.cn/article/dhhhiic.html
掃二維碼與項(xiàng)目經(jīng)理溝通

我們?cè)谖⑿派?4小時(shí)期待你的聲音

解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流