一.前言
ListView 作为一个 Android 5.x 之前的一个用于显示数据列表的控件,或许在今天都已经被 RecyclerView 完全替代,但是其中的缓存机制仍然值得我们去了解,对后面学习 RecyclerView 的缓存机制有很大的帮助。
下面将根据 ListView 的三个过程彻底理解其缓存机制
- OnLayout 过程,这个过程实践上有两次,而且两次是有区别的。
 - 滑动一个 Item ,即最上面的一个 item 移除屏幕,屏幕下面出现后一个 item 。
 - 滑动一个以上的item .
 
二.RecycleBin机制
ListView 的缓存实际上都是由 RecyclerBin 类完成的,这是 ListView 的父类 AbsListView 的一个内部类,它也是 GridView 的一个父类,说明 ListView 的缓存和 GridView 的缓存实际上有很多相似的地方。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class 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 数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14void 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。
1
2
3
4
5
6
7
8
9
10View 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 中。
1
2
3
4
5
6
7
8
9void addScrapView(View scrap, int position) {
...
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
...
}getScrapView(),用于从废弃缓存中取出一个 ItemView,如果只有一个类型就直接从 mCurrentScrap 当中获取尾部的一个 view 进行返回,同样取出后就直接移除元素。
1
2
3
4
5
6
7
8
9
10
11
12View 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 个 | 
1  | @Override  | 
1  | 
  | 
1  | switch 里面  | 
因为是第一次 OnLayout ,因此有效的操作实际上就到  fillFromTop 这个方法1
2
3
4
5
6
7
8private 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 有个具体的循环。1
2
3
4
5
6
7
8
9
10
11
12
13
14private 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 。重点关注一下 makeAndView1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private 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;
}
1  | View obtainView(int position, boolean[] outMetadata) {  | 
我们可以知道 mAdapter.getView 方法就是 BaseAdapter 中的 getView 方法。1
2
3
4
5
6
7
8
9
10@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 ++ .1
2
3
4
5
6
7
8private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;
        ...
            children[index] = child;
            mChildrenCount++;
           ...
    }

(2)第二次 OnLayout
经过一次 OnLayout 后之前的三个变量变化如下:
| 变量                                                     | 数量           |
| ——————————————————– | ————– |
| mActiveViews (表示屏幕可见的itemView )                   | 0 个           |
| mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) | 0个            |
| getChildCount()/childCount                               | 占满屏幕的数量 |
首先还是还是 从 layoutChildren 开始1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27@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,但是最后还是回到 makeAndaddView1
2
3
4
5
6private View fillSpecific(int position, int top) {
        boolean tempIsSelected = position == mSelectedPosition;
        View temp = makeAndAddView(position, top, true, mListPadding.left, 
        ....
        ....
    }
1  | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,  | 
因为 ActiveView 不为 null 了,所以这里就将之前保存的 每个 itemView 重新添加到 ListView ViewGroup . 而且 ActiveView 每次get 都会进行删除。
这样三个变量的结果就为
| 变量                                                     | 数量 |
| ——————————————————– | —- |
| mActiveViews (表示屏幕可见的itemView )                   | 0 个 |
| mCurrentScrap/mScrapViews[0] ((表示废弃移除的itemView )) | 0个  |
| getChildCount()/childCount                               | n 个 |

四.滑动一个 item
因为是 滑动所以肯定在 onTouchEvent 的 MOVE 里面1
2
3
4
5
6
7
8
9
10@Override
  public boolean onTouchEvent(MotionEvent ev) {
   
      ....
      switch (actionMasked) {
           ....
          case MotionEvent.ACTION_MOVE: {
              onTouchMove(ev, vtev);
              break;
          }
1  | private void onTouchMove(MotionEvent ev, MotionEvent vtev) {  | 
在 layoutChildren 最后又会回到 makeAndAddView1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private 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 最后还会调用1
2
3
4
5
6
7
8
9
10
11
12for (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 个
五.继续滑动
1  | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,  | 
1  | final View scrapView = mRecycler.getScrapView(position);  | 
上面的过程实际上就是将之前移除屏幕的 itemView 重新获取并设置到 mAdapter.getView 中,这也是 我们在写 getView 方法的时候需要对 convertView 进行判断,因为这样就可以利用 ListView 的缓存机制,不用重新进行 LayoutInflate 。
最后:
- 一个 ListView 共创建的 itemView 数就是屏幕显示的 数量+1 ,这个原因在滑动一个 item 的时候就说明。
 
为了证明这个说法,最后做一下验证。