由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

相关文章
相关标签/搜索