ListView缓存机制的实现

     由于ListView使用懒加载的机制,只加载当前屏幕中可见条目的视图,处于不可见的条目是不会被加载的。在动态滑动过程中,屏幕的

可见元素不断的发生变化,需要不断的创建需要显示在当前屏幕中的条目元素,而通常创建条目view集合的方法为inflate xml文件,这是

一个比较耗时的操作。所以谷歌的工程师使用了条目convertView的缓存机制,缓存机制的实现类为RecycleBin,它是作为AbsListView的内

部类存在的。分析完RecycleBin,我们基本上就掌握了缓存机制的原理。

    

      一.二级缓存池的初始化

 

      public void setViewTypeCount(int viewTypeCount) {

            if (viewTypeCount < 1) {

                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");

            }

            //noinspection unchecked

            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];

            for (int i = 0; i < viewTypeCount; i++) {

                scrapViews[i] = new ArrayList<View>();

            }

            mViewTypeCount = viewTypeCount;

            mCurrentScrap = scrapViews[0];

            mScrapViews = scrapViews;

       }

 

     这个方法为初始化缓存池。

     缓存池其实就是一个ArrayList<View>。

     scrapViews 缓冲池集合,这是一个数组,数组的每一个元素为ArrayList<View>。所以缓存池不只一个,缓存池的个数为viewTypeCount

     这个方法只有一个地方调用,即ListView的setAdapter方法中,如下:

 

     public void setAdapter(ListAdapter adapter) {

          ............

          mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

     }

 

     缓存池集合在整个adapter的生命周期中size是不会改变的,adapter.notifyDatasetChanged不会改变缓存池集合的size.

     mAdapter.getViewTypeCount()的返回值必须唯一,如果在setAdaper中返回值为2,adapter.notifyDatasetChanged之后返回值改为3,

     这时数据确实发生了改变,则必然会抛出数组下标越界异常,具体抛出在layoutChildren()方法中的

     recycleBin.addScrapView(getChildAt(i));,因为add缓冲池方法不会检查数组角标。

 

 

     二.二级缓存池的清理

     void clear() {

            if (mViewTypeCount == 1) {

                final ArrayList<View> scrap = mCurrentScrap;

                final int scrapCount = scrap.size();

                for (int i = 0; i < scrapCount; i++) {

                    removeDetachedView(scrap.remove(scrapCount - 1 - i), false);

                }

            } else {

                final int typeCount = mViewTypeCount;

                for (int i = 0; i < typeCount; i++) {

                    final ArrayList<View> scrap = mScrapViews[i];

                    final int scrapCount = scrap.size();

                    for (int j = 0; j < scrapCount; j++) {

                        removeDetachedView(scrap.remove(scrapCount - 1 - j), false);

                    }

                }

            }

      }

     

      清空缓存池集合

      默认情况下,mViewTypeCount为1,缓冲池为mCurrentScrap。如果重写了adapter的getViewTypeCount,则所有缓存池都会清空。

 

 

      三.从二级缓存池查找元素

      View getScrapView(int position) {

            ArrayList<View> scrapViews;

            if (mViewTypeCount == 1) {

                scrapViews = mCurrentScrap;

                int size = scrapViews.size();

                if (size > 0) {

                    return scrapViews.remove(size - 1);

                } else {

                    return null;

                }

            } else {

                int whichScrap = mAdapter.getItemViewType(position);

                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {

                    scrapViews = mScrapViews[whichScrap];

                    int size = scrapViews.size();

                    if (size > 0) {

                        return scrapViews.remove(size - 1);

                    }

                }

            }

            return null;

        }

 

     根据position从缓存池查找对应的缓存itemView。查找时先找缓存池,然后再从缓存池中取出最后一个元素。

     查找对应的缓存池是根据scrapViews数组的下标直接映射的。下标是通过mAdapter.getItemViewType(position)

     取得的。所以重写getItemViewType方法时, type的返回值必须在[0.....getViewTypeCount]之间,其他的type

     值将导致某些类型的条目不被复用,因为type的值不在数组下标范围内,不能取出相应的缓存池。

     

 

     四.添加元素到二级缓存池

     void addScrapView(View scrap) {

            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();

            if (lp == null) {

                return;

            }

 

            // Don't put header or footer views or views that should be ignored

            // into the scrap heap

            int viewType = lp.viewType;

            if (!shouldRecycleViewType(viewType)) {

                return;

            }

 

            if (mViewTypeCount == 1) {

                mCurrentScrap.add(scrap);

            } else {

                mScrapViews[viewType].add(scrap);

            }

 

            if (mRecyclerListener != null) {

                mRecyclerListener.onMovedToScrapHeap(scrap);

            }

     }

 

     将某一个条目添加到缓存池中。

     添加时先找viewType,在找到对应的缓存池添加。

     这个地方没有做数组下标越界检查,如果type值大于缓存池集合的大小,则会抛出异常。

 

 

     五.一级缓存池分析

 

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

                // Don't put header or footer views into the scrap heap

                if (lp != null && lp.viewType != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {

                    // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.

                    //        However, we will NOT place them into scrap views.

                    activeViews[i] = child;

                }

            }

        }

       当前屏幕中的视图缓存集合。

       mActiveViews为view的数组,只保存当前屏幕中的视图条目。

       这个方法在layoutChildren中(dataChanged == false)时调用。recycleBin.fillActiveViews(childCount, firstPosition);

       如果adaper中的数据没有发生改变,则ListView在layout时会保存所有当前屏幕中的元素。以备下次界面更新时使用。

 

       View getActiveView(int position) {

            int index = position - mFirstActivePosition;

            final View[] activeViews = mActiveViews;

            if (index >=0 && index < activeViews.length) {

                final View match = activeViews[index];

                activeViews[index] = null;

                return match;

            }

            return null;

        }

       

       获取当前屏幕中的view视图。如果较上一次比较,adapter中数据没有发生改变,在创建itemView时,会优先从activeViews池中找

       是否存在已有的条目view,如果有则直接使用,连setData的过程都没有了。如下:

        if (!mDataChanged) {

            // Try to use an exsiting view for this position

            child = mRecycler.getActiveView(position);

            if (child != null) {

                if (ViewDebug.TRACE_RECYCLER) {

                    ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,

                            position, getChildCount());

                }

 

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

 

       由于setupChild方法没有调用adaper.getview方法,所以这种情况下根本就没有调用getview的重写方法。

       obtainVew(position);这种方法生成的view是调用getView方法的。

 

总结:

        1.ListView存在两个缓存池系统,一级缓存复用时不需要调用getView,二级缓存需要调用getView.

 

        2.ListView的缓存并不是像某些博客上面写的循环利用图那样简单,向下滑动时,上面的Item被下面的再次使用,这是一种误导。

          实际情况是一个缓存堆栈,后进先出,上面消失的item不一定就是被下面的再次使用了,下面要使用的只是缓存堆栈的最后一个,

          和上面消失的没有直接联系。

 

        3.ListView的二级缓存可以缓存不同类别的View。即使某个View被滑出屏幕很远了,回来时仍然可以复用。

posted @ 2016-06-03 16:28  dap  阅读(1376)  评论(0)    收藏  举报