彻底理解 ListView----缓存机制

2018-11-16  本文已影响27人  海盗的帽子

csdn
个人博客

一.前言

ListView 作为一个 Android 5.x 之前的一个用于显示数据列表的控件,或许在今天都已经被 RecyclerView 完全替代,但是其中的缓存机制仍然值得我们去了解,对后面学习 RecyclerView 的缓存机制有很大的帮助。

下面将根据 ListView 的三个过程彻底理解其缓存机制

二.RecycleBin机制

ListView 的缓存实际上都是由 RecyclerBin 类完成的,这是 ListView 的父类 AbsListView 的一个内部类,它也是 GridView 的一个父类,说明 ListView 的缓存和 GridView 的缓存实际上有很多相似的地方。

    class RecycleBin {
      
        // 第一个可见的 item 的下标
        private int mFirstActivePosition;

       // 表示屏幕上可见的 itemView  
        private View[] mActiveViews = new View[0];
      
        //表示废弃的 itemView ,即屏幕上被移除的 itemView 
        //就会添加到这里, 注意这里是个 数组,
        //数组的每个元素都是 List ,因为 ListView 可能存在多个
        //类型的 item ,因此用不同的 List 进行存储。
        private ArrayList<View>[] mScrapViews;

       // 表示不同类型的 itemView 的数量
        private int mViewTypeCount;

        // 表示mScrapViews 数组中一个元素,默认是 第一个
        private ArrayList<View> mCurrentScrap;
        
        ...
           //下面就是初始化的过程。
            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;

与上面对应的有四个方法,分为两类,

对于可见的 itemView 有两个操作:
 void fillActiveViews(int childCount, int firstActivePosition) {
            ...
            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                
                    activeViews[i] = child;
              
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }

 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;
        }
对于移除屏幕的 itemView 也有两个操作:
  void addScrapView(View scrap, int position) {
               ...
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }
            ...
        }
  View getScrapView(int position) {
            final int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap < 0) {
                return null;
            }
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else if (whichScrap < mScrapViews.length) {
                return retrieveFromScrap(mScrapViews[whichScrap], position);
            }
            return null;
        }

三.OnLayout 过程

一个 View 的绘制的时候至少会进行 2 次 onMeasure、onLayout,原因可参考这篇文章
View为什么会至少进行2次onMeasure、onLayout,那么对于 ListView 这两次过程由于缓存机制的存在,就显得不一样。

(1)第一次 OnLayout

对于 RecyclerBin 中的几个变量,因为还未添加任何 View 所以都为 0.

变量 数量
mActiveViews (表示屏幕可见的itemView ) 0 个
mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) 0个
getChildCount()/childCount 0 个
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        mInLayout = true;
        ...
        //因为是第一次 OnLayout 所以  getChildCount 
        //还是 0 
        final int childCount = getChildCount();
        if (changed) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
            mRecycler.markChildrenDirty();
        }

       //直接进入  layoutChildren
        layoutChildren();
        ....
    }

    @Override
    protected void layoutChildren() {
        ...
        // 因为 childcount 为 0 ,所以这里并没有什么作用
        //但是 在第二次的时候 这里就需要注意
        //现在可以先跳过。
           // 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();
     //
    }
 switch 里面
  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);
                    }
                } 

因为是第一次 OnLayout ,因此有效的操作实际上就到 fillFromTop 这个方法

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

fillFromTop->fillDown 这两个方法就是进行第一次往 ListView 添加 View 。
其中的 fillDown 有个具体的循环。

     private View fillDown(int pos, int nextTop) {
        View selectedView = null;
        ...
        int end = (mBottom - mTop);
        ...
        //进入一个循环
        while (nextTop < end && pos < mItemCount) {
         
            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

       
        }

     }

上面的循环就是 根据屏幕的大下,对 ListView 添加满屏幕的 ItemView 。重点关注一下 makeAndView

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
            // Try to use an existing view for this position.
            //尝试从 getActiveView 获取,但是这个时候为 0
            //所以 activeView 为 null
            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;
            }
        }
        
        //通过 obtainView 获取
        // 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;
    }
   View obtainView(int position, boolean[] outMetadata) {
    ...
     //首先会获取一个 ScrapView 缓存废弃的 itemView ,因为这个时候为 0 
     //所以 会将 null 传到 mAdapter.getView 这个方法中。
     final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
         ...
          return child;
    }

我们可以知道 mAdapter.getView 方法就是 BaseAdapter 中的 getView 方法。

  @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                if (convertView==null){
                    convertView = LayoutInflater.from(Main2Activity.this).inflate(R.layout.item,null);
                }
                TextView textView = convertView.findViewById(R.id.tv_text);
                textView.setText((position + ":对应为" + convertView).replace("android.widget.",""));
                return convertView;
            }
        });

此时 convertView 就是 scrapView ,因为这时为 null ,所以就通过 LayoutInflater 进行加载。这样 obainView 放回一个 加载的 View , 最后回到 setupChild ,在 setupChild 就将 ItemView 添加到 ListViewGoup 并 mChildrenCount ++ .

private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;
        ...
            children[index] = child;
            mChildrenCount++;
           ...
    }
image.png

(2)第二次 OnLayout

经过一次 OnLayout 后之前的三个变量变化如下:

变量 数量
mActiveViews (表示屏幕可见的itemView ) 0 个
mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) 0个
getChildCount()/childCount 占满屏幕的数量

首先还是还是 从 layoutChildren 开始

  @Override
    protected void layoutChildren() {
        ...
        // 因为 childcount 这个时候就有值了 n ,
        //假设为  n
        // 首先判断有没有数据改变  dataChanged 
        // 没有就进入 else 
           // 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 {
               // 这里就将 屏幕上的 itemView 添加到 
                //ActiveViews  中 ,ActiveViews 就是表示屏幕上的 itemView 
                //集合
                recycleBin.fillActiveViews(childCount, firstPosition);
            }
             //然后就将 所有的 View 从 ListView 中先移除
             //这是为了后面操作导致重复添加。
            // Clear out old views
            detachAllViewsFromParent();
     //
    }

上面的逻辑就是将 ListView 中的itemView 添加到 ActiveViews 数组中,然后就先移除,因为保存到了 ActiveViews 中,所以不用担心会重新 LayoutInflate 的问题。

变量 数量
mActiveViews (表示屏幕可见的itemView ) n 个
mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) 0个
getChildCount()/childCount n 个

因为是第二次 onLayout ,所以不会进入 fillTop ->fillDown ,而是进入 fillSpecific,但是最后还是回到 makeAndaddView

private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
        View temp = makeAndAddView(position, top, true, mListPadding.left, 
        ....
        ....
    }
    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
            // Try to use an existing view for this position.
            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;
            }
        }
      ...
    }

因为 ActiveView 不为 null 了,所以这里就将之前保存的 每个 itemView 重新添加到 ListView ViewGroup . 而且 ActiveView 每次get 都会进行删除。
这样三个变量的结果就为

变量 数量
mActiveViews (表示屏幕可见的itemView ) 0 个
mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) 0个
getChildCount()/childCount n 个
image.png

四.滑动一个 item

因为是 滑动所以肯定在 onTouchEvent 的 MOVE 里面

  @Override
    public boolean onTouchEvent(MotionEvent ev) {
     
        ....
        switch (actionMasked) {
             ....
            case MotionEvent.ACTION_MOVE: {
                onTouchMove(ev, vtev);
                break;
            }
  private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
         // 这里又回到 layoutChildren
        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }

在 layoutChildren 最后又会回到 makeAndAddView

   private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        if (!mDataChanged) {
            // Try to use an existing view for this position.
            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;
            }
        }
         // 执行下面的
        // 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;
    }

这里因为 之前的 getActiveView 已经将所有的 item取出,所以还是会通过 obtainView 去加载一个 item.而且在 onTouchMove 最后还会调用

  for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                if (child.getTop() <= bottom) {
                    break;
                } else {
                     ....
                     // 将移除屏幕的 itemView 添加到 ScrapView 
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        }

这个时候那个几个变量的变化为
mActiveViews (表示屏幕可见的itemView )| 0 个
mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) | 1个
getChildCount()/childCount | n+1 个

五.继续滑动

  private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
       
        final View child = obtainView(position, mIsScrap);

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

        return child;
    }
  final View scrapView = mRecycler.getScrapView(position);
        //这个时候和第一次就不同了 因为 这个时候 的 scrapView
        //就不为  null, 因此 scrapView 和 convertView 就不为null.
        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();
            }
        }

上面的过程实际上就是将之前移除屏幕的 itemView 重新获取并设置到 mAdapter.getView 中,这也是 我们在写 getView 方法的时候需要对 convertView 进行判断,因为这样就可以利用 ListView 的缓存机制,不用重新进行 LayoutInflate 。

最后:

为了证明这个说法,最后做一下验证。


image.png
上一篇 下一篇

猜你喜欢

热点阅读