ListView和GridView的缓存机制及measure过程

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html


在Android开发中我们经常使用ListView和GridView,它们都有一套缓存机制,通过复用防止view的不停创建。

ListView和GridView都是AbsListView的子类,使用其内部类RecycleBin来进行view的缓存。


1、View的Transient状态

要想搞懂RecycleBin的缓存机制,我们首先要了解Transient和Scrap都是什么。
Transient是View的一种状态,可以通过View的hasTransientState函数来判断,官方解释如下:

A view with transient state cannot be trivially rebound from an external data source, such as an adapter binding item views in a list. This may be because the view is performing an animation, tracking user selection of content, or similar.


从解释上看,Transient是指View的一种不稳定状态,是瞬时状态,比如说正在执行一个动画,有可能下一秒就改变了。

而Scrap则是ListView和GridView的缓存状态,当一个Item不可见被回收后存入缓存。


2、RecycleBin

在RecycleBin中与缓存相关的有三个List:
private ArrayList<View>[] mScrapViews;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
其中mTransientStateViews和mTransientStateViewsById都是缓存Transient状态的view的,而mScrapViews则是缓存Scrap状态的view。
我们从添加缓存开始来看,来看RecycleBin的addScrapView函数,部分代码如下:
void addScrapView(View scrap, int position) {
    ...


    // Don't scrap views that have transient state.
    final boolean scrapHasTransientState = scrap.hasTransientState();
    if (scrapHasTransientState) {
        if (mAdapter != null && mAdapterHasStableIds) {
            // If the adapter has stable IDs, we can reuse the view for
            // the same data.
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<>();
            }
            mTransientStateViewsById.put(lp.itemId, scrap);
        } else if (!mDataChanged) {
            // If the data hasn't changed, we can reuse the views at
            // their old positions.
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<>();
            }
            mTransientStateViews.put(position, scrap);
        } else {
            // Otherwise, we'll have to remove the view and start over.
            getSkippedScrap().add(scrap);
        }
    } else {
        if (mViewTypeCount == 1) {
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }


        if (mRecyclerListener != null) {
            mRecyclerListener.onMovedToScrapHeap(scrap);
        }
    }
}

在这里我们先判断view是否处于Transient状态,如果是Transient,则将其保存至mTransientStateViews或mTransientStateViewsById中。

至于到底保存到哪个list中,则通过mAdapterHasStableIds变量来判断,mAdapterHasStableIds则是通过Adapter的hasStableIds函数获得的,这个函数是需要子类去实现,它的含义是Adapter拥有稳定的ItemId,即Adapter中同一个Object的ItemId是固定不变的,这就需要我们一定要重写Adapter的getItemId方法,否则这里就会出现问题。


关于ItemId这部分,在AbsListView的setItemViewLayoutParams可以查看到:
private void setItemViewLayoutParams(View child, int position) {
    final ViewGroup.LayoutParams vlp = child.getLayoutParams();
    ...


    if (mAdapterHasStableIds) {
        lp.itemId = mAdapter.getItemId(position);
    }
    lp.viewType = mAdapter.getItemViewType(position);
    if (lp != vlp) {
      child.setLayoutParams(lp);
    }
}

回到addScrapView函数,如果不是Transient状态,则会将child保存到mScrapViews中。


3、obtainView

前面我们看到了添加缓存的过程,那么在哪里使用呢?

obtainView函数是AbsListView的一个函数,用于获取每个item的view,其中就包括使用缓存机制。

这个函数的代码如下:
View obtainView(int position, boolean[] isScrap) {
    ...


    // 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);
            }
        }


        isScrap[0] = true;


        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        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;
            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }


    ...


    return child;
}

这里面包含两个部分

第一部分:
通过RecycleBin的getTransientStateView获取transient状态的view。
如果存在对应position的transient状态的view,再判断transientView的viewType与这个position的ViewType是否一致。
如果ViewType一致,则调用Adapter的getView方法获取child,而transientView作为convertView参数。
如果得到的child的view与transientView不是同一个对象,比如getView中未使用convertView,则将child添加进ScrapView缓存中。
第一部分结束直接return了,不会继续执行下一部分。

第二部分:
如果不存在transient状态的view,即getTransientStateView获取的是null,那么通过RecycleBin的getScrapView函数从缓存列表中获取一个scrapView。
注意这里没有判断ViewType,是因为getScrapView函数内部进行判断处理了。
然后调用Adapter的getView方法获取child,而将scrapView作为convertView参数。
最后同样判断得到的child的view与scrapView是不是同一个对象,不是则添加进ScrapView缓存。


4、getView的调用


以上就是ListView和GridView的缓存机制。

那么我们来思考另外一个问题:
经常使用Adapter的同学可能会发现,当初始化页面的时候,getView的调用并不是从0到count走一遍即可。那么为什么会这样?这样的意义在哪?

这就要从ListView和GridView的measuer说起。



5、GridView的onMeasure

先来看看GridView的onMeasure方法,关键代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
    final int count = mItemCount;
    if (count > 0) {
        final View child = obtainView(0, mIsScrap);


        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
        ...


        child.measure(childWidthSpec, childHeightSpec);


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


        if (mRecycler.shouldRecycleViewType(p.viewType)) {
            mRecycler.addScrapView(child, -1);
        }
    }
    
    if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }


    if (heightMode == MeasureSpec.AT_MOST) {
        int ourSize =  mListPadding.top + mListPadding.bottom;
       
        final int numColumns = mNumColumns;
        for (int i = 0; i < count; i += numColumns) {
            ourSize += childHeight;
            if (i + numColumns < count) {
                ourSize += mVerticalSpacing;
            }
            if (ourSize >= heightSize) {
                ourSize = heightSize;
                break;
            }
        }
        heightSize = ourSize;
    }


    ...


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

当GridView有Adapter且其count>0时,通过obtainView这个函数获取到了position为0的child。
在这里就解释来getView的调用问题,因为通过前面内存我们知道obtainView函数中调用了getView,所以对于GridView来说position为0的getView会提前被调用一次。
那么这里为什么要得到这个child?
我们继续向下看,拿到child之后收到调用了它的measure函数进行自身测量,然后拿到child的高度MeasuredHeight。
继续向下看,当height的SpecMode为UNSPECIFIED或AT_MOST时,则需要用这个child的MeasuredHeight去计算GridView的高度。

当SpecMode为UNSPECIFIED时,GridView的高度只是一个child的高度,这就是为什么在ListView或ScrollView中嵌套GridView只显示一行的原因。

当SpecMode为AT_MOST,需要考虑GridView的ColumnNum,GridView的高度实际上是第一个child的高度和rowNum的乘积,并且加上垂直方向的间隔mVerticalSpacing。
上面的嵌套情况,我们一般的做法是将GridView完全撑开,即自定义一个GridView并重写onMeasuer方法,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
         MeasureSpec.AT_MOST);
   super.onMeasure(widthMeasureSpec, expandSpec);
}

这种情况下正是SpecMode为AT_MOST的情况,注意这时的GridView的撑开的高度只与第一个child的高度有关!



6、ListView的onMeasure

上面我们研究了GridView的onMeasure,下面来看看ListView的onMeasure,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Sets up mListPadding
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    ...

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


        // Lay out child directly against the parent measure spec so that
        // we can obtain exected minimum width and height.
        measureScrapChild(child, 0, widthMeasureSpec, heightSize);


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


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


    ...


    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;
}

同样,当有Adapter且其count>0时,通过obtainView这个函数获取到了第一个child。
然后我们没有看到child的measure函数,但是执行了一个measureScrapChild函数,这个函数中对child进行了一次measure,这里就不贴出代码了。
在高度计算方面,SpecMode为UNSPECIFIED时与GridView一样,这也解释了ScrollView或ListView嵌套ListView为啥只显示一行。
与GridView一样,解决嵌套问题也是自定义ListView并重写onMeasure方法。
但是这里SpecMode为AT_MOST的情况与GridView有所不同,我们看到执行了一个measureHeightOfChildren函数,这个函数代码如下:
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
        int maxHeight, int disallowPartialChildPosition) {
    final ListAdapter adapter = mAdapter;
    if (adapter == null) {
        return mListPadding.top + mListPadding.bottom;
    }


    ...


    endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
    ...


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


        measureScrapChild(child, i, widthMeasureSpec, maxHeight);


        if (i > 0) {
            // Count the divider for all but one child
            returnedHeight += dividerHeight;
        }


        // 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();


        ...
    }


    // At this point, we went through the range of children, and they each
    // completely fit, so return the returnedHeight
    return returnedHeight;
}

当Adapter不为空,这时startPosition是0,而endPosition是count-1。
再往下看,发现会遍历拿到所有的child,并通过measureScrapChild函数执行它们的measure函数。
并且将这些child的高度累加起来,同时还会加上divider的高度。

这里就与GridView有所不同了。GridView只用了第一个child去做乘积,而ListView则用到了所有child。所以当SpecMode不是AT_MOST时,ListView之后提前调用一次getView,position 是0。但是如果SpecMode是AT_MOST时,ListView先调用一次position为0的getView,然后再遍历调用一遍所有的getView,如果算上添加布局时的调用,第一个child的getView就会被调用三次!


Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

猜你喜欢

转载自blog.csdn.net/chzphoenix/article/details/80015067