Android listView的缓存机制RecycleBin

处理ListView过程中用到的缓存

几个重要的集合

mActiveViews new View[0];可见view的数组
mScrapViewsArrayList<View>[];不可见view的数组集合,根据不同的viewType对应的一个数组
mCurrentScrapArrayList<View>;viewTYpe为1的集合或者mScrapViews的第一元素

初始化重要的几个集合

        在调用setAdapter的时候,首先会调用setViewTypeCount来初始化上述几个重要的集合

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //根据viewType初始化mScrapViews
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
           //mScrapViews第一个元素
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }

ListView绘制之显示   

       ListView最终继承View,那么在View绘制的过程分三步:onMeasure测量view的大小,onLayout确定view的布局,onDraw将view绘制到界面上。其中ListView中重点在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;

        final int childCount = getChildCount();
        if (changed) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }
      //重点来了,进行子元素的绘制,实现过程是在ListView中

        layoutChildren();

        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

        // TODO: Move somewhere sane. This doesn't belong in onLayout().
        if (mFastScroll != null) {
            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
        }
        mInLayout = false;
    }

其中重点在于layoutChildren(),主要实现在ListView中,实现子元素的绘制,下面重点介绍。另外我们知道View从初始化到显示到界面,都要经历两次onMeasure和onLayout,那么我们就分别介绍下这两次,layoutChildern都经历了些什么

1、第一次layout

1)layoutChildern()

      此时ListView中没有任何子view,childCount = 0,并且mActiveViews和mScrapViews中也没有缓存任何view, 会根据mLayoutMode来决定执行第一次layout。默认的为LAYOUT_NORMAL,并且事件是从上往下,所以走到第一if,调用fillFromTop()来执行下面的逻辑。

@Override
    protected void layoutChildren() {
         ...... //省略代码
        try {
            ...... //省略代码

            final int childCount = getChildCount();
  ...... //省略代码
          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 {
                    ...... //省略代码
                }
                break;
            }
            // Flush any cached views that did not get reused above
            recycleBin.scrapActiveViews();   
          ...... //省略代码
      
        } finally {
         ...... //省略代码
        }
    }

2)fillFromTop()    

进入到fillFromTop(),继续跟进到fillDown(),从fillDown()中可以看到最终调用到 makeAndAddView()来返回对应的View,那就直接进入到makeAndAddView()方法。

3)makeAndAddView()

在makeAndAddView()中,我们可以看到首先通过mDataChanged去看是否数据发生变化来决定下面的流程。此时因为第一次layout,所以此时该条件成立

然后去获取activeView,但是由于第一次layout,此时获取的肯定为null。所以执行obtainView()来获取该位置下的view

 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
       //数据没有发生变化
        if (!mDataChanged) {
            // 该缓存过activeView,则视图加载这个位置的view
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                // Found it. We're reusing an existing child, so it just needs
                // to be positioned like a scrap view.
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }
        // 通过下面的方法返回viw
        final View child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured.
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

4)obtainView()       

      那直接进入到父类AbsListView下的obtainView()看下里面的逻辑:

      首先获取transientView,这个跟我们讨论的无关,暂时忽略;

      接着就是去获取缓存过的scapeView,由于第一次layout,此时获取的肯定为null;

 View obtainView(int position, boolean[] outMetadata) {
         ......//省略代码
        // 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) {
           ......//省略代码
            return transientView;
        }
        //获取缓存过的scrapView
        final View scrapView = mRecycler.getScrapView(position);
        //根据scrapView通过getView方法创建view
        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 if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            }
        }
       // 下面的代码主要就是设置child的背景色(就是平时我们设置的ListView的这个属性mCacheColorHint)和布局参数(LayoutParams)
       ......//省略代码

        return child;
    }

那么就根据这个scapeView为null的传入adapter.getView创建对应的child。此时就调用到了我们自定义的Adapter的getView方法,也就是传入到getView中的convertView为null,按照常规的写法,此时convertView为null,则通过布局文件创建出该View。所以我们可以看到如果scapeView不为null,即我们缓存过scapeView时,此时传入的convertView不为null,也就会走我们在Adapter定义的直接从该convertView中取出将holder设置成tag的holder。

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder;
		if (convertView == null) {
			convertView = mInflater.inflate(onBindLayout(), parent, false);
			holder = new ViewHolder();
			convertView.setTag(holder);
		} else {
			holder = (ViewHolder) convertView.getTag();
		}
		......//省略代码
		return convertView;
	}

那么继续往下执行scapeView为null,剩下的代码就是设置该view的背景色和布局参数,所以则直接将创建的getView方法返回。

5)返回到makeAndAddView()看下面的代码

 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

这里主要知道就是调用 addViewInLayout将4中获得的view添加到ListView中。

通过上述的5步就已经完成了第一次Layout,那么紧接着的第二次Layout来了哟


2、第二次Layout

第二次的Layout仍然会继续从layoutChildern()开始。但相比较于第一次childCount不为0,那么相应的下面的逻辑也就发生变化了。

1)layoutChildern

此时childCount不为0,走到缓存view的过程,此时数据并没有发生变化,所以就将当前的view加入到mActiveViews中。是不是疑问这里缓存当前view的用处何在呢?其实下面的一行代码就清晰了。

@Override
    protected void layoutChildren() {
         ...... //省略代码

        try {
            ...... //省略代码

            final int childCount = getChildCount();      
           ...... //省略代码
       
            // 根据数据是否发生变化,将view保存到对应的集合中,此时数据并没有发生变化
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
             ...... //省略代码
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            //将之前添加的view从ListView中移除
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
          ...... //省略代码
            default:
                if (childCount == 0) {
       ...... //省略代码
                } 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();
   
          ...... //省略代码
      
        } finally {
         ...... //省略代码
        }
    }

 detachAllViewsFromParent();会将之前加入的view都移除,为了防止在渲染的时候,view重复,那么在后面在加载的时候会不会还需要重新通过布局文件来inflater进来呢?其实这就是mActiveViews这个集合的作用,就是不需要在重新inflater,直接将缓存到mActiveViews这个里面的view取出即可。

  //将之前添加的view从ListView中移除
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

接着还说根据mLayoutMode为LAYOUT_NORMA,并且childCount不为0,走到else之中,我们可以看到最终调用到fillSpecific()方法。

2)fillSpecific()

会创建出当前position的view,然后根据mStackFromBottom分别创建出前一个和后一个的view,最后根据不同的情况返回对应的位置的view,最终还是调用到makeAndAddView()来创建该view

 private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
//首先是当前position的view
        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
        // Possibly changed again in fillUp if we add rows above this one.
        mFirstPosition = position;

        View above;
        View below;
//然后加载该view下的前一个view和后一个view
        final int dividerHeight = mDividerHeight;
        if (!mStackFromBottom) {
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            // This will correct for the top of the first view not touching the top of the list
            adjustViewsUpOrDown();
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                correctTooHigh(childCount);
            }
        } else {
            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
            // This will correct for the bottom of the last view not touching the bottom of the list
            adjustViewsUpOrDown();
            above = fillUp(position - 1, temp.getTop() - dividerHeight);
            int childCount = getChildCount();
            if (childCount > 0) {
                 correctTooLow(childCount);
            }
        }
//最后根据不同的情况返回对应的view
        if (tempIsSelected) {
            return temp;
        } else if (above != null) {
            return above;
        } else {
            return below;
        }
    }

3)makeAndAddView()

这次调用的makeAndAddView()和第一次layout已经不一样了,此时可以取到对应位置的activeView,直接通过setupChild()将activeView加到ListView中

 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
       //数据没有发生变化
        if (!mDataChanged) {
            // 该缓存过activeView,则视图加载这个位置的view
            final View activeView = mRecycler.getActiveView(position);
            if (activeView != null) {
                // Found it. We're reusing an existing child, so it just needs
                // to be positioned like a scrap view.
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }
      ......//省略代码
    }

此时经过上述的 第一次和第二次Layout,该ListView第一屏的数据已经显示出来了。我们也可以将mActiveView理解成ListView的一个缓存,就是在在第二次layout时候,临时保存下当前显示的view,方便最后在显示的时候,可以直接把之前创建的view显示出来

ListView滑动过程中缓存机制

当用户进行滑动显示ListView的更多数据的时候,这个地方就涉及到了ListView的另外一个缓存数组mScrapViews。这里当然要去看下ListView的onTouchEvent()事件。

我们主要看下在滑动的过程中,当前的view是怎么加入到RecycleBin中,新的View是怎么创建的

1)onTouchEvent()

在onTouchEvent()中看下ACTION_MOVE事件,跟踪到onTouchMove()事件中

2)onTouchMove()

若数据源发生变化的时候,则重新去layoutChildern,此时我们先不去考虑数据源发生变化的情况。手指在界面上滑动相当于相当于TouchMode是TOUCH_MODE_SCROLL,跟踪代码进入到scrollIfNeeded()中。

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {       
        ......//省略代码
        //如果数据源发生变化,则调用该方法
        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }
        ......//省略代码
        switch (mTouchMode) {    
         ......//省略代码
            case TOUCH_MODE_OVERSCROLL:
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }

3)scrollIfNeeded()

该方法在手指滑动的时候就会调用到,所以这个方法会被调用多次。根据匹配 TOUCH_MODE_SCROLL该模式,进入到第一个if条件中。只要有滑动,incrementalDeltaY这个值就不为0,那么就会调用到trackMotionScroll()中。

 private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        ......//省略代码
//匹配改种模式
        if (mTouchMode == TOUCH_MODE_SCROLL) {
             ......//省略代码

                // 只要incrementalDeltaY有滑动,则这个就有值,则调用到下面的这个方法里面
                boolean atEdge = false;
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
                }

             ......//省略代码
            }
        }
}

4)trackMotionScroll()

首先会根据down滑动的方向,根据该子view的bottom如果已经小于top,说明该子view已经从顶部移除屏幕,然后将该view到mScrapViews中,若是从下往上,则从0开始加进去,如果是从上往下,则从最后一个开始加入;

然后调用detachViewsFromParent,将所有的缓存后的view从屏幕中移除;

通过offsetChildrenTopAndBottom()将listview中所有的子view进行偏移 ;

接下来的代码就是判断是不是所有的第一屏的view已经移除屏幕,如果已经移除,则调用fillGap()来加载第二屏的数据。那进入到fillGap()方法中看看里面的逻辑。

  boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
......//省略代码
//根据down是从下往上滑,进入到下面流程中

        if (down) {
......//省略代码
            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) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        } else {
......//省略代码
         }
......//省略代码
       if (count > 0) {
            detachViewsFromParent(start, count);
            mRecycler.removeSkippedScrap();
        }
......//省略代码
     offsetChildrenTopAndBottom(incrementalDeltaY);
......//省略代码
       if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
            fillGap(down);
        }
......//省略代码

}

5)fillGap() 

最终实现在ListView中,显然就是根据down来确定调用fillDown()/fillUp(),那么最终调用到了makeAndAddView()方法。

6)makeAndAddView()

此时和前面的第一次layout的makeAndAddView()判断逻辑是一样的,此时的activeView是null,该集合里面的元素是在什么时候清空的呢?当然就是在getActiveView()方法里面,获得当前position的view之后,接着就将该position 下的view置为null。同理mScapeView也是一样的。

所以仍然回到obtainView()中获取view。

7)obtainView()

 View obtainView(int position, boolean[] outMetadata) {
         ......//省略代码
      
        //获取缓存过的scrapView
        final View scrapView = mRecycler.getScrapView(position);
        //根据scrapView通过getView方法创建view
        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 if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            }
        }
        ......//省略代码

        return child;
    }

此时获取到scrapView不为null,那么传入到getView()中获取对应的child,此时这里就是我们在getView()中的convertView不为null的情况,通常做法就是从convertView中取出对应的tag,然后下面就是对holder中的控件进行赋值即可。

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder;
		if (convertView == null) {
			convertView = mInflater.inflate(onBindLayout(), parent, false);
			holder = new ViewHolder();
			convertView.setTag(holder);
		} else {
			holder = (ViewHolder) convertView.getTag();
		}
		......//省略代码
		return convertView;
	}

那么继续往下执行scapeView为null,剩下的代码就是设置该view的背景色和布局参数,所以将getView()得到的child返回。

8)返回到makeAndAddView()看下面的代码

 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

在setupChild()主要知道就是调用 addViewInLayout将4中获得的view添加到ListView中。

当数据源发生变化时

1)layoutChildern()

相比较于两次layout,其中就是dataChange为true,会将View加载到mScapView中,剩下的逻辑同第二次layout。那么就进入到fillSpecific()方法

@Override
    protected void layoutChildren() {
         ...... //省略代码

        try {
            ...... //省略代码

            final int childCount = getChildCount();      
           ...... //省略代码
       
            // 根据数据是否发生变化,将view保存到对应的集合中,此时数据并没有发生变化
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;
            if (dataChanged) {
                 for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                }
            } else {
                  ...... //省略代码
            }

            //将之前添加的view从ListView中移除
            detachAllViewsFromParent();
            recycleBin.removeSkippedScrap();

            switch (mLayoutMode) {
          ...... //省略代码
            default:
                if (childCount == 0) {
       ...... //省略代码
                } 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();
   
          ...... //省略代码
      
        } finally {
         ...... //省略代码
        }
    }

2)fillSpecific()

同第二次layout,那么进入到makeAndAddView()

3)makeAndAddView()

现在mDataChanged已经为true,所以进入到obtainView()

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
            ......// 省略代码
        }
        // Make a new view for this position, or convert an unused view if
        // possible.
        final View child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured.
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

4)obtainView()

同ListView滑动过程中中的obtainView()。所以不在多余解释。

总结

通过上面的几个过程的源码跟踪,我们可以大体了解ListView中缓存机制的处理过程:

1)在初始化显示第一屏的数据的时候,经过两次layout,第一次layout主要就是通过adapter的getView()中的inflater的布局文件创建出该item的view;在第二次layout的时候,首先会将之前创建的view放到mActiveView中暂时缓存下,然后将所有的View移除,在将mActiveView中的view加载到ListView中,这样第一屏的数据就显示出来了。注意当从mActiveView取出view的时候,也会将该集合中的view给清空。

2)当向上滑动时,则首先会将移除屏幕的view缓存到mScapView中,在滑动的过程中,依次又会将mScapView取出View加载到ListView中

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值