前言
虽然现在在展示数据的时候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 还是很有必要的,通过了解 ListView,既可以帮助理解更加复杂的 RecyclerView,也可以更进一步地理解 ListView 和 RecyclerView 的区别。本文将基于 API28 分析 ListView 源码。
RecycleBin
RecycleBin 是 AbsListView 中的一个内部类,所以继承于 AbsListView 的子类,也就是 ListView 和 GridView,都可以使用这个类。RecycleBin 机制是 ListView 能够实现成百上千条数据都不会 OOM 的一个重要原因。
类注释
1 | /** |
通过类注释,可以得知:
RecycleBin 有两个重要的存储:ActiveViews 和 ScrapViews。ActiveViews 是在布局开始时在屏幕上显示的那些视图。在布局结束时,ActiveViews 中的所有视图都降级为 ScrapViews。ScrapViews 是适配器可能使用的旧视图,避免不必要地重新分配视图。
主要成员变量
1 | private View[] mActiveViews = new View[0]; |
主要方法
先看一下主要的方法:
- 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 | protected void onLayout(boolean changed, int l, int t, int r, int b) { |
主要看 layoutChildren 方法,该方法对子元素进行布局,该方法在 AbsListView 是一个空方法,ListView 重写了该方法:
ListView#layoutChildren
1 |
|
该方法较长,只列出了主要代码。重点看 switch 块,这里根据 layoutMode 进行布局,一般走 default。现在先分析第一次 onLayout 的情况,默认从上往下布局,调用 fillFromTop 方法:
ListView#fillFromTop
1 | private View fillFromTop(int nextTop) { |
该方法先保证 mFirstPosition 的合理性,之后调用了 fillDown 方法:
ListView#fillDown
1 | private View fillDown(int pos, int nextTop) { |
重点看 makeAndAddView 方法,该方法用于添加子 View
ListView#makeAndAddView
1 | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, |
该方法先从 RecycleBin 的 ActiveViews 或通过 obtainView 方法获取子 View,再通过 setupChild 方法测量和放置子 View。
第一次 layout 时,RecycleBin 并没有缓存 ActiveViews,所以只能通过 obtainView 方法获取子 View,ListView 并没有该方法,该方法在其父类 AbsListView 中
AbsListView#obtainView
1 | View obtainView(int position, boolean[] isScrap) { |
在该方法,先从 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 |
|
在第二次 layout 中,子 View 数量不为 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起来。然后清除所有旧的子 View。由于子 View 数量不为 0,之后会调用 fillSpecific 方法:
ListView#fillSpecific
1 | private View fillSpecific(int position, int top) { |
该方法先设置当前 position 的子 View,然后以 position 为中心,分别向上和向下设置其他子 View。由于第二次 layout 时传入的 position 就是第一个子 View 的位置,所以和第一次 layout 的布局顺序是差不多的。获取并设置子 View 还是通过 makeAndAddView 方法。
ListView#makeAndAddView
1 | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, |
这里和第一次 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 | boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { |
该方法首先将滑出屏幕的子 View 添加进 RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后让剩下的子 View 进行相应的偏移,达到内容随手指的拖动而滚动的效果。最后调用 fillGap 方法加载屏幕外的数据来填充布局,fillGap 在 AbsListView 是一个抽象方法,ListView 中有具体实现。
ListView#fillGap
1 |
|
该方法根据滑动方向,调用 fillDown 或 fillUp 方法添加子 View,无论调用拿个方法,最终都是调用 makeAndAddView 方法:
ListView#makeAndAddView
1 | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, |
先从 RecycleBin 的 ActiveViews 中获取,如果还没有进行第二次 layout 的话,是可以获取到的,如果已经进行过第二次 layout,那么就获取不到了,因为第二次 layout 的时候已经从 ActiveViews 中拿到过子 View,而 ActiveViews 不能重复利用,所以就获取不到了。
如果 ActiveViews 获取不到,就会调用 obtainView 方法获取:
AbsListView#obtainView
1 | View obtainView(int position, boolean[] isScrap) { |
这次和第一次 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 |
|
在该方法中,ListView 绑定传入的 adapter,并为 adapter 注册 AdapterDataSetObserver,用于通知数据源的改变。 最后调用 requestLayout 方法,该方法最终会调用 ListView 的 onLayout,来到第一次 onLayout 的过程。
如果数据源发生了改变,想要更新 ListView 的时候,我们会调用 Adapter 的 notifyDataSetChanged 方法:
BaseAdapter#notifyDataSetChanged
1 | public void notifyDataSetChanged() { |
又调用了 DataSetObservable 的 notifyChanged:1
2
3
4
5
6
7public 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
11public 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 |
|
在该方法中,首先将 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 中,在之后填充布局时重新利用。