美文网首页
彻底理解 ListView----缓存机制

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

作者: 海盗的帽子 | 来源:发表于2018-11-16 20:54 被阅读27次

    csdn
    个人博客

    一.前言

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

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

    • OnLayout 过程,这个过程实践上有两次,而且两次是有区别的。
    • 滑动一个 Item ,即最上面的一个 item 移除屏幕,屏幕下面出现后一个 item 。
    • 滑动一个以上的item .

    二.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 有两个操作:
    • fillActiveViews ,将屏幕上可见的 itemView 添加到 ActiveViews 数组。
     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;
                    }
                }
            }
    
    
    • getActiveView(), 根据位置取出 ActiveViews 数组 中的 itemView ,并将最对应的数组元素置为 null。
     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 也有两个操作:
    • addScrapView() ,将移除 的 itemView 添加到 mScrapViews/mCurrentScrap 中。
      void addScrapView(View scrap, int position) {
                   ...
                    if (mViewTypeCount == 1) {
                        mCurrentScrap.add(scrap);
                    } else {
                        mScrapViews[viewType].add(scrap);
                    }
                ...
            }
    
    • getScrapView(),用于从废弃缓存中取出一个 ItemView,如果只有一个类型就直接从 mCurrentScrap 当中获取尾部的一个 view 进行返回,同样取出后就直接移除元素。
      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 。

    最后:

    • 一个 ListView 共创建的 itemView 数就是屏幕显示的 数量+1 ,这个原因在滑动一个 item 的时候就说明。

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


    image.png

    相关文章

      网友评论

          本文标题:彻底理解 ListView----缓存机制

          本文链接:https://www.haomeiwen.com/subject/uzdvfqtx.html