ListView的RecycleBin机制-结合源码分析

由于这些东西比较容易忘记,记录一下,方便以后查看

ListView能够展示成百上千条数据都不会OOM,是因为使用了RecycleBin机制来复用View,所以Listview内部添加的View也就屏幕那几个

当使用ListView.setAdapter()给它设置数据适配器的时候,就会调用requestLayout()来绘制出需要显示的内容.

重点是在onLayout()这个过程,所以就从这里的源码看起了,onLayout在ListView的父类AbsListView中实现

ListView至少会调用两次onLayout

第一次onLayout
 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mInLayout = true;
        final int childCount = getChildCount();
        if (changed) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }
         //重点代码在这里面
        layoutChildren();
        mInLayout = false;
        ...
        }
    }

onLayout会调用layoutChildren()来实现,AbsListView的layoutChildren是空方法,所以看ListView的该方法实现.

@Override
    protected void layoutChildren() {
        ...
        try {
            super.layoutChildren();
            invalidate();
            
            ...
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            //移除所有的view,下面会重新添加,避免重复添加相同的view
            detachAllViewsFromParent(); 
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
            ...
            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;
            }

           ...
    }

删除了很多代码,这里只看一些重点代码,看多了犯困,一开始会判断dataChanged.默认是false,当数据改变的时候的true,所以会调用recycleBin.fillActiveViews(childCount, firstPosition)方法
recycleBin.fillActiveViews()

void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;

            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    activeViews[i] = child;
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }

这个时候childCount是0 ,所以这个方法暂时不用管,退出方法,返回到layoutChildren()方法当中继续,接下来会switch (mLayoutMode),判断一下,会进入default里面,这个时候childCount == 0,然后发现里面还有一个判断mStackFromBottom,并且会调用fillFromTop或者fillUp方法,这两个方法都是去创建ListView的子View,并且显示出来

这里会调用fillFromTop(),fillFromTop方法再调用fillDown()方法

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;
        }
        //是否超过了ListView的高度,或者超过item数量了
        while (nextTop < end && pos < mItemCount) {
            // 判断是否是选择的position
            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;
    }

这里有个while 循环,调用makeAndAddView创建ListView的子View

看下makeAndAddView()

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            child = mRecycler.getActiveView(position);
            //如果有可用的view,直接添加并且返回
            if (child != null) {
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        //这里必须返回一个view 
        child = obtainView(position, mIsScrap);
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

先是进入mRecycler.getActiveView(position) 尝试从可用view数组mActiveViews里面获取,这个数组是在上面fillActiveViews()方法里面赋值的,因为刚刚调用fillActiveViews的时候childCount==0,所以这里是null,然后调用obtainView()方法,得到子View

obtainView() 在父类AbsListView实现

View obtainView(int position, boolean[] isScrap) {

        ...
        final View scrapView = mRecycler.getScrapView(position);
        //调用mAdapter.getView方法
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }
            }
        }
        ...
        return child;
    }

先调用mRecycler.getScrapView从废弃的View缓存中拿到scrapView,这个时候是null,然后就到了我们平时熟悉的地方mAdapter.getView(...)方法来创建view,并且返回出去,调用setupChild添加到 ListView里面

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView==null){
            convertView = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
            viewHolder = new ViewHolder();
            viewHolder.textView1=(TextView) convertView.findViewById(R.id.textView1);
            convertView.setTag(viewHolder);
        }
        else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.textView1.setText(data.get(position).getContent);

        return convertView;
    }

所以我们平时要判断if(convertView==null)来判inflate断创建还是复用

小结:

private View fillDown(int pos, int nextTop) {
        //是否超过了ListView的高度,或者超过item数量了
        while (nextTop < end && pos < mItemCount) {
            // 判断是否是选择的position
            boolean selected = pos == mSelectedPosition;
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
           ...
        return selectedView;
    }

就算Adapter有很多数据,在fillDown()方法中,有个while循环判断while (nextTop < end && pos < mItemCount),限制着ListView一次只会创建屏幕能显示的子View个数,保证ListView中的内容能够迅速展示到屏幕上

第一次Layout过程结束

第二次onLayout

第二次Layout和第一次Layout的基本流程是差不多的,从layoutChildren()方法开始看起:

@Override
    protected void layoutChildren() {
        ...
        try {
            super.layoutChildren();
            invalidate();
            
            ...
            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            //移除所有的view,下面会重新添加,避免重复添加相同的view
            detachAllViewsFromParent(); 
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
            ...
            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;
            }

           ...
    }

同样的会调用recycleBin.fillActiveViews(childCount, firstPosition)方法:

void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;
            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    activeViews[i] = child;
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }

这里和上面第一次不一样了,这个时候childCount是有数量的,所以会把ListView的子View缓存到mActiveViews[ ] 数组里面,后面将会用到.

然后会调用一个非常重要的方法detachAllViewsFromParent(),这个方法会把 所有ListView中的子View全部清除,保证第二次Layout过程不会产生一份重复的数据.

然后同样的进入到switchdefault里面,但是这次childCount不为0了,进入else然后调用fillSpecific()方法,然后fillSpecific()里面其实还是调用fillUp()或者fillDown()方法,并且最后都是调用makeAndAddView()来实现创建View

所以主要看makeAndAddView()

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            child = mRecycler.getActiveView(position);
            //如果有可用的view,直接添加并且返回
            if (child != null) {
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        //这里必须返回一个view 
        child = obtainView(position, mIsScrap);
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

这里也和第一次不一样了,由于在上面detachAllViewsFromParent清空ListView子View之前,已经把子View缓存到RecycleBin的mActiveViews[ ]数组里面,所以这次child = mRecycler.getActiveView(position);就能直接得到之前的view,然后再次添加并且返回

经历了这样一次detach又attach的过程,ListView中所有的子View又都可以正常显示出来了

那么第二次Layout过程结束

到这里ListViw就能正常的显示一屏幕的数据了,但是还有很多数据没显示出来的怎么办?所以要继续查看滑动事件的源码:

AbsListView的onTouchEvent()
@Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                onTouchMove(ev, vtev);
                break;
            }
            case MotionEvent.ACTION_UP: {
                onTouchUp(ev);
                break;
            }

        ...
        return true;
    }

删除了很多代码,这里只关注ACTION_MOVE事件,所以这里会调用onTouchMove()方法,然后内部调用scrollIfNeeded(),最后会调用到trackMotionScroll()方法,只要在屏幕上稍微有一点点移动,这个方法就会被调用,所以滑动的时候会被调用多次

trackMotionScroll()

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return true;
        }

        ...
        
        //判断滚动方式 上/下
        final boolean down = incrementalDeltaY < 0;
        if (down) {
            int top = -incrementalDeltaY;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                if (child.getBottom() >= top) {
                    break;
                } else {
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        } else {
            int bottom = getHeight() - incrementalDeltaY;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                bottom -= listPadding.bottom;
            }
            for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                if (child.getTop() <= bottom) {
                    break;
                } else {
                    start = i;
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        }
        ...
        //如果计数器 > 0,就删除滚出屏幕的view
        if (count > 0) {
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        }

        //移动ListView的内容
        offsetChildrenTopAndBottom(incrementalDeltaY);

        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }
        
        ...
        
        return false;
    }

deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,所以可以根据incrementalDeltaY的正负来判断往上滚还是往下滚..

这里就拿down为true来看,这里会循环判断ListView的childCount,如果child.getBottom() >= top,就跳过,否则即是子View的bottom比ListView的top还小(也就是手指往上滑的时候,子View也会往上移动,当往上滚出屏幕的时候)就使用到RecycleBin机制的mRecycler.addScrapView(child, position)把这个View添加到弃用的数组里面缓存起来

然后判断if (count > 0)也就是如果有滚出屏幕的view,就调用detachViewsFromParent()把这个view从ListView里面删除掉

然后调用ViewGroupoffsetChildrenTopAndBottom()方法来移动相应的偏移量,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果

ViewGroup的offsetChildrenTopAndBottom()方法

 public void offsetChildrenTopAndBottom(int offset) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        boolean invalidate = false;
        for (int i = 0; i < count; i++) {
            final View v = children[i];
            v.mTop += offset;
            v.mBottom += offset;
            if (v.mRenderNode != null) {
                invalidate = true;
                v.mRenderNode.offsetTopAndBottom(offset);
            }
        }
        if (invalidate) {
            invalidateViewProperty(false, false);
        }
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }

然后调用fillGap(down)方法

void fillGap(boolean down) {
        final int count = getChildCount();
        if (down) {
            int paddingTop = 0;
            ...
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } else {
            int paddingBottom = 0;
            ...
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }

可以看到fillGap()最后还是调用fillDown()或者fillUp()方法来实现,最终还是调用makeAndAddView()方法来实现

再看一下makeAndAddView():

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            child = mRecycler.getActiveView(position);
            //如果有可用的view,直接添加并且返回
            if (child != null) {
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }
        //这里必须返回一个view 
        child = obtainView(position, mIsScrap);
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

由于上面调用过一次mRecycler.getActiveView()方法从mActiveViews[ ]中取出了可用的View,mActiveViews每取出一个View就会把对应的View=null,所以这里拿到的是null,走到obtainView()方法里面

obtainView()

View obtainView(int position, boolean[] isScrap) {

        ...
        final View scrapView = mRecycler.getScrapView(position);
        //调用mAdapter.getView方法
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;
                    child.dispatchFinishTemporaryDetach();
                } else {
                    isScrap[0] = false;
                }
            }
        }
        ...
        return child;
    }

这次调用mRecycler.getScrapView()就能从上面滚出屏幕,被保存在弃用的数组里面的View,来进行重新复用了

getScrapView()

View getActiveView(int position) {
            int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;
            if (index >=0 && index < activeViews.length) {
                final View match = activeViews[index];
                //每取出一个弃用的View,就会把index置为null,所以之后保存一屏幕的数据
                activeViews[index] = null;
                return match;
            }
            return null;
        }

ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加

简单流程图

参考:
https://blog.csdn.net/guolin_blog/article/details/44996879

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容