listview列表项重用机制与初始化流程分析

本文深入解析了Android中ListView的工作原理及其实现机制,包括ListView的测量过程、布局过程以及convertView和viewholder的重用机制。

现在网上优化listview的内容一大把,重用convertView,viewholder,但是分析listview的却很少,很多人都不清楚为什么这么做能优化listview

或者不清楚listview的重用机制,实现机制。今天特地研究了下源码,开个帖子,记录下。

一:

listview依次继承AbsListView,AdapterView,ViewGroup,View

首先,view第一步都是onMeasure计算高宽,我们来分析下是如何计算宽高的。我们找到listview的onMeasure

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

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



View child = obtainView(0, mIsScrap) 创建了position为0(也就是列表第一行)的列表项,
measureScrapChild(child, 0, widthMeasureSpec) 调用列表项的measure计算宽高
mRecycler.addScrapView(child, 0); 放入RecycleBin中之后复用。
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); 计算listview总高。
--------------------上面是总体逻辑,现在我们进到方法里面看看具体是如何实现的-----------------
obtainView(0, mIsScrap) 这个方法非常重要,是listview实现convertView重用的关键方法。
我们贴下源码:
</pre><pre name="code" class="java">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);
                }
            }


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

<span style="white-space:pre">	</span>    ....

        return child;
    }


此方法里分两个逻辑处理 
a:
View transientView = mRecycler.getTransientStateView(position); 从transient状态中获取是否有缓存的view,</span>
View updatedView = mAdapter.getView(position, transientView, this); 这个方法是不是很惊喜,大家终于知道我们的adapter中的getview方法是从
哪调用的了吧,如果在transient状态中获取到了缓存的view,就会传入到convertView中。
View scrapView = mRecycler.getScrapView(position); 从scrap状态中获取缓存view,
View child = mAdapter.getView(position, scrapView, this);同上,如果获取到了传入adapter的getview中。
OK 讲到这里要说下RecycleBin,RecycleBin就是listview实现列表项重用的实现类了,RecycleBin将列表项分为active跟scrap两类,active里存放当前
可见的列表项,scrap类里放着移出屏幕的废弃列表项。我们继续跟踪mRecycler.getScrapView(position) 看看里面是如何实现。
<span style="font-family: Arial, Helvetica, sans-serif;">View getScrapView(int position) {
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else {
                final int whichScrap = mAdapter.getItemViewType(position);
                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
                    return retrieveFromScrap(mScrapViews[whichScrap], position);
                }
            }
            return null;
        }
</span>
<span style="font-family: Arial, Helvetica, sans-serif;">判断了列表项的类型是一种还是多种,我们先只看一种的情况下,明白了一种的情况,多种TYPE情况下也是一样的。我们跟踪进入retrieveFromScrap方法。</span>
<span style="font-family: Arial, Helvetica, sans-serif;">private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
            final int size = scrapViews.size();
            if (size > 0) {
                // See if we still have a view for this position or ID.
                for (int i = 0; i < size; i++) {
                    final View view = scrapViews.get(i);
                    final AbsListView.LayoutParams params =
                            (AbsListView.LayoutParams) view.getLayoutParams();


                    if (mAdapterHasStableIds) {
                        final long id = mAdapter.getItemId(position);
                        if (id == params.itemId) {
                            return scrapViews.remove(i);
                        }
                    } else if (params.scrappedFromPosition == position) {
                        final View scrap = scrapViews.remove(i);
                        clearAccessibilityFromScrap(scrap);
                        return scrap;
                    }
                }
                final View scrap = scrapViews.remove(size - 1);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            } else {
                return null;
            }
        }
</span>
先遍历了scrap列表,如果有id匹配或者position匹配就返回列表项,如果全都不匹配,View scrap = scrapViews.remove(size - 1);获取scrap列表最后一项返回。
OK,跟到这里 obtainView()方法的逻辑就结束了。接下来我们看listview总高是如何产生的,方法measureHeightOfChildren()
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">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);


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


            if (returnedHeight >= maxHeight) {
                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
                            && (returnedHeight != maxHeight) // i'th child did not fit completely
                        ? prevHeightWithoutPartialChild
                        : maxHeight;
            }


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

        return returnedHeight;
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">重点是这个for循环,循环调用obtainView(i, isScrap) 依次创造列表项,然后measure列表项的宽高,
returnedHeight += child.getMeasuredHeight() 累计列表总高,
if (returnedHeight >= maxHeight)   当高超出maxHeight return 返回高度。
在上面分析了单个列表项的源码分析后,这里理解起来就很简单了。
二:
到现在高宽已经计算好了,就到了onlayout这一步了,我们在listview的父类AbsListView中找到了onlayout方法。
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        ...

        layoutChildren();
        ...
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">没发现有什么重要内容,但是看到layoutChildren()这个,回到listview中找到这个方法,原来重要内容都在这里。
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">protected void layoutChildren() {
        .....

            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }


            // Clear out old views
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();


            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
                break;
            case LAYOUT_MOVE_SELECTION:
                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
                break;
            default:
                if (childCount == 0) {
                    if (!mStackFromBottom) {
                        final int position = lookForSelectablePosition(0, true);
                        setSelectedPositionInt(position);
                        sel = fillFromTop(childrenTop);
                    } else {
                        final int position = lookForSelectablePosition(mItemCount - 1, false);
                        setSelectedPositionInt(position);
                        sel = fillUp(mItemCount - 1, childrenBottom);
                    }
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        sel = fillSpecific(mSelectedPosition,
                                oldSel == null ? childrenTop : oldSel.getTop());
                    } else if (mFirstPosition < mItemCount) {
                        sel = fillSpecific(mFirstPosition,
                                oldFirst == null ? childrenTop : oldFirst.getTop());
                    } else {
                        sel = fillSpecific(0, childrenTop);
                    }
                }
                break;
            }


            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();

            .....
<span style="white-space:pre">	</span>    .....
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">源码中layoutChildren()内容非常多。。好重量级的方法。。。我们又要找重点了。
重点一:
if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }
将当前的所有列表项先放入recycleBin缓存,以备后续重用。
重点二:
判断了mLayoutMode,分别调用fillFromSelection,fillFromMiddle,fillSpecific,fillUp,fillFromTop好多fill方法,但其实这些方法的内部实现机制也是差不多的,
我们挑一个来看,fillFromTop();
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">private View fillFromTop(int nextTop) {
        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
        if (mFirstPosition < 0) {
            mFirstPosition = 0;
        }
        return fillDown(mFirstPosition, nextTop);
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;"><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">fillFromTop()调用了 fillDown();
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">private View fillDown(int pos, int nextTop) {
        View selectedView = null;


        int end = (mBottom - mTop);
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            end -= mListPadding.bottom;
        }


        while (nextTop < end && pos < mItemCount) {
            // is this the selected item?
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);


            nextTop = child.getBottom() + mDividerHeight;
            if (selected) {
                selectedView = child;
            }
            pos++;
        }


        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
        return selectedView;
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">这里是一个while循环,nextTop < end 表示列表总高还能容下列表项,pos < mItemCount 当前position比总数小就不停得产生列表项,我们看这个产生列表项的
方法进去看看child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;

        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);


                return child;
            }
        }


        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);


        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);


        return child;
    }


这里又出现两个分支,
if (!mDataChanged) 如果数据未改变,

child = mRecycler.getActiveView(position); 从recycleBin中的active类中获取缓存view,
否则 child = obtainView(position, mIsScrap); 又是这个obtainview方法,这个方法上面已经讲过获取缓存view的逻辑了。所以看到这里,
基本能明白listview在创建过程中重用view的机制了。
我们接着看,不论上面的哪一个分支,都会走到setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);这个方法中。
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean recycled) {
        ....
<span style="white-space:pre">	</span>....

        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
                p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
            attachViewToParent(child, flowDown ? -1 : 0, p);
        } else {
            p.forceAdd = false;
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                p.recycledHeaderFooter = true;
            }
            addViewInLayout(child, flowDown ? -1 : 0, p, true);
        }

<span style="white-space:pre">	</span>....
 <span style="white-space:pre">	</span>....
    }
</pre><pre name="code" class="java" style="font-family: Arial, Helvetica, sans-serif;">重点是attachViewToParent(child, flowDown ? -1 : 0, p);跟addViewInLayout(child, flowDown ? -1 : 0, p, true);这两个方法。
这两个方法走进去最后都会到Viewgroup的addInArray(child, index);这个方法是将view添加到Viewgroup中去的方法。
这下明白了,原来listview是在onlayout过程中将一条条列表项添加到viewgroup中,listview的本质就是一个viewgroup,里面
一大堆整齐排列的列表项。
最后,listview代码量非常庞大,好几万行,看起来确实有点累 0.0  本人水平有限,文中如有错误,欢迎指正。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值