新聞中心
1 基本使用

平頂山網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站設(shè)計(jì)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營維護(hù)。成都創(chuàng)新互聯(lián)公司成立于2013年到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)公司。
ScrollView 是 React Native(后面簡稱:RN) 中最常見的組件之一。理解 ScrollView 的原理,有利于寫出高性能的 RN 應(yīng)用。
ScrollView 的基本使用也非常簡單,如下:
- ...
它和 View 組件一樣,可以包含一個(gè)或者多個(gè)子組件。對子組件的布局可以是垂直或者水平的,通過屬性 horizontal=true/false 來控制。甚至還默認(rèn)支持“下拉”刷新操作。另外還有一個(gè)特別贊的特性,超出屏幕的 View 會(huì)自動(dòng)被移除,從而節(jié)省資源和提高繪制效率。我們來看如下一個(gè)例子:
- class ScrollViewTest extends Component {
- render() {
- let children = [];
- for (var i = 0; i < 20; i++) {
- children.push(
{"T" + i} - );
- }
- return (
- {children}
- );
- }
- }
在 Android 上的效果如下:
如圖,我們在 ScrollView 中添加了 20 個(gè)子組件,但是我們的屏幕任意時(shí)刻最多只能顯示 5 個(gè)子項(xiàng)目。
下面我們來看實(shí)際對應(yīng)的 Native 控件的情況。RN 中的 ScrollView 對應(yīng)到 Native 的 RCTScrollView,自動(dòng)把子組件包含在一個(gè) ViewGroup 中(因?yàn)锳ndroid 的 ScrollView 只能有一個(gè)直接子控件),如下圖中的紅色框內(nèi):
注意到,我們在 JS 中添加了 20 個(gè)子組件,但是在 RCTViewGroup 中只有在屏幕上顯示的 5 個(gè)子控件,在屏幕外的組件,也會(huì)自動(dòng)添加到 View 樹中,這與 Native 的 ScrollView 表現(xiàn)一致。
其實(shí),RN 中的 ScrollView 有一個(gè) removeClippedSubviews 屬性,表示如果子 View 超出可視區(qū)域,是否自動(dòng)移除,雖然默認(rèn)是 true。但是也需要子 View 的 overflow: 'hidden'屬性配合。所以,給子組件的 style 添加如下屬性即可。
{"T" + i} - ;
- const styles = StyleSheet.create({
- child: {
- ...
- overflow: 'hidden',
- },
- });
得到的效果是,在使用上完全沒有區(qū)別,而我們來看一下界面的 Tree View,如下圖:
可見,屏幕外的子 View,就被自動(dòng)從 View 樹中移除了。
同時(shí),我們來看一下 iOS 平臺上的表現(xiàn),與 Android 上類似:
這印證了我們前面的結(jié)論,RN 自動(dòng)優(yōu)化了 Native 平臺 ScrollView,在這個(gè)層面,我們可以說 RN 比 Native 的性能還要高。
2 性能研究
通過上面的實(shí)例,我們可以看到,ScrollView 應(yīng)該是非常高效的,它使用簡單,并且還能按需構(gòu)建 View 樹,高效渲染,有點(diǎn)類似 Native 平臺上的 ListView 了,是我心目完美 ScrollView 該有的樣子。
但是,之前看到騰訊的 TAT.ronnie 一篇文章 探索 react native 首屏渲染最佳實(shí)踐,文中提到的優(yōu)化方法,主要就是針對 ScrollView 的。作者認(rèn)為,在 ScrollView 中,即使不可見(例如,超出屏幕)的組件還是會(huì)繪制的。為了優(yōu)化 ScrollView 的繪制性能,不可見的組件,應(yīng)該在 JS 中避免添加到 ScrollView 中。
顯然,這與我們前面觀察到的結(jié)論是矛盾的。但是,作者的通過那樣處理,確實(shí)優(yōu)化了顯示性能,這是怎么回事呢?為了驗(yàn)證,我們也和文中一樣,使用 componentDidMount() 和 componentWillMount() 的時(shí)間差衡量顯示速度。在 Android 上,測試 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)存也有類似趨勢,說明 TAT.ronnie 的改進(jìn)方法確實(shí)是有效的。另外我們也注意到,隨著子組件的數(shù)量增加,Draw 的時(shí)間并沒有明顯的變化,其實(shí) Measure 和 Layout 時(shí)間也沒有明顯的變化。
說明 ScrollView 雖然有 removeClippedSubviews 屬性,也確實(shí)在 View Hierarchy 中去掉了不可見的 View。但是組件的加載時(shí)間消耗資源還是隨著子組件的數(shù)量成正比。
3 原因分析
來看一下 RN 中 ScrollView 的相關(guān)的源碼,主要分析 Android 平臺的代碼,iOS 類似,就不贅述了。
- // ScrollView.js
- var AndroidScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps);
- var AndroidHorizontalScrollView = requireNativeComponent(
- 'AndroidHorizontalScrollView',
- ScrollView,
- nativeOnlyProps
- );
- var ScrollView = React.createClass({
- render: function() {
- var contentContainer =
; - ...
- removeClippedSubviews={this.props.removeClippedSubviews}
- collapsable={false}>
- {this.props.children}
- var ScrollViewClass;
- if (Platform.OS === 'ios') {
- ...
- } else if (Platform.OS === 'android') {
- if (this.props.horizontal) {
- ScrollViewClass = AndroidHorizontalScrollView;
- } else {
- ScrollViewClass = AndroidScrollView;
- }
- }
- // 為了簡單,忽略有下拉刷新的情況
- return (
- {contentContainer}
- );
- }
- });
JS 部分的代碼邏輯很簡單。首先把 ScrollView 所有子組件包裝在一個(gè) View contentContainer 中,并繼承設(shè)置了 removeClippedSubviews 屬性。根據(jù) ScrollView 是否是水平方向,決定是用 RCTScrollView 或者 AndroidHorizontalScrollView Native 組件來包含 contentContainer。
所以,我們先來看 RCTScrollView 本地組件對應(yīng)的代碼(AndroidHorizontalScrollView 原理也類似)。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)的代碼。主要在下面這段代碼:
- @Override
- public void updateClippingRect() {
- if (!mRemoveClippedSubviews) {
- return;
- }
- ...
- View contentView = getChildAt(0);
- if (contentView instanceof ReactClippingViewGroup) {
- ((ReactClippingViewGroup) contentView).updateClippingRect();
- }
- }
可見,如果不開啟 mRemoveClippedSubviews,它就和普通的 ScrollView 一樣,否者,它就會(huì)調(diào)用了它的第一個(gè)(也是唯一的一個(gè))子 View 的 updateClippingRect() 方法。從上面的 JS 中我們可以看到,它的第一個(gè)子元素應(yīng)該就是一個(gè) View 組件,對應(yīng)的 Native 的控件就是 ReactViewGroup。 ReactViewGroup 是 RN for Android 中最基礎(chǔ)的控件,它直接繼承于 android.view.ViewGroup:
- public class ReactViewGroup extends ViewGroup implements
- ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView {
- private boolean mRemoveClippedSubviews = false;
- // 用來保存所有子 View 的數(shù)組,包括可見和不可見的
- private @Nullable View[] mAllChildren = null;
- private int mAllChildrenCount;
- // 當(dāng)前 ReactViewGroup 于父 View 相交矩陣,
- // 也就是它自己在父 View 中可見區(qū)域
- private @Nullable Rect mClippingRect;
- ...
- }
在 ReactViewGroup 中實(shí)現(xiàn) removeClippedSubviews 的功能也非常直接,需要更新界面 Layout 的時(shí)候,遍歷所有的子 View,看子 View 是否在 mClippingRect 區(qū)域內(nèi),如果在,就通過 addViewInLayout() 方法添加此 View,否者就通過 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à)累加起來,對 APP 造成的延遲和卡頓是相當(dāng)可觀的。例如前面的測試中有 1000 個(gè)子組件,加載時(shí)間竟然長達(dá) 9.5 秒。我們用Method Tracing 看一下創(chuàng)建一個(gè)子 View 所花的時(shí)間,如下圖:
這里只是簡單的創(chuàng)建一個(gè) TextView 就消耗了大約 25ms 的時(shí)間。當(dāng)然 Tracing 過程本身會(huì)拖慢 APP 運(yùn)行,但是不影響我們的結(jié)論。所以 Android 中列表類的控件,都內(nèi)部支持對 View 的復(fù)用,盡量避免創(chuàng)建 View。
通過前面的分析,我們可以得到的結(jié)論是:RN 中的 ScrollView 并不像我們想象的那樣高性能。
4 ListView
在這里提到 ListView,是因?yàn)?RN 中的 ListView 就是基于 ScrollView 的,但是有一些優(yōu)化。這里簡要介紹一些 ListView 的原理。
ListView 其實(shí)是對 ScrollView 的一個(gè)封裝,對應(yīng)到 Native 平臺,和 ScrollView 的表現(xiàn)一模一樣。但是 ListView 在顯示列表內(nèi)容的時(shí)候,會(huì)根據(jù)滑動(dòng)距離,逐步向 ScrollView 中添加子組件(通過調(diào)用 renderRow() 方法)。注意到 ListView 有 initialListSize 屬性,表示第一次加載的時(shí)候添加多少個(gè)子項(xiàng),默認(rèn)是 10,還有 pageSize 屬性,表示每次需要添加的時(shí)候,增加多少個(gè)子項(xiàng),默認(rèn)是 1。
通過上面的分析我們可以看到,ListView 在第一次加載的時(shí)候,不論你的列表有多大,默認(rèn)最多加載 initialListSize 個(gè)子項(xiàng),所以能保證啟動(dòng)速度,如果還沒有充滿,或者在向下滑動(dòng)過程中,再組件添加子項(xiàng)。這樣的操作似乎比較合理,但是注意到,整個(gè)操作中,會(huì)逐漸向 ListView 中添加子項(xiàng),新出現(xiàn)的子項(xiàng),都是通過創(chuàng)建新的 View,而完全沒有復(fù)用的過程。所以,如果在應(yīng)用中,ListView 中的子項(xiàng)數(shù)量特別多,ListView 往下滑動(dòng)的過程中,內(nèi)存會(huì)逐漸上漲的。
值得一提的是,ListView 提供了 renderScrollComponent,可以使用其他 Scroll 組件來替換 ScrollView,并且 RecyclerViewBackedScrollView 組件來作為備選??吹竭@個(gè)名字我很欣喜,說明它支持子項(xiàng)的回收復(fù)用(Recycler)。首先,看到 iOS 的實(shí)現(xiàn) RecyclerViewBackedScrollView.ios.js,其實(shí)它就是 ScrollView,并沒有實(shí)現(xiàn)所謂的復(fù)用,失望了一半。繼續(xù)看 Android 的實(shí)現(xiàn),它實(shí)際上是對應(yīng) Native 的 com.facebook.react.views.recyclerview.AndroidRecyclerViewBackedScrollView,它繼承與 Android 的 RecyclerView??吹竭@里,如果使用這種方法,我直觀感覺 RN 的 ListView 性能在 Android 上表現(xiàn)應(yīng)該會(huì)比 iOS 好。
我們繼續(xù)來看它是怎么實(shí)現(xiàn)回收復(fù)用的,AndroidRecyclerViewBackedScrollView 內(nèi)部實(shí)現(xiàn)了一個(gè) RecyclerView.Adapter,如下:
- static class ReactListAdapter extends Adapter
{ - private final List
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();
- }
- }
注意到這里有一個(gè) mViews,用來保存所有的子 View,綁定 View 的時(shí)候只是簡單用一個(gè)空的 View(RecyclableWrapperViewGroup)包了一下。這樣一來,RecyclerView 完全沒有什么起到復(fù)用的作用呀!測試一下,確實(shí)也是這樣,性能問題還是很嚴(yán)重。
這里我們也可以得到一個(gè)結(jié)論:RN 中的 ListView 也不是我們想象的 ListView 該有的性能。
5 改進(jìn)方案
通過前面的分析,我們已經(jīng)知道了 RN 中的 ScrollView 或者 ListView 的性能瓶頸了,同時(shí)也有了改進(jìn)的思路。下面針對各種情況分析:
- 如果要優(yōu)化首次加載速度,也就是啟動(dòng)速度:可以參考 TAT.ronnie 的文章中的方法,根據(jù)實(shí)際情況,最小化 ScrollView 或者 ListView 初始子項(xiàng)數(shù)量;
- 優(yōu)化內(nèi)存:因?yàn)?ScrollView/ListView 會(huì)保存所有子 View 在內(nèi)存中,因?yàn)槲覀儧]法刪掉子項(xiàng),但是我們可以盡量減少每個(gè)子項(xiàng)所占的內(nèi)存。例如這個(gè)項(xiàng)目 react-native-sglistview,它在子項(xiàng)不可見的時(shí)候,就把它退化成一個(gè)最基本的 View;
- 終極解決方案:要真正達(dá)到高性能,就需要盡量少的創(chuàng)建 View,要想辦法真正重復(fù)利用已經(jīng)創(chuàng)建的子項(xiàng)。目前只有一些想法,待我實(shí)現(xiàn)了,再來更新。
網(wǎng)頁題目:ReactNative中ScrollView性能探究
本文來源:http://m.fisionsoft.com.cn/article/dhhhiic.html


咨詢
建站咨詢
