GridView Adapter里的getView为啥会多次调用position 0

GridView Adapter里的getView为啥会多次调用position 0

一、问题描述

GridView Adapter里的getView方法总会调用很多次的position 0
GridView Adapter里的getView为啥会多次调用position 0_第1张图片
图中的GridView 包含9个大小相同的itemView,区别在于itemView中包含的textView中的文字不同。

二、问题分析

getView方法

既然是getView引起的,我们就先从这个方法本身入手,看看这里到底干了啥?
首先,我们知道

The Adapter provides access to the data items.
The Adapter is also responsible for making a View for each item in the data set.

那么getView呢?
顾名思义,就是得到一个View用来展示在数据集中特定位置的数据。所以我们需要再往上找,看看是它是怎么被调用的?

onMeasure方法

接下来,我们需要找的就是GridView,阅读GridView源码时,我们主要关注以下3个方法:onMeasure、onLayout、onDraw,那么我们先来看下onMeasure。

  • onMeasure是干啥的? 这里我们要追溯到View的measure方法:计算出一个view的大小,而它实际的计算工作是由它的子类重写onMeasure方法来实现。
  • 具体源码解析
  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        // MeasureSpec 是父View对子View大小的期望:包括测量模式、测量大小
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // codes about measuring childWidth and child height

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        // getCount怎么实现的?
        //     @Override
        // public int getCount() {
        //     return data.size();
        // }
        final int count = mItemCount;
        // 这里就说明了如果我们的count 是数据集中数据的个数
        if (count > 0) {
            // 调用obtainView方法来获得position为0的itemView
            final View child = obtainView(0, mIsScrap);

            AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
            if (p == null) {
                p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
                child.setLayoutParams(p);
            }
            p.viewType = mAdapter.getItemViewType(0);
            p.forceAdd = true;

            int childHeightSpec = getChildMeasureSpec(
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
            int childWidthSpec = getChildMeasureSpec(
                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
            // 让position为0的itemView测量下自己,从而求出宽和高
            child.measure(childWidthSpec, childHeightSpec);

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

            if (mRecycler.shouldRecycleViewType(p.viewType)) {
                mRecycler.addScrapView(child, -1);
            }
        }

        // codes about setting different measuringSize according to different measuringMode
        setMeasuredDimension(widthSize, heightSize);
        mWidthMeasureSpec = widthMeasureSpec;
    }  

三、进一步分析

  • obtainView方法:
    从上面的源码分析中我们可以看到在onMeasure方法中回调用obtainView方法来拿到position为0 的itemView,它是怎么拿的?
View obtainView(int position, boolean[] isScrap) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
    child = mAdapter.getView(position, scrapView, this);
// some other codes
    return child;
}

好啦,现在我们知道是谁老是调适配器里的getView了。但是问题还没有结束,为什么它要调呢?

  • 梳理调用流程:
    流程梳理
    由上图我们知道原来是requestLayout干的,那么什么会触发GridView的requestLayout呢?
    再次阅读GridView源码,有多种情况都会触发:

    • setSelection: set the currently selected item
    • onFocusChanged
    • setGravity: set the gravity for this grid.
    • setHorizontalSpacing
    • setVerticalSpacing
    • setColumnWidth
    • 等等;

    结合以上分析,回到之前我们最顶部的结果,为什么有9个itemView就会调用多次position为0的getView呢?
    进一步分析GridView代码我们可以发现:

    @Override
    protected void layoutChildren() {
        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = true;
        }
        // ....
     }

    也就是说在第一次layoutChildren的时候,mBlockLayoutRequrest 这个标志位会置为true,那么这个标志位是干什么的?

    When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
    当然这里可能还有其他情况会触发requestLayout,欢迎大家补充。
    当然这里可能还有其他情况会trigger requestLayout,欢迎大家补充。
    补充解释一下:
    GridView中onLayout是在它的父类AbsListView中定义的,那么他是怎么实现的呢?

    /**
     * Subclasses should NOT override this method but
     *  {@link #layoutChildren()} instead.
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mInLayout = true;
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }
       // 从下面这段代码中我们可以发现,GridView在第一次Layout时,每增加一个新的itemView都会调用一次onLayout()
        if (mFastScroller != null && (mItemCount != mOldItemCount || mDataChanged)) {
            mFastScroller.onItemCountChanged(mItemCount);
        }
       // ok,我们终于找到了你
        layoutChildren();
        mInLayout = false;
    
        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
    } 

    通过查看源码,我们终于清楚了getView在position 0调用多次的原因。那有没有啥解决办法呢?

四、解决办法

  • 方法一:在getView中新增判断:即判断postion 0之前是否调用过,如果没有调用过,则在创建的同时把它保存下来,若已经调用过的直接返回之前保存的view不再重新创建;
  • 方法二:期待中。。。

五、参考文章

Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)

ListView / GirdView Adpater的getView方法,首项多次调用

Android好奇宝宝03有点坑的GridView

Android Developers: View’s requestLayout

Android ListView工作原理完全解析,带你从源码的角度彻底理解

你可能感兴趣的:(android)