Android应用篇 - ListView 设计分析

ListView 是 Android 常用的控件之一,其中 Adapter 跟 AdapterView 之间就运用了经典的桥接模式。不过 ListView 也存在不少问题,例如局部刷新,整体的性能等,目前 RecycleView 已经成为主流。

目录:

  1. ListView 的继承关系
  2. Adapter 的作用
  3. RecycleBin 的回收机制
  4. 源码分析

1. ListView 的继承关系

Android 官网的 API 文档中已经有了:

ListView -> AbsListView -> AdapterView -> ViewGroup -> View -> Object,目前已知的直接子类是 ExpandableListView。当然,继承 AbsListView 的还有 GridView:

2. Adapter 的作用

在使用 ListView 时,一定得设置 adapter,才能绑定数据并显示。主要是为了扩展性,ListView 本身只负责视图的展示,但是什么数据,子项视图具体长什么样,都丢给适配器去控制。adapter 里面可以设置数据源,视图源,一般用 holder 来优化卡顿问题。

3. RecycleBin 的回收机制

RecycleBin 机制可谓是 ListView 设计的精髓之一了,用 ListView 展示列表,成千上万的数据但是不会造成 OOM (图片内存没有处理好除外),RecycleBin 便是最大的功臣。RecycleBin 定义在 AbsListView 类中,适用于它所有的子类。

  • 3.1 RecycleBin 代码
class RecycleBin {
        private RecyclerListener mRecyclerListener;

        // 第一个活跃视图的位置索引
        private int mFirstActivePosition;
        // 活跃视图数组,布局开始在屏幕上的视图
        private View[] mActiveViews = new View[0];
        // 可以由适配器用作转换视图的未排序视图
        private ArrayList<View>[] mScrapViews;
        // view 类型数量
        private int mViewTypeCount;

        private ArrayList<View> mCurrentScrap;
        private ArrayList<View> mSkippedScrap;

        private SparseArray<View> mTransientStateViews;
        private LongSparseArray<View> mTransientStateViewsById;

        // ...

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            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;
        }
        
        void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }
            mFirstActivePosition = firstActivePosition;

            //noinspection MismatchedReadAndWriteOfArray
            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
                // Don't put header or footer views into the scrap heap
                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    activeViews[i] = child;
                    lp.scrappedFromPosition = firstActivePosition + i;
                }
            }
        }

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

        View 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;
        }

        void addScrapView(View scrap, int position) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }

            lp.scrappedFromPosition = position;

            final int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    getSkippedScrap().add(scrap);
                }
                return;
            }

            scrap.dispatchStartTemporaryDetach();

            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

            final boolean scrapHasTransientState = scrap.hasTransientState();
            if (scrapHasTransientState) {
                if (mAdapter != null && mAdapterHasStableIds) {
                    // If the adapter has stable IDs, we can reuse the view for
                    // the same data.
                    if (mTransientStateViewsById == null) {
                        mTransientStateViewsById = new LongSparseArray<>();
                    }
                    mTransientStateViewsById.put(lp.itemId, scrap);
                } else if (!mDataChanged) {
                    // If the data hasn't changed, we can reuse the views at
                    // their old positions.
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArray<>();
                    }
                    mTransientStateViews.put(position, scrap);
                } else {
                    // Otherwise, we'll have to remove the view and start over.
                    getSkippedScrap().add(scrap);
                }
            } else {
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }

                if (mRecyclerListener != null) {
                    mRecyclerListener.onMovedToScrapHeap(scrap);
                }
            }
        }

        // ...
    }

这边只贴了几个比较重要的方法:

  • setViewTypeCount():Adapter 当中可以重写一个 getViewTypeCount() 来表示 ListView 中有几种类型的数据项,而setViewTypeCount() 方法的作用就是为每种类型的数据项都单独启用一个 RecycleBin 缓存机制。实际上,getViewTypeCount() 方法通常情况下使用的并不是很多,所以我们只要知道 RecycleBin 当中有这样一个功能就行了。
  • 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 进行返回。
  • 3.2 RecycleBin 原理图

可以看到:Recycler 根据 type 进行了分区,当 Item 滚动出屏幕时,会被放入到 Recycler 中,比如 Item1。然后继续往上滚动,此时需要一个 View 来填充底部,这时从 Recycler 中取出一个,作为 Item8,通过 adapter 的 getView() 的 convertView 返回这个 View。

所以我们的 adapter 的 getView() 一般这么写:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = getLayoutInflater().inflate(getItemViewLayoutRes(), parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else if (convertView.getTag() instanceof ViewHolder) {
            holder = (ViewHolder) convertView.getTag();
        } else {
            return convertView;
        }

        return getItemView(position, convertView, holder);
    }
  • 1. 判断 convertView 是否为空,不为空 (活跃的活着 从Recycler 中取出来的),则直接使用,避免大量填充创建 View。
  • 2. 使用 Holder 对象,避免大量的 findViewById(),直接绑定数据即可。
  • 3.3 正确使用 getView(),会创建几个 converView?

ListView 中通过 recycler 缓存已经生成的 convertView 来实现对 item 中不同的布局的复用。无论是单一类型,还是多种类型的item 布局,其原理都是一样的。同一种布局在滑动的过程中,最多在 ListView 的显示范围内能同时显示的最大数目,即为要生成的 convertView 的数目,其余就可以有足够数量的布局来进行复用了。在有多种不同布局的情况下,getView() 通过首先调用getItemViewType(position) 来查找不同类型的布局的缓存。当然,是在正确覆盖 adapter 中关键方法的前提下,缓存都会正常的工作。

简单点说:类型只有一种时,只需要缓存一屏幕数量的 view,缓存是按照类型存储分 list 存储,但是每种类型的 item 最大缓存数量是某一屏幕中出现该类型 item 的最大数量 (注意是最大的,不一定正好一屏幕都是这种类型)。

4. 源码分析

View 的执行流程无非就分为三步,onMeasure() 用于测量 View 的大小,onLayout() 用于确定 View 的布局,onDraw() 用于将View 绘制到界面上。而在 ListView 当中,onMeasure() 并没有什么特殊的地方,因为它终归是一个 View,占用的空间最多并且通常也就是整个屏幕。onDraw() 在 ListView 当中也没有什么意义,因为 ListView 本身并不负责绘制,而是由 ListView 当中的子元素来进行绘制的。那么 ListView 大部分的神奇功能其实都是在 onLayout() 方法中进行的了。

  • 4.1 第一次 layout

主要代码在 AbsListView.onLayout():

    @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;

        if (mFastScroll != null) {
            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
        }
    }

如果 ListView 的大小或者位置发生了变化,那么 changed 变量就会变成 true,此时会要求所有的子布局都强制进行重绘。

然后进入 ListView.layoutChildren():

@Override
protected void layoutChildren() {
    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (!blockLayoutRequests) {
        mBlockLayoutRequests = true;
    } else {
        return;
    }
    try {
        super.layoutChildren();
        invalidate();
        if (mAdapter == null) {
            resetList();
            invokeOnItemScrollListener();
            return;
        }
        int childrenTop = mListPadding.top;
        int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
        // 第一次时为0
        int childCount = getChildCount();
        // ...
        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }
        // ...
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i));
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            // 第一次 layout 进入这个分支
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        // ...
        detachAllViewsFromParent();
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    // 第一次 layout 进入到这
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } 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;
        }
        recycleBin.scrapActiveViews();
        // ...
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

现在 ListView 当中目前没有任何子 View,数据都还是由 Adapter 管理的,并没有展示到界面上,因此 getChildCount() == 0。接着会根据 dataChanged 这个布尔型的值来判断执行逻辑,dataChanged 只有在数据源发生改变的情况下才会变成 true,其它情况都是 false,因此这里会调用 RecycleBin 的 fillActiveViews() 方法。fillActiveViews() 方法是为了将 ListView 的子 View 进行缓存的,但是目前 ListView 中还没有任何的子 View,因此这一行暂时还起不了任何作用。

接下来会根据 mLayoutMode 的值来决定布局模式,默认情况下都是普通模式 LAYOUT_NORMAL,因此会进入到 default 语句当中。而下面又会紧接着进行两次 if 判断,childCount 目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到fillFromTop() 方法。

ListView.fillFromTop():

   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 开始,自顶至底去填充 ListView。先对 mFirstPosition 做一下合法性校验,然后调用 fillDown() 方法填充 ListView。

ListView.fillDown():

    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 child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

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

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

这边使用 while 循环来执行填充逻辑,一开始 nextTop 的值是第一个子元素顶部距离整个 ListView 顶部的像素值,pos 则是刚刚传入的 mFirstPosition 的值,而 end 是 ListView 底部减去顶部所得的像素值,mItemCount 则是 Adapter 中的元素数量。因此一开始的情况下 nextTop 必定是小于 end 值的,并且 pos 也是小于 mItemCount 值的。那么每执行一次 while 循环,pos 的值都会加1,并且 nextTop 也会增加,当 nextTop 大于等于 end 时,也就是子元素已经超出当前屏幕了,或者 pos 大于等于mItemCount 时,也就是所有 Adapter 中的元素都被遍历结束了,就会跳出 while 循环。

ListView.makeAndAddView():

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;


        if (!mDataChanged) {
            child = mRecycler.getActiveView(position);
            if (child != null) {
                setupChild(child, position, y, flow, childrenLeft, selected, true);
                return child;
            }
        }

        child = obtainView(position, mIsScrap);

        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

先尝试从 RecycleBin 当中获取一个 active view,不过此时 RecycleBin 当中还没有缓存任何的 View,所以这里得到的值为 null。继续往下执行,调用 obtainView() 方法来再次尝试获取一个 View,这次的 obtainView() 方法是可以保证一定返回一个 View 的,于是下面立刻将获取到的 View 传入到了 setupChild() 方法当中。

AbsListView.obtainView():

View obtainView(int position, boolean[] isScrap) {
        // ...
        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                mRecycler.addScrapView(scrapView, position);
            } else {
                isScrap[0] = true;
                child.dispatchFinishTemporaryDetach();
            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position);

        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }
        return child;
    }

先调用 RecycleBin 的 getScrapView() 方法来尝试获取一个废弃缓存中的 View,同样这里肯定是获取不到的,getScrapView() 方法会返回一个 null。调用 mAdapter 的 getView() 方法来去获取一个 View,mAdapter 就是当前 ListView 关联的适配器了,而 getView() 就是自定义 Adapter 重写的 getView()。

getView() 方法接受的三个参数,第一个参数 position 代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数 convertView,刚才传入的是 null,说明没有 convertView 可以利用,因此我们会调用 LayoutInflater的 inflate() 方法来去加载一个布局。接下来会对这个 view 进行一些属性和值的设定,最后将 view 返回。

那么这个 View 也会作为 obtainView() 的结果进行返回,并最终传入到 setupChild() 方法当中。也就是说,第一次 layout 过程当中,所有的子 View 都是调用 LayoutInflater 的 inflate() 方法加载出来的,这样就会相对比较耗时,但是后面就不会再有这种情况了。

  • 4.2 第二次 layout

ListView 在展示到界面上之前都会经历至少两次 onMeasure() 和两次 onLayout() 的过程,其实第二次 layout 和第一次 layout 的基本流程是差不多的,但是获取的 View 将从 RecycleBin 中取出来,而不用再去填充。

  • 4.3 流程图

ActiveViews 是第一次和第二次 layout 时用的,不区分类型,获取完后便会移除元素。而滑出滑入屏幕复用主要是 ScrapViews,这个是区分 type 类型的。
 

发布了126 篇原创文章 · 获赞 215 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/88580689
今日推荐