ListView 源码分析

前言

虽然现在在展示数据的时候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 还是很有必要的,通过了解 ListView,既可以帮助理解更加复杂的 RecyclerView,也可以更进一步地理解 ListView 和 RecyclerView 的区别。本文将基于 API28 分析 ListView 源码。

RecycleBin

RecycleBin 是 AbsListView 中的一个内部类,所以继承于 AbsListView 的子类,也就是 ListView 和 GridView,都可以使用这个类。RecycleBin 机制是 ListView 能够实现成百上千条数据都不会 OOM 的一个重要原因。

类注释

1
2
3
4
5
6
7
/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
* storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
* start of a layout. By construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
* could potentially be used by the adapter to avoid allocating views unnecessarily.
*/

通过类注释,可以得知:

RecycleBin 有两个重要的存储:ActiveViews 和 ScrapViews。ActiveViews 是在布局开始时在屏幕上显示的那些视图。在布局结束时,ActiveViews 中的所有视图都降级为 ScrapViews。ScrapViews 是适配器可能使用的旧视图,避免不必要地重新分配视图。

主要成员变量

1
2
3
4
5
6
7
8
9
private View[] mActiveViews = new View[0];

// 之所以使用集合数组,是因为可能有多种类型的 item,同一类型的废弃 item 放在同一 list 中
private ArrayList<View>[] mScrapViews;

// 指向 scrapViews[0](只有一种类型 item 的时候使用它)
private ArrayList<View> mCurrentScrap;

private ArrayList<View> mSkippedScrap;

主要方法

先看一下主要的方法:

  • void fillActiveViews(int childCount, int firstActivePosition):第一个参数表示要存储的 View 的数量,第二个参数表示 ListView 中第一个可见元素的索引。调用该方法后就可以根据参数将 ListView 中的指定元素存储到 mActiveViews 数组中。
  • View getActiveView(int position):根据索引获取相应的 ActiveView,获取到后就将该 View 从 ActiveViews 从移除,下次再获取该位置的 ActiveView,将会返回 false,也就是说 ActiveView 不能被重复利用。
  • void addScrapView(View scrap, int position):该方法将一个废弃(比如滚动出了屏幕)的 View 缓存起来。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 来存储废弃的 View。
  • View getScrapView(int position):根据索引找到对应类型的 ScrapViews,并从中获取一个 ScrapView 返回。
  • public void setViewTypeCount(int viewTypeCount):Adapter 可以重写 getViewTypeCount() 方法来表示 ListView 有几种类型的 item,而 setViewTypeCount 根据类型数来初始化 mScrapViews 数组,mCurrentScrap 指向第 0 号数组,所以如果只有一种类型,就可以使用 mCurrentScrap;如果有多种类型,就使用 mScrapViews。

onLayout

View 的三大流程中,对于 ListView 而言,onMeasure 并没有什么特别的,因为它终归是一个 View,占用的空间最多也就是整个屏幕。onDraw 也没有什么意义,因为 ListView 本身并不负责绘制,绘制的任务交由子元素自己完成。ListView 大部分的神奇功能都是在 onLayout 中完成的,因此下面分析一些 ListView 的 onLayout过程。

ListView 并没有重写 onLayout 方法,重写 onLayout 的逻辑在其父类 AbsListView 中:

AbsListView#onLayout

1
2
3
4
5
6
7
8
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// ...

layoutChildren();

// ...
}

主要看 layoutChildren 方法,该方法对子元素进行布局,该方法在 AbsListView 是一个空方法,ListView 重写了该方法:

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
75
76
77
78
79
80
81
82
  @Override
protected void layoutChildren() {
// ...

try {
// ...

final int childrenTop = mListPadding.top;
// 当前拥有的子 View 个数,第一次 layout 时子 View 个数为 0
final int childCount = getChildCount();

// ...

// 在调用 adapter.notifyDatasetChanged() 方法时,dataChanged 为 true
// 默认情况下,dataChanged 为 false
boolean dataChanged = mDataChanged;

// ...

final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
// 将当前所有 item 的 View 添加到 RecycleBin 的 ScrapViews 中保存起来
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
// 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
recycleBin.fillActiveViews(childCount, firstPosition);
}

// 清除所有子 View
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();

// 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
switch (mLayoutMode) {
// ...

default:
// 第一次 onLayout 时,childCount 为 0
if (childCount == 0) {
// 判断布局是从上往下还是从下往上,默认为从上往下,进入 if 块
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,childCount 不为 0,包括两种情况:
// 1. 首次布局中的第二次 onLayout
// 2. 后续已经存在子 View,但数据发送改变时,例如调用了 adapter.nitifyDatasetChanged()
else {
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;
}

// 将未使用的 ActiveViews 移动到 ScrapViews 中
recycleBin.scrapActiveViews();

// ...

// 布局完成后,重置 layoutMode 和 mDataChanged
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;

// ...
} // ...
}

该方法较长,只列出了主要代码。重点看 switch 块,这里根据 layoutMode 进行布局,一般走 default。现在先分析第一次 onLayout 的情况,默认从上往下布局,调用 fillFromTop 方法:

ListView#fillFromTop

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

该方法先保证 mFirstPosition 的合理性,之后调用了 fillDown 方法:

ListView#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
  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;
}

// 当子元素超出当前屏幕或全部子元素遍历完时,退出循环
while (nextTop < end && pos < mItemCount) {
boolean selected = pos == mSelectedPosition;
// 添加子 View
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}

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

重点看 makeAndAddView 方法,该方法用于添加子 View

ListView#makeAndAddView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 先尝试从 ActiveView 中获取
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}

// 通过 obtainView 方法获取子 View
final View child = obtainView(position, mIsScrap);

// 测量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

return child;
}

该方法先从 RecycleBin 的 ActiveViews 或通过 obtainView 方法获取子 View,再通过 setupChild 方法测量和放置子 View。

第一次 layout 时,RecycleBin 并没有缓存 ActiveViews,所以只能通过 obtainView 方法获取子 View,ListView 并没有该方法,该方法在其父类 AbsListView 中

AbsListView#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
  View obtainView(int position, boolean[] isScrap) {
// ...

// 从 ScrapViews 中获取一个 scrapView
final View scrapView = mRecycler.getScrapView(position);
// 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}

}
}
// ...

return child;
}

在该方法,先从 ScrapViews 中获取一个 scrapView,之后调用 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入。

在第一次 layout 中,由于 scrapView 为 null,所以所有的子 View 都是通过 LayoutInflater 的 inflate 方法加载出来的,相对比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

第二次 layout

在某些手机版本中(9.0 版本好像没有这种情况),View 在展示到界面上时会经历两次 onLayout。如果 ListView 进行了两次 onLayout 的话,就会存在一份重复的元素了。因此 ListView 在 layoutChildren 中对第二次 layout 做了处理,非常巧妙地解决了这个问题。

下面就来分析一些 ListView 的第二次 layout 过程,首先看 layoutChildren 方法中的变化:

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
  @Override
protected void layoutChildren() {
// ...

try {
// ...

// 当前拥有的子 View 个数,第二次 layout 时子 View 个数不为 0
final int childCount = getChildCount();

// ...

// 将当前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起来
recycleBin.fillActiveViews(childCount, firstPosition);

// 清除所有子 View
detachAllViewsFromParent();

// 通常情况下,mLayoutMode 为 LAYOUT_NORMAL,走 default
switch (mLayoutMode) {
// ...

default:
if (childCount == 0) {// ...}
// 第二次 layout 时进入 else 块
else {
// 一开始没有选中 item,mSelectedPosition 的值为 -1
// 所以不会进入 if 块,而是调用 fillSpecific 方法
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;
}
// ...
} // ...
}

在第二次 layout 中,子 View 数量不为 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除所有旧的子 View。由于子 View 数量不为 0,之后会调用 fillSpecific 方法:

ListView#fillSpecific

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
  private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;

// 获取并设置当前 position 的子 View
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;

View above;
View below;

// 以 position 为中心,分别向上和向下获取并设置其他子 View
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}

if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}

该方法先设置当前 position 的子 View,然后以 position 为中心,分别向上和向下设置其他子 View。由于第二次 layout 时传入的 position 就是第一个子 View 的位置,所以和第一次 layout 的布局顺序是差不多的。获取并设置子 View 还是通过 makeAndAddView 方法。

ListView#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) {
if (!mDataChanged) {
// 第二次 layout 时,可以从 ActiveViews 中获取到子 View
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}

// ...
}

这里和第一次 layout 不同的是,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

小结

第一次 layout 时,由于当前子 View 数量为 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都没有缓存,所以只能在 Adapter 的 getView 方法中,通过 LayoutInflate 的 inflate 方法加载子 View,相对来说比较耗时,不过一开始只会加载第一屏的数据,这样就保证了 ListView 的内容能够迅速显示在屏幕上。

在某些手机版本中,第一次显示 ListView 时可能会发生两次 layout。和第一次 layout 过程不同,在进行第二次 layout 时,子 View 数量不为 0,就可以先将所有子View 添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除旧的子 View,之后再次设置新的子 View 时,由于之前已经把旧的子 View 存到了 ActiveViews,所以可以直接从 ActiveViews 中获取到子 View,无需再通过 inflate 方法加载子 View。

(注:在 Android 9.0 版本中,Button 显示时调用了两次 onMeasure、一次 onLayout、两次 onDraw;TextView 显示时调用了两次 onMeasure、一次 onLayout、一次 onDraw;ListView 会调用多次 onMeasure、一次 onLayout、多次 onDraw。所以在 9.0 版本并不会发生第二次 layout。)

滑动加载更多数据

上面 layout 过程分析的只是加载第一页的数据,如果有很多数据,剩下的数据将会在滑动过程中加载。下面将分析一下滑动加载数据的过程。

该过程涉及到事件分发,所以是从 AbsListView 的 onTouchEvent 方法开始,滑动对应 ACTION_MOVE,所以接下来调用 onTouchMove 方法,里面又有一个 switch 语句判断 mTouchMode,这里对应 TOUCH_MODE_SCROLL,所以接下来调用 scrollIfNeeded 方法,里面又继续调用 trackMotionScroll 方法。

下面看一下 trackMotionScroll 方法:

AbsListView#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
56
57
58
59
60
61
62
63
64
65
66
67
68
  boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
// ...

// incrementalDeltaY < 0,说明是向下滑动(这里指内容,手指是向上滑动的)
final boolean down = incrementalDeltaY < 0;

// getHeaderViewsCount 和 getFooterViewsCount 默认返回 0
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();

int start = 0; // 开始移除的索引
int count = 0; // 移除的数量

// 向下滑动
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
// 从上往下遍历子 View
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
// 如果该子 View 的 bottom 值大于等于滑动的距离
// 说明该子 View 以及其后的 View 都在屏幕上,退出循环
if (child.getBottom() >= top) {
break;
}
// 如果该子 View 的 bottom 值小于滑动的距离,说明该子 View 已经不在屏幕上
else {
count++;
int position = firstPosition + i; // 该子 View 的索引
// 将不在屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
// 向上滑动,和向下滑动的过程相似,也是将不在屏幕上的子 View 添加进 RecycleBin 的 ScrapViews 中
// ...
}

// 将不在屏幕的子 View 全部 detach 掉
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}

// 让所有的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果
offsetChildrenTopAndBottom(incrementalDeltaY);

// 向下滑动时,更新 mFirstPosition(之后填充布局时会用到)
if (down) {
mFirstPosition += count;
}

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
// 如果第一个 View 的顶部或最后一个 View 的底部移入屏幕
// 说明要加载屏幕外的数据来填充布局
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}

// ...

return false;
}

该方法首先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后调用 fillGap 方法加载屏幕外的数据来填充布局,fillGap 在 AbsListView 是一个抽象方法,ListView 中有具体实现。

ListView#fillGap

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
  @Override
void fillGap(boolean down) {
final int count = getChildCount();
// 如果是向下滑动,就通过 fillDown 方法从上往下添加子 View
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
}
// 如果是向下滑动,就通过 fillUp 方法从下往上添加子 View
else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

该方法根据滑动方向,调用 fillDown 或 fillUp 方法添加子 View,无论调用拿个方法,最终都是调用 makeAndAddView 方法:

ListView#makeAndAddView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 先尝试从 ActiveViews 中获取
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}

// 通过 obtainView 方法获取子 View
final View child = obtainView(position, mIsScrap);

// 测量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

return child;
}

先从 RecycleBin 的 ActiveViews 中获取,如果还没有进行第二次 layout 的话,是可以获取到的,如果已经进行过第二次 layout,那么就获取不到了,因为第二次 layout 的时候已经从 ActiveViews 中拿到过子 View,而 ActiveViews 不能重复利用,所以就获取不到了。

如果 ActiveViews 获取不到,就会调用 obtainView 方法获取:

AbsListView#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
  View obtainView(int position, boolean[] isScrap) {
// ...

// 从 ScrapViews 中获取一个 scrapView
final View scrapView = mRecycler.getScrapView(position);
// 从 Adapter 的 getView 方法获取子 View,并将刚才得到的 scrapView 作为第二个参数传入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 该 scrapView 没有被用户利用,将其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}

}
}
// ...

return child;
}

这次和第一次 layout 的情况不一样,因为之前把移除屏幕的子 View 添加到了 ScrapViews 中,所以现在就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以利用这个缓存 View,不用再 inflate 一个子 View 了。

小结

ListView 在滑动时,先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并从父布局中 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后通过加载屏幕外的数据来填充布局,这时就可以从 ScrapViews 中得到之前移除的子 View,并传入 Adapter 的 getView 方法。用户就可以重复利用这个缓存 View,无需再重新 inflate 一个子 View。

Adapter 相关

ListView 只是负责展示各子 View,各子 View 具体如何填充数据是交由 Adapter 来完成的。ListView 通过 setAdapter 方法和 Adapter 建立联系。先看一下该方法:

ListView#setAdapter

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
  @Override
public void setAdapter(ListAdapter adapter) {

// 如果之前绑定过 Adapter,先取消注册 AdapterDataSetObserver
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}

resetList();
mRecycler.clear();

// 设置新的 Adapter
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
// 如果 ListView 有 headerView 或 footerView,需包装传入的 adapter
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}

// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);

if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
// 设置 item 个数
mItemCount = mAdapter.getCount();
checkFocus();

// 生成并在 Adapter 中注册 AdapterDataSetObserver,用于通知数据源的改变
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);

mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

//...
} // ...

// 进行视图重绘
requestLayout();
}

在该方法中,ListView 绑定传入的 adapter,并为 adapter 注册 AdapterDataSetObserver,用于通知数据源的改变。 最后调用 requestLayout 方法,该方法最终会调用 ListView 的 onLayout,来到第一次 onLayout 的过程。

如果数据源发生了改变,想要更新 ListView 的时候,我们会调用 Adapter 的 notifyDataSetChanged 方法:

BaseAdapter#notifyDataSetChanged

1
2
3
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}

又调用了 DataSetObservable 的 notifyChanged:

1
2
3
4
5
6
7
public void notifyChanged() {
synchronized(mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}

这里的 mObservers 定义在 DataSetObservable 的父类 Observable 中:

1
protected final ArrayList<T> mObservers = new ArrayList<T>();

mObservers 的元素是从哪里来的呢?要从 setAdapter 的这一句说起:

1
mAdapter.registerDataSetObserver(mDataSetObserver);

这一句最终调用了 Observable 的 registerObserver 方法:

1
2
3
4
5
6
7
8
9
10
11
public void registerObserver(T observer) {
if (observer == null) {
throw new IllegalArgumentException("The observer is null.");
}
synchronized(mObservers) {
if (mObservers.contains(observer)) {
throw new IllegalStateException("Observer " + observer + " is already registered.");
}
mObservers.add(observer);
}
}

可以看到,这里将 Adapter 注册的 AdapterDataSetObserver 添加进了 mObservers 中。

所以饶了一大圈,Adapter 的 notifyDataSetChanged 方法最终调用了 AdapterDataSetObserver(AdapterView 的一个内部类)的 onChanged 方法:

AdapterDataSetObserver#onChanged

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onChanged() {
// 将 mDataChanged 属性设置为 true
mDataChanged = true;
mOldItemCount = mItemCount;
// 更新 item 数量
mItemCount = getAdapter().getCount();

// ...

// 最后进行视图重绘
requestLayout();
}

在该方法中,首先将 mDataChanged 属性设置为 true,并更新 item 数量,最后进行视图重绘,在 onLayout 中更新子 View。

写在最后

到此为止,对于 ListView 的 分析就告一段落了。在分析 ListView 的过程,发现经常遇到也是最重要的就是 onLayout 过程以及 RecycleBin 机制。

无论是设置 Adapter 还是通知 Adapter 更新数据的过程,最终都会回到视图重绘,也就是 onLayout。而 onLayout 过程也会根据是第一次 layout、第二次 layout 还是数据源改变的情况从不同途径获取子 View,是通过 inflate 加载还是从 RecycleBin 的 ActiveViews 或 ScrapViews 获取。

RecycleBin 对子 View 的回收也是 ListView 的一个重点或是巧妙之处,在第二次 layout 时,会把子 View 添加到 RecycleBin 的 ActiveViews 中,之后获取新的子 View 时就可以直接从 ActiveViews 获取。在滑动过程中,滑出屏幕的子 View 又会被添加到 RecycleBin 的 ScrapViews 中,在之后填充布局时重新利用。

参考

-------------    本文到此结束  感谢您的阅读    -------------
0%