ListView 缓存解析

Posted by LinYaoTian on 2019-02-03

AbsListView

继承关系

从继承关系中可以看出,GridView 和 ListView 都是继承自 AbsListView,因此两者在工作原理和实现上都是有很多共同点的,而缓存逻辑应为 GridView 和 ListView 共有的,因此缓存应该是在 AbsListView 中实现的。

Adapter 的作用

顾名思义,Adapter 是适配器的意思,它的作用是在 ListView 和数据源中起到一个桥梁的作用,ListView 并不会直接和数据源打交道,而是借助 Adapter 这个桥梁去访问真正的数据源。

Adapter 提供统一的接口让 ListView 不必担心任何适配问题,用户可以继承 Adapter 去实现与特定数据的适配功能。

RecycleBin

ListView 能够实现成百上千条数据都不会出现 OOM ,最重要的一个原因就是 RecycleBin 机制,它是 AsbListView 的一个内部类,GridView 和 ListView 都可以使用它。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    /**
*RecycleBin有助于在布局中重用视图。RecycleBin有两个级别的存储:ActiveViews和ScrapViews。
*ActiveViews是在布局开始时出现在屏幕上的视图。通过构造,它们显示当前信息。
*在布局的最后,ActiveViews中的所有视图都被降级为ScrapViews。
*ScrapViews是可以被适配器使用的旧视图,以避免不必要地分配视图。
*
*/
class RecycleBin {
private RecyclerListener mRecyclerListener;

/**
* 在mActiveViews中存储的第一个View的位置.
*/
private int mFirstActivePosition;

/**
*在布局开始时在屏幕上的视图。这个数组在布局开始时填充,
*在布局的末尾,mActiveViews中的所有视图都被移动到mScrapViews
*mActiveViews表示一个连续的视图范围,第一个视图的位置存储在mFirstActivePosition。
*/
private View[] mActiveViews = new View[0];

/**
*可将适配器用作转换视图的未排序视图。
*/
private ArrayList<View>[] mScrapViews;

private int mViewTypeCount;

private ArrayList<View> mCurrentScrap;

/**
* RecycleBin当中使用mActiveViews这个数组来存储View,
* 调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
*
* @param childCount
* 第一个参数表示要存储的view的数量
* @param firstActivePosition
* ListView中第一个可见元素的position值
*/
void fillActiveViews(int childCount, int firstActivePosition) {

}

/**
*该方法与上面的fillActiveViews对应,功能是获取对应于指定位置的视图。视图如果被发现,就会从mActiveViews删除
*
* @param position
* 表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。
* @return
* 返回找到的View 下次获取同样位置的View将会返回null。
*/
View getActiveView(int position) {

return null;
}

/**
* 根据mViewTypeCount把一个View放进 ScapViews list. 这些View是未经过排序的.
*
* @param scrap 需要被加入的View
* @param position scrap 原来的位置
*/
void addScrapView(View scrap, int position) {

}

/**
* @return 根据mViewTypeCount从mScapViews中获取
*/
View getScrapView(int position) {

return null;
}

/**
* @return 用于从ScapViews list中取出一个View,这些废弃缓存中的View是没有顺序可言的,
* 因此retrieveFromScrap方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrapview进行返 回.
*/
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
if(...){
return scrap;
} else {
return null;
}
}


/**
*Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,
*setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。
*/

public void setViewTypeCount(int viewTypeCount) {

}

}

重要的成员变量:

1
2
3
4
5
6
7
8
9
10
//第一个可见的View存储的位置
private int mFirstActivePosition;
//可见的View数组(作用也就是 Layout 过程防止重复添加数据,在2次 layout 后所有元素都为 null,不会再被使用 )
private View[] mActiveViews = new View[0];
//不可见的的View数组,是一个集合数组,每一种type的item都有一个集合来缓存
private ArrayList<View>[] mScrapViews;
//View的Type的数量
private int mViewTypeCount;
//mScrapViews的第一个元素
private ArrayList<View> mCurrentScrap;

重要方法:

  • fillActiveViews(): 这个方法接收两个参数,第一个参数表示要存储的 View 的数量,第二个参数表示 ListView 中第一个可见元素的 position 值。RecycleBin 当中使用 mActiveViews 这个数组来存储 View,调用这个方法后就会根据传入的参数来将 ListView 中的指定元素存储到mActiveViews 数组当中。
  • getActiveView() : 这个方法和 fillActiveViews() 是对应的,用于从 mActiveViews 数组当中获取数据。该方法接收一个 position 参数,表示元素在 ListView 当中的位置,方法内部会自动将position 值转换成 mActiveViews 数组对应的下标值。需要注意的是,mActiveViews 当中所存储的 View,一旦被获取了之后就会从 mActiveViews 当中移除,下次获取同样位置的 View 将会返回 null,也就是说 mActiveViews 不能被重复利用。
  • addScrapView() :用于将一个废弃的 View 进行缓存,该方法接收一个 View 参数,当有某个View 确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对 View 进行缓存,RecycleBin 当中使用 mScrapViews 和 mCurrentScrap 这两个 List 来存储废弃 View。
  • getScrapView() : 用于从废弃缓存中取出一个 View,这些废弃缓存中的 View 是没有顺序可言的,因此 getScrapView() 方法中的算法也非常简单,就是直接从 mCurrentScrap 当中获取尾部的一个 scrap view 进行返回。
  • setViewTypeCount() : 我们都知道 Adapter 当中可以重写一个 getViewTypeCount() 来表示ListView 中有几种类型的数据项,而 setViewTypeCount() 方法的作用就是为每种类型的数据项都单独启用一个 RecycleBin 缓存机制。实际上,getViewTypeCount() 方法通常情况下使用的并不是很多,所以我们只要知道 RecycleBin 当中有这样一个功能就行了。

Layout 过程

对于任意一个 View,先要显示到界面上,至少需要经过两次 onMeasure() 和 onLayout() 的调用。

对于其他 View ,我们并不关心它测量和布局了几次,因为逻辑都是一样,但是对于 ListView 来说,它是在 layout 过程中添加数据的,如果多次 layout,那么不就重复添加数据了嘛?这里 ListView 做了很好的处理来避免重复添加数据的问题。

第一次 Layout

ListView 中并没有 onLayout() 这个方法,那肯定就是在父类 AbsListView 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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();
}

layoutChildren();
mInLayout = false;

mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

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

复杂逻辑被封装到了 layoutChildren() 中,在 AbsListView 中它是一个空方法,这很正常,因为子元素的布局应该由具体的子类来负责完成,而不是由父类来完成。

ListView 的 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Override
protected void layoutChildren() {

try {

final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
/**
*第1次Layout时,ListView内还没有数据,数据还在Adapter那,这里childCount=0
*/

final int childCount = getChildCount();

int index = 0;
int delta = 0;

......
/**
*dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,
*/
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
......

} else {
/**
*RecycleBin的fillActiveViews()方法缓存View
*可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
*/
recycleBin.fillActiveViews(childCount, firstPosition);
}

/**
*目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
*/
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();

/**
*mLayoutMode默认为LAYOUT_NORMAL
*/
switch (mLayoutMode) {

default:
if (childCount == 0) {
if (!mStackFromBottom) {
/**
*mStackFromBottom我们在上面的属性中讲过,该属性默认为false
*/
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
//调用fillFromTop方法
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}

}
break;
}

/**
*RecycleBin的scrapActiveViews从mActivieViews缓存到mScrapActiveViews
*可是目前RecycleBin的mActivieViews也没什么数据,因此这一行暂时还起不了任何作用。
*/
recycleBin.scrapActiveViews();

......
}
}

第一次布局时,layoutChildrent() 主要要从第 15 行开始看起,调用 fillFromTop() 去从顶部填充 ListView。

跟进 fillFromTop()

1
2
3
4
5
6
7
8
9
10
/**
*参数nextTop表示下一个子View应该放置的位置,
*这里传入的nextTop=mListPadding.top;明显第一个子View是放在ListView的最上方,
*注意padding不属于子View,属于父View的一部分
*/
private View fillFromTop(int nextTop) {
...
//调用fillDown
return fillDown(mFirstPosition, nextTop);
}

跟进 fillDown()

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 从pos开始从上向下填充ListViwe
*
* @param pos
* list中的位置
*
* @param nextTop
* 下一个Item应该放置的位置
*
* @return
* 返回选择位置的View,这个位置在我们的放置范围之内
*/
private View fillDown(int pos, int nextTop) {
View selectedView = null;

int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}

/**
*这里循环判断,终止的条件是下一个item放置的位置>listview的底部或者当前的位置>总数
*/
while (nextTop < end && pos < mItemCount) {

boolean selected = pos == mSelectedPosition;
/**
*这里调用makeAndAddView来获得一个View
*/
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

//更新nextTop的值
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
//自增pos
pos++;
}

setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}

主要看 makeAndAddView() ,它的作用是获取一个 View,并添加到 ListView 中。

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
29
30
31
32
33
34
35
36
37
38
/**
* 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
* @param position
* 列表中的逻辑位置
* @param y
* 被添加View的上 边位置或者下 边位置
* @param flow
* 如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return
* 返回被添加的View
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;


if (!mDataChanged) {
// 尝试从mActiveViews中获取,第1次肯定是没有获取到
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);

return child;
}
}

// 为这个position创建View
child = obtainView(position, mIsScrap);

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

return child;
}

我们第1次 layout 时,尝试从 mActiveViews 中获取 View ,这里 child 肯定为 null,那么我们跟进obtainView() ,在ListView中没有找到该方法,那么应该在其父类中..跟进

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
/**
* 获取一个视图,并让它显示与指定的数据相关联的数据的位置。
*当我们已经发现视图无法在RecycleBin重复使用。剩下的唯一选择就是转换旧视图或创建新视图。
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}

主要的逻辑是:尝试从scrapViews复用View,如果没有,则通过 getView() 创建一个新的 View(也就 Adapter 中的那个 getView()

第一次 layout ,在 scrapViews 中肯定是没有缓存的,因此会创建一个新 View。

从这里可以看出,在 Adapter 的 getView(int position, View convertView, ViewGroup parent) 中,需要判断 convertView 是否为 null,如果为 null,则需要通过 LayoutInflater 加载 View,否则可以复用 View。

回到 makeAndAddView() 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;

......
// view我们这里已经得到了
child = obtainView(position, mIsScrap);

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

return child;
}

跟进 setupChild()

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
/**
* 添加一个子View然后确定该子View的测量(如果必要的话)和合理的位置
*
* @param child
* 被添加的子View
* @param position
* 子View的位置
* @param y
* 子View被放置的坐标
* @param flowDown
* 如果是true的话,那么上面参数中的y表示子View的上 边的位置,否则为下 边的位置
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled 布尔值,意为child是否是从RecycleBin中得到的,如果是的话,不需要重新Measure
* 第1次layout时,该值为false
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled) {
...
if(recycled && ...){
attachViewToParent(child, flowDown ? -1 : 0, p);
}else{
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
...
}

第一次 layout ,View 是通过 LayoutInflater新创建的,因此不可能进行过 recycled,所以会执行 addViewInLayout() 添加 View 到 ListView 中。那么根据 fillDown() 方法中的while循环,会让子元素 View 将整个 ListView 控件填满然后就跳出,也就是说即使我们的 Adapter 中有一千条数据,ListView 也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证 ListView 中的内容能够迅速展示到屏幕上。

至此,第一次 layout 结束。

ListView 的第一次 Layout 中,即使 Adapter 中有几千条数据,也只会填充一个屏幕的 ListView 子 View,而且没有涉及到 RecycleBin 的运作。

第二次 Layout

还是从 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
protected void layoutChildren() {

invalidate();
...
// 缓存 View 到 activeViews 中
recycleBin.fillActiveViews(childCount, firstPosition);

/**
* 第2次Layout时,ListView中已经有了一屏的子View,
* 调用detachAllViewsFromParent();把ListView中的所有子View detach了
* 这也是多次调用layout不会重复添加数据的原因
*/
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();

/**
*mLayoutMode默认为LAYOUT_NORMAL,即会到 Default
*/
switch (mLayoutMode) {
...
default:
if (childCount == 0) {

} else {
/**
* 第2次Layout时childCount不为0
*/
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;
}

/**
*RecycleBin的scrapActiveViews方法把mActivieViews中的View再缓存到mScrapActiveViews
*/
recycleBin.scrapActiveViews();

......
}
}

主要看 2 个操作:

  • detachAllViewsFromParent() :把 ListView 中所有的子 View 清除掉,从而保证了第二次 Layout 过程不会添加重复数据。因为之前已经将 View 缓存到 ActiveViews 中了,因此待会儿用缓存来加载 Item View 会快很多,不会太影响效率。

  • fillSpecific() :其实第二次 Layout 和第一次 Layout 的基本流程是差不多的,第 2 次 Layout 时 childCount 不为0,我们进入了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
    }

    因为我们还没有选中任何一个 View,所以 mSelectedPosition = -1,因此会进入第二个逻辑判断。

跟进 fillSpecific()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/	**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
...
}

它和 fillUp()fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View 先加载到屏幕上,然后再加载该子 View 往上以及往下的其它子 View。那么由于这里我们传入的 position 就是第一个子View 的位置,于是 fillSpecific() 方法的作用就基本上和 fillDown()方法是差不多的了。

继续跟进 makeAndAddView()

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
29
30
31
32
33
34
35
36
/	**
* 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
* @param position
* 列表中的逻辑位置
* @param y
* 被添加View的上 边位置或者下 边位置
* @param flow
* 如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return
* 返回被添加的View
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;


if (!mDataChanged) {
// 尝试从mActiveViews中获取,第2次layout中child可以从mActiveViews中获取到
child = mRecycler.getActiveView(position);
if (child != null) {
//这里child不为空,调用setupChild,注意最后一个参数为true
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}

// 为这个position创建View
child = obtainView(position, mIsScrap);

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

return child;
}

因为在 layoutChildren() 时已经将 ListView 中的 View 缓存到了 mActiveViews 中,因此这里会执行 21 行从缓存中获取 View,然后执行 setupChild() ,方法的最后传了一个 true 参数,表示当前 View 是被回收过的。

跟进 setupChild()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
...
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
....
}

这里的参数 recycled = true,因此会执行 attachViewParent() ,而第一次 Layout 时会执行 addViewInLayout() 。两者的区别是,如果我们需要向 ViewGroup 中添加一个新的 View,应该调用 addViewInLayout() ,而如果想要将之前 detach 的 View 重新 attach 到 ViewGroup 上,就应该调用 attachToParent() 。那么由于前面在 layoutChildren() 方法当中调用了 detachAllViewsFromParent() 方法,这样 ListView 中所有的子 View 都是处于 detach 状态的,所以这里attachViewToParent() 方法是正确的选择。

经历这样一个 detach 又 attach 的过程,ListView 中所有的子 View 又都可以显示出来,第二次 Layout 结束。

也就是说,ListView 的第二次 Layout 过程中,把 ListView 中所有子 View 缓存到 RecycleBin 中的 mActivieViews,然后再 detach 掉 ListView 中所有的子 View,接着又 attach 回来(这时使用的是 mActivieViews 中的缓存,没有重新 Inflate),然后再把 mActivieView 缓存到 mScrapViews 中(getActiveView() 中获取到缓存 View 后,就会把它移除,也就是说 mActiveViews 中的同一个缓存不能获得两次)。

滑动加载更多数据

经过两次 Layout ,虽然我们在 ListView 中已经可以看到内容了,然而关于 ListView 最神奇的部分我们却还没有接触到,因为目前 ListView 中只是加载并显示了第一屏的数据而已。比如说我们的 Adapter 当中有 1000 条数据,但是第一屏只显示了 10 条,ListView 中也只有10个子 View 而已,那么剩下的 990 是怎样工作并显示到界面上的呢?这就要看一下 ListView 滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。

由于滑动部分的机制是属于通用型的,即 ListView 和 GridView 都会使用同样的机制,因此这部分代码就肯定是写在 AbsListView当中的了。那么监听触控事件是在 onTouchEvent() 方法当中进行的,我们就来看一下 AbsListView 中的这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (actionMasked) {
......

case MotionEvent.ACTION_MOVE: {
...
trackMotionScroll(deltaY, incrementalDeltaY);
break;
}
......
}

......
return true;
}

手指在 ListView 中滑动时,TouchMode 一直等于 TOUCH_MODE_SCROLL 这个值,相应 case 中我们主要要看trackMotionScroll() ,因为滑动过程中的复杂过程都是在这个方法中处理的。

跟进 trackMotionScroll()

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/ **
* @param incrementalDeltaY 代表滑动方向,大于 0 代表用户向上滑动,反之则反之
*/
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int top = listPadding.top - incrementalDeltaY;
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) {
// 添加到缓存中
mRecycler.addScrapView(child);
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
}
...
if (count > 0) {
// 把所有移出屏幕的子View全部detach掉
detachViewsFromParent(start, count);
}
// 让Item偏移指定距离,实现了 ListView 的滚动
offsetChildrenTopAndBottom(incrementalDeltaY);
...
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
// 加载屏幕外的 Item
fillGap(down);
}

return false;
}

可以看到,从第 10 行开始,当 ListView 向下滑动的时候,就会进入一个 for 循环当中,从上往下依次获取子 View,第 29 行当中,如果该子 View 的 bottom 值已经小于 top 值了,就说明这个子 View 已经移出屏幕了,所以会调用 RecycleBin 的addScrapView() 方法将这个 View 加入到废弃缓存当中,并将 count 计数器加1,计数器用于记录有多少个子 View 被移出了屏幕。那么如果是 ListView 向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子 View,然后判断该子 View的 top 值是不是大于 bottom 值了,如果大于的话说明子 View 已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。

在 44 行调用detachViewsFromParent(),会把所有移出屏幕的子 View 全部 detach 掉 ,在 ListView 的概念中,所有看不到的 View 就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证 ListView 的高性能和高效率。

紧接着调用了offsetChildrenTopAndBottom()方法,并将 incrementalDeltaY 作为参数传入,这个方法的作用是让 ListView 中所有的子 View 都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动 ,ListView 的内容也会随着滚动的效果

第 51 行调用了 fillGap() ,用来加载屏幕外的数据。

fillGap() 是一个抽象类,因此我们到 ListView 中找到这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// down 代表加载的方向
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
getListPaddingTop();
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - getListPaddingBottom();
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

down 参数用于表示 ListView 是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用 fillDown()方法,而如果是向上滑动的话就会调用 fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对 ListView 进行填充,所以这两个方法我们就不看了,但是填充 ListView 会通过调用 makeAndAddView() 方法来完成,又是makeAndAddView() 方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:

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
29
30
31
32
33
34
35
/	**
* 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
* @param position
* 列表中的逻辑位置
* @param y
* 被添加View的上 边位置或者下 边位置
* @param flow
* 如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return
* 返回被添加的View
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;


if (!mDataChanged) {
// 尝试从mActiveViews中获取,因为我们第2次layout中已经从mActiveViews中获取到View了,,所以这次获取的为null
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}

// 为这个position创建View
child = obtainView(position, mIsScrap);

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

return child;
}

不管怎么说,这里首先仍然是会尝试调用 RecycleBin 的 getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次 Layout 过程中我们已经从 mActiveViews 中获取过了数据,而根据 RecycleBin 的机制,mActiveViews 是不能够重复利用的,因此这里返回的值肯定是 null。

既然 getActiveView()方法返回的值是 null,那么就还是会走到第 29 行的 obtainView() 方法当中:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
// 1
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
// 2
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}

在 1 处会调用 RecyleBin 的 getScrapView()方法来尝试从废弃缓存中获取一个 View,那么废弃缓存有没有 View 呢?

这里要分两种情况:

  • 屏幕中有一个 View 的部分被移出屏幕,而有一个新 View 部分进入屏幕 。此时废弃缓存为空,因此这个新的 View 是 inflate 出来的。
  • 另一种情况是,有某些 View 已经被移出屏幕,刚才在 trackMotionScroll() 方法中我们就已经看到了,一旦有任何子 View 被移出了屏幕,就会将它加入到废弃缓存 mScrapViews 中,而从 obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么 ListView 神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView 中的子 View 其实来来回回就那么几个,移出屏幕的子 View 会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现 OOM 的情况,甚至内存都不会有所增加。

在 2 处会调用 getView() ,也就是将从 mScrapViews 中获取的 View 传入 Adapter 的 getView() 中:

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
public class ListViewAdapter extends BaseAdapter {

@Override
public View getView(int position, View convertView, ViewGroup parent) {

if (convertView == null){
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.list_view_item_simple, null);
viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view);
convertView.setTag(viewHolder);

Log.d(TAG,"convertView == null");
}else {
Log.d(TAG,"convertView != null");
viewHolder = (ViewHolder) convertView.getTag();
}

viewHolder.mTextView.setText(mList.get(position));
return convertView;
}


static class ViewHolder {
private TextView mTextView;
}
}

难怪平时我们在写 getView()方法是要判断一下 convertView 是不是等于 null,如果等于 null 才调用 inflate() 方法来加载布局,不等于 null 就可以直接利用 convertView,因为 convertView 就是我们之间利用过的 View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把 convertView 中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样。