大家元旦快乐~
好记性不如烂笔头,所以我准备弄个源码解析系列,不准备详细解析源码,但把基本原理和设计思想梳理清楚,也给自己留个笔记存档好在后面需要的时候翻起。
今天就从 ListView 开始。
ListView 的核心在于 layoutChildren 函数,分两种情况,一种是全新加载,第二种是非全新加载。主要区别在于 ListView 内部的 View 缓存池的使用,下面依次来讲下。
在 layoutChildren 里面,会根据 LayoutMode 选择调用 fillSpecific/fillUp/fillFromTop 之类的函数来进行填充,这个只是策略问题不是很关键,最终这些函数都调用了 makeAndAddView,然后再调用 obtainView,再调用 adapter.getView,拿到 view 之后再使用 setupChild 加入到 ListView 里面去并放好位置(包含 child 自己的 measure),流程如下:
setupChild 函数里面片段:
全新加载的很好理解,每个 Item 都是按照上面的流程走,并且每个 Item 的 view 都是通过 getView 里面 inflate 出来的(这种情况 getView 的 convertView 传过来是空,意味着 ListView 还没有缓存 view 可以使用)
非全新加载,比如页面滑动,或者 adapter 数据发生变化,这种情况下面整体流程和全新加载没有区别,但在部分函数调用里面有细微差别,比如:
layoutChildren 里面首先将 ListView 的 child 都放入缓存池:
- // 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);
- }
缓存池管理就是这个 RecycleBin 对象,他里面有两种缓存,一种叫 ActiveViews,看上面代码如果不是数据发生变化的非全新加载(比如页面滚动),则把所有子 view 都放入 ActiveViews,然后重新计算位置重新摆放新的 view 的时候,就会首先从 ActiveViews 里面拿出缓存 view,看 makeAndAddView 函数的第一段就是:更巧妙的是,在拿 ActiveView 的缓存 view 的时候,会根据位置来拿,这样的 view 认为是不需要重新经过 adapter 的 getView 函数的,这样极大的提高了效率。(页面滚动的时候页面里面的 item 只是位置变化,不需要重新调用 adapter.getView 函数)
- 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;
- }
假如 ActiveViews 里面拿不到缓存 view 了,比如 ListView 高度发生了变化,需要更多的 view 来填充,这个时候就会从另外一种缓存里面拿,叫做 ScrapViews。这种缓存 view 是属于 ListView 被填满了,结果还剩余有 view 就会被放入到这个缓存池里面来。在 layoutChildren 函数的尾部可以看到有这样一段代码就是这个意思:
- // Flush any cached views that did not get reused above
- recycleBin.scrapActiveViews();
- /**
- * Move all views remaining in mActiveViews to mScrapViews.
- */
- void scrapActiveViews() {
- final View[] activeViews = mActiveViews;
- final boolean hasListener = mRecyclerListener != null;
- final boolean multipleScraps = mViewTypeCount > 1;
- ArrayList < View > scrapViews = mCurrentScrap;
- final int count = activeViews.length;
- for (int i = count - 1; i >= 0; i--) {
- .......
从 ScrapViews 拿缓存 view 的代码在 obtainView 里面:
- final View scrapView = mRecycler.getScrapView(position);
- 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();
- }
- }
可以看到 adapter.getView 的第二个参数 convertView 就是从 ScrapViews 里面拿过来的缓存 view,可能为空也可能不为空,所以我们的 getView 函数都要有 null 判断。
这样子我们就能串起来了,在 makeAndAddView 里面先看看 ActiveViews 里面有没有缓存 view,有的话则直接成功返回也不需要调用 adapter.getView。没有的话则继续调用 obtainView,然后再去 ScrapViews 里面看看有没有缓存 view,ScrapViews 里面拿出来的 view 都需要重新经过 adapter.getView 进行重新刷数据。ScrapViews 可能拿到缓存 view 也可能拿不到,所以 getView 实现需要 null 判断。
我们可以把 ActiveViews 和 ScrapViews 理解为一二级缓存,有效率上面的差别(很明显后者要经过 getView 重新绑定数据)
ListView 的 layoutChildren 在开始的时候通过 fillActiveViews 将子 view 全部放到 ActiveViews 里面,然后等会重新布局的时候又首先从 ActiveViews 里面拿出来,不够用的时候再继续从 ScrapViews 里面拿,非常高效。所以 ActiveViews 可以理解为 layout 期间的一个临时产物。
整体流程就结束了,下面介绍下有些细节的地方可以从中学习到的。
View 有 onAttachedToWindow/onDetachFromWindow,还有一个不怎么常用的 onStartTemporaryDetach/onFinishTemporaryDetach,表示开始和结束临时 Detach,这种状态 View 其实没有真正触发 onDetachFromWindow,只是临时的 Detached 了,在 addScrapView 函数里看到有调用 start,view 被放入 ScrapViews 缓存池了,临时被 detach:然后在 obtainView 里面,从 ScrapViews 里面重新拿出来要使用了,再调用 finish:
这个临时 detached 挺有意思,结合 ViewGroup 的 detachAllViewsFromParent(在 layoutChildren 里面一开始就会调用这个方法),让子 view 的 parent 都设成 null,可以达到一些很巧妙的实现。
ScrapViews 里面的缓存 view 有的是真正 onDetachFromWindow,有的则是 onStartTemporaryDetach,造成这个的原因主要是 scrapActiveViews 和 addScrapView 两个实现上的差异,不过这个不影响实际使用,obtainView 里面会根据实际情况来决定拿到的缓存 view 是要重新 attachToWindow 还是 finshTemporaryDetach,这个 outMetData[0](和 mIsScrap[0] 是同一个)就是标记缓存 view 是不是之前已经 detachFromWiindow(之前是 isTemporarilyDetached 的,说明还未真正 detachFromWindow)然后 setupChild 里面会根据是不是 attachedToWindow 做不同的操作:重新指定 parent(attachViewToParent)还是重新 attachToWindow(addViewInLayout)
源码解析就怕别人看不懂,但愿对同学们有些帮助~
来源: https://segmentfault.com/a/1190000012652122