由Dialog裏面嵌套ListView以後的高度自適應引發的ListView性能優化

先說ListView給高的正確作法. android:layout_height屬性:java

必須將ListView的佈局高度屬性設置爲非「wrap_content」(能夠是「match_parent / fill_parent / 400dp等絕對數值」)android

廢話少說先來張bug圖填樓git

圖片

<!--more-->github

前言

隨着RecyclerView的普及,ListView差很少是安卓快要淘汰的控件了,可是咱們有時候仍是會用到,基本上能夠說是前些年最經常使用的Android控件之一了.拋開咱們的主題,咱們先來談談ListView的一些小小的細節,多是不少開發者在開發過程當中並無注意到的細節,這些細節設置會影響到咱們的App的性能.緩存

  • android:layout_height屬性

咱們在使用ListView的時候極可能隨手就會寫一個layout_height=」wrap_content」或者layout_height=」match_parent」,很是很是普通,咋一看,我寫的沒錯啊...但是實際上layout_height=」wrap_content」 是錯誤的寫法!!!會嚴重影響程序的性能 咱們先來作一個實驗: xml佈局文件以下ide

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ></ListView>
</LinearLayout>

java部分代碼 圖片佈局

運行log 圖片性能

咱們會發現getView總共被調用了15次!其中4次是null的,11次爲重複調用,ListView的item數目只有3項!!!太可怕了this

咱們試着將ListView的高度屬性改成layout_height=」match_parent」,而後看看 咱們能夠看到getView()只被調用了3次!這應該是咱們指望的結果!spa

緣由分析: 瞭解緣由前,咱們應該先了解View的繪製流程,以前個人博客沒有關於View繪製流程的介紹,那麼在這邊說一下,是一個很重要的知識點. View的繪製流程是經過 onMeasure()->onLayout()->onDraw()

onMeasure() :主要工做是測量視圖的大小.從頂層的父View到子View遞歸調用measure方法,measure方法又回調onMeasure().

onLayout: 主要工做是肯定View的位置,進行頁面佈局.從頂層的父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子view所獲得的佈局大小和佈局參數,將子view放在合適的位置上

onDraw() 主要工做是繪製視圖.ViewRoot建立一個Canvas對象,而後調用onDraw()方法.總共6個步驟.1.繪製視圖背景,2.保存當前畫布的圖層(Layer),3.繪製View內容,4.繪製View的子View視圖,沒有的話就不繪製,5.還原圖層,6.繪製滾動條.

瞭解了View的繪製流程,那麼咱們回到這個問題上.設置ListView的屬性layout_height=」wrap_content」,就意味着Listview的高度由子View決定,當在onMeasure()的時候,須要測量子View的高度,那咱們來看看Listview的onMeasure()方法.

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
                heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            measureScrapChild(child, 0, widthMeasureSpec);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState&MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize , heightSize);
        mWidthMeasureSpec = widthMeasureSpec;
    }

其中

if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

比較重要

再看measureHeightOfChildren()

final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            final int maxHeight, int disallowPartialChildPosition) {

        ...

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);

            measureScrapChild(child, i, widthMeasureSpec);
            ...

            // Recycle the view before we possibly return from the method
            if (recyle && recycleBin.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                recycleBin.addScrapView(child, -1);
            }

            returnedHeight += child.getMeasuredHeight();

            if (returnedHeight >= maxHeight) {
                ...
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                ...
            }
        }
        return returnedHeight;
    }

obtainView(i, isScrap)是子View的實例 measureScrapChild(child, i, widthMeasureSpec); 測量子View recycleBin.addScrapView(child, -1);將子View加入緩存,能夠用來複用 if (returnedHeight >= maxHeight) {return ...;}若是已經測量的子View的高度大於maxHeight的話就直接return出循環,這樣的作法也很好理解,實際上是ListView很聰明的一種作法,你能夠想一想好比說這個屏幕只能畫10個Item高度,你有20個Item,那麼畫出10個就好了,剩下的十個就不必畫了~

咱們如今看下obtainView()方法

View obtainView(int position, boolean[] isScrap) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        isScrap[0] = false;

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            // Scrap view implies temporary detachment.
            isScrap[0] = true;
            return transientView;
        }

        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else {
                isScrap[0] = true;

                child.dispatchFinishTemporaryDetach();
            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position);

        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        return child;
    }

獲得一個視圖,它顯示的數據與指定的位置。這叫作當咱們已經發現的觀點不是可供重用的回收站。剩下的惟一的選擇是將一個古老的視圖或製做一個新的.(這是方法註釋的翻譯,大體能夠理解他的意思)

咱們應該關注下如下兩行代碼:

...
  final View scrapView = mRecycler.getScrapView(position);
  final View child = mAdapter.getView(position, scrapView, this);
...

這兩行代碼的意思就是說先從緩存裏面取出來一個廢棄的view,而後將當前的位置跟view做爲參數傳入到getView()方法中.這個廢棄的,而後又做爲參數的view就是convertView.

而後咱們總結下剛剛的步驟: A、測量第0項的時候,convertView確定是null的 View scrapView = mRecycler.getScrapView(position)也是空的,因此咱們在log上能夠看到. B、第0項測量結束,這個第0項的View就被加入到複用緩存當中了; C、開始測量第1項,這時由於是有第0項的View緩存的,因此getView的參數convertView就是這個第0項的View緩存,而後重複B步驟添加到緩存,只不過這個View緩存仍是第0項的View; D、繼續測量第2項,重複C。

因此前面說到onMeasure方法會致使getView調用,而一個View的onMeasure方法調用時機並非由自身決定,而是由其父視圖來決定。ListView放在FrameLayout和RelativeLayout中其onMeasure方法的調用次數是徹底不一樣的。在RelativeLayout中oMeasure()方法調用會翻倍.

因爲onMeasure方法會屢次被調用,上述問題中是兩次,其實完整的調用順序是onMeasure - onLayout - onMeasure - onLayout - onDraw。

因此根據上面的結論咱們能夠得出,若是LitsView的android:layout_height屬性設置爲wrap_content將會引發getView的屢次測量

現象

如上bug圖...

產生的緣由

  • ListView的高度設置成了android:layout_height屬性設置爲wrap_content

  • ListView的父類是RelativeLayout,RelativiLayout佈局會使子佈局View的Measure週期翻倍,有興趣能夠看下三大基礎佈局性能比較

解決辦法

根據每一個Item的高度,而後再根據Adapter的count來動態算高. 代碼以下:

public class SetHeight {

    public void setListViewHeightBasedOnChildren(ListView listView, android.widget.BaseAdapter adapter) {

        if (adapter==null){
            return;
        }
        int totalHeight = 0;

        for (int i = 0; i < adapter.getCount(); i++) { // listAdapter.getCount()返回數據項的數目

            View listItem = adapter.getView(i, null, listView);

            listItem.measure(0, 0); // 計算子項View 的寬高

            totalHeight += listItem.getMeasuredHeight(); // 統計全部子項的總高度

        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();

        params.height = totalHeight
                + (listView.getDividerHeight() * (adapter.getCount() - 1));

        // listView.getDividerHeight()獲取子項間分隔符佔用的高度

        // params.height最後獲得整個ListView完整顯示須要的高度

        listView.setLayoutParams(params);

    }

}

xml佈局,注意要將ListView的父類設置爲LinearLayout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/txt_cancel"
        android:orientation="vertical">
        <View
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y2"
            android:background="#cccccc" />

        <ListView
            android:id="@+id/lv_remain_item"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:cacheColorHint="#00000000"
            ></ListView>

        <View
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y2"
            android:background="#cccccc" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        >

        <TextView
            android:id="@+id/txt_cancel"
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y120"
            android:layout_alignParentBottom="true"
            android:gravity="center"
            android:text="cancel"
            android:textSize="@dimen/x32" />
    </LinearLayout>
</LinearLayout>

而後在Listview使用處,調用該方法.

userListDialog.getmListView().setAdapter(scaleUserAdapter);
 SetHeight.setListViewHeightBasedOnChildren(userListDialog.getmListView(),scaleUserAdapter);

運行結果

getView()調用狀況 GitHub代碼地址:ListViewDialog,喜歡的話歡迎Start

相關文章
相關標籤/搜索