博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ListView缓存源码分析
阅读量:4181 次
发布时间:2019-05-26

本文共 20935 字,大约阅读时间需要 69 分钟。

        ListView是一个ViewGroup,对于ListView的使用需要分为两个部分,一个是ListView本身,二是adapter,他们各自的作用也是很分明的,ListView负责显示和缓存已经显示过的View;而adapter负责创建View和负责显示界面的内容,如果ListView中已经缓存了,就会从ListView将缓存的View取出传递给adapter,这样adapter就不需要重新创建View了。

        这里先看Adapter的这个接口的功能:

public interface Adapter {    // 数据是由adapter来维护的,所有当数据有变化时,只有adapter清楚,    // 这样当数据更改并要刷新界面时,这个功能就得由adapter来完成了,    // 这里注册DataSetObserver的作用就是用来通知系统需要刷新界面了,    // 调用notifyDataSetChanged()请求刷新界面,内部调用的就是observer    void registerDataSetObserver(DataSetObserver observer);    // 上面注册了,这里自然就是解注册了    void unregisterDataSetObserver(DataSetObserver observer);    // 返回数据的条数    int getCount();    // 返回该位置的数据实体    Object getItem(int position);    // 返回该位置的id,一般就直接返回position了    long getItemId(int position);    boolean hasStableIds();    // adapter的主要作用是创建View,然后对生成的View进行填充数据,之后会返回给ListView,    // 直到生成的View能填充一屏数据为止,这个阶段参数convertView是为null的,当滑动屏幕    // 的时候,有些View会滑出屏幕,这时候这个View就需要回收了,也就是缓存起来,同时,    // 也会有view滑进来,这个滑进来的view会先从缓存中去读取,如果有就会以convertView传    // 进来,这个阶段convertView就是缓存起来的view,这样就达到了复用的目的    // convertView需要展示的数据就是通过position来拿到的    View getView(int position, View convertView, ViewGroup parent);    static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;    // 决定了传给getView()中view的类型(如果ListView中有多个不同的item)    // 返回的值在0(包含)到getViewTypeCount()(不包含)之间    // 原因:getViewTypeCount()决定了缓存View的类型,View是通过ArrayList    // 来进行保存的,而ArrayList又是保存在数组中的,而这个数组的大小    // 又是由getViewTypeCount()决定的,而这里返回的值就决定了是从    // 数组的那个位置拿缓存的view    // 这里在举个例子:假如ListView中有四种item,也就是有四种布局,实际也    // 就是四种View了,这种取名为viewTypeA,viewTypeB,viewTypeC,viewTypeD,    // 这里有一点需要注意,这几种view都会保存这里返回的值,决定他们缓存时是    // 缓存在数组的那个位置,这里返回的值一定是0~3,取出时也是由这里决定取哪    // 个位置缓存的view    int getItemViewType(int position);    // 这个方法决定了ListView中可以显示几种View    int getViewTypeCount();    static final int NO_SELECTION = Integer.MIN_VALUE;    // 这里就是判断当前是否有数据,它的作用就是在没有数据时ListView该怎么显示,    // 在ListView中设置setEmptyView(),那么当没有数据时,自然就会显示这个view了    boolean isEmpty();    default @Nullable CharSequence[] getAutofillOptions() {    return null;    }}

上面已经对adapter中各个接口的作用做了说明,主要作用还是决定了View中显示的内容。

        看完adapter后,再来看看ListView中用来缓存View的一个内部类RecycleBin,它位于ListView的父类AbsListView中,它的作用就是用来缓存view以及复用时取出对应类型的view,这里先来看下RecycleBin源码实现:

class RecycleBin {    private RecyclerListener mRecyclerListener;    // 显示在屏幕上第一个View所处的位置    private int mFirstActivePosition;    // 保存的是需要显示在屏幕上view    private View[] mActiveViews = new View[0];    // view移除屏幕后就缓存在这里,    // 这个数组的大小由adapter的getViewTypeCount()决定    private ArrayList
[] mScrapViews; // 缓存View类型的数量 private int mViewTypeCount; private ArrayList
mCurrentScrap; private ArrayList
mSkippedScrap; private SparseArray
mTransientStateViews; private LongSparseArray
mTransientStateViewsById; // 这个方法在setAdapter()会调用到,viewTypeCount就是adapter的getViewTypeCount()的值 public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //这里创建了一个类型为ArrayList,大小是viewTypeCount的数组,就是用来缓存移出屏幕的View ArrayList
[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList
(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } // 清楚所有缓存起来的view void clear() { if (mViewTypeCount == 1) { final ArrayList
scrap = mCurrentScrap; clearScrap(scrap); } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList
scrap = mScrapViews[i]; clearScrap(scrap); } } clearTransientStateViews(); } // 这里就是保存所有将要显示在屏幕上的View,childCount就是一屏有多少条数数据 // 这里有个需要注意的地方,layout会执行两遍,第一遍会将adapter中生成的view添 // 加到ListView中,当执行第二遍的时候,再次执行这个方法时,就会将ListView中 // 的子View添加到mActiveViews中,由于第二遍执行的时候也会添加View到ListView中, // 所以这里就是为了解决这个问题,第二次layout的时候,先将ListView中的子view添 // 加到mActiveViews,然后再将ListView中的子view移除,等到再次需要添加子view的 // 时候,就直接从mActiveViews拿了 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) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; // Remember the position so that setupChild() doesn't reset state. lp.scrappedFromPosition = firstActivePosition + i; } } } // 这里就是返回需要显示的view,并移除 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 View getScrapView(int position) { // getItemViewType()返回的值就是作为mScrapViews下标的索引 final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { // 只有一种类型的view就是从这里返回 return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { // 有多种类型的view时就是从这里返回 return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; } // 这里就是将滑出的view缓存起来 void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } lp.scrappedFromPosition = position; // 这里lp.viewType的值就是getItemViewType()的值,正好和前面取缓存view对应上了 final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } ...... final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { ...... } else { clearScrapForRebind(scrap); if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { //这里就是缓存view,这个viewType就是getItemViewType()的值 mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } } // 这里就是返回缓存起来的view,也就是复用View private View retrieveFromScrap(ArrayList
scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. // Traverse backwards to find the most recently used scrap view for (int i = size - 1; i >= 0; i--) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { //一般都是执行这里 final View scrap = scrapViews.remove(i); clearScrapForRebind(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearScrapForRebind(scrap); return scrap; } else { return null; } }}

看完上面的代码,这里先来做一些总结:

1、Adapter主要负责数据的处理;

2、Adapter当没有复用View的时候,负责创建View;

3、Adapter在拿到View之后,对View中需要的数据进行填充;

4、RecycleBin主要是对View的缓存和复用的逻辑处理;

上面分析完后,接下来就是该考虑创建出来的VIew是如何显示的,分析的是ListView,那自然是在ListView中去寻找了,这里我们看下ListView的layoutChildren()这个方法,这里需要注意一点,绘制一次,这个方法会执行两次:

protected void layoutChildren() {        ......        try {            ......            final int childrenBottom = mBottom - mTop - mListPadding.bottom;            // 这里初次进来的时候,返回的是0,不是初次进来的时候返回的就是将显示在界面上View的数量            final int childCount = getChildCount();            ......            // 记录第一个显示View的位置,            final int firstPosition = mFirstPosition;            // 这里拿到RecycleBin对象,方便对view的缓存            final RecycleBin recycleBin = mRecycler;            if (dataChanged) {                // 数据发生变化时才会执行到这里,这里实际就是将所有的子View缓存起来                for (int i = 0; i < childCount; i++) {                    recycleBin.addScrapView(getChildAt(i), firstPosition + i);                }            } else {                // 数据没有变化时会执行到这里,第一次布局的时候childCount = 0,                // 第二次执行的时候会将所有的View添加到RecycleBin的mActiveViews中                recycleBin.fillActiveViews(childCount, firstPosition);            }            // 这里在第二次布局的时候会将第一次添加的view去不清除,这样就不会产生一份重复的数据,            // 前面已经将ListView中的view添加到了mActiveViews中,虽说这里进行了解绑,但再次添加            // 的时候是直接将mActiveViews中的添加进去就可以了,所以对性能没什么影响            detachAllViewsFromParent();            recycleBin.removeSkippedScrap();            switch (mLayoutMode) {                case LAYOUT_SET_SELECTION:                    ......                    break;                case LAYOUT_SYNC:                    sel = fillSpecific(mSyncPosition, mSpecificTop);                    break;                case LAYOUT_FORCE_BOTTOM:                    sel = fillUp(mItemCount - 1, childrenBottom);                    adjustViewsUpOrDown();                    break;                case LAYOUT_FORCE_TOP:                    ......                    break;                case LAYOUT_SPECIFIC:                    ......                    break;                case LAYOUT_MOVE_SELECTION:                    sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);                    break;                default:                    // 默认情况下都是普通模式LAYOUT_NORMAL,所有会执行到这里,第一次childCount = 0,                    // 第二次执行到这里的时候ListView中有子View,childCount就不等于0                    if (childCount == 0) {                        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);                        }                    } else {                        if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {                            // 有选中的view时会执行这里                            sel = fillSpecific(mSelectedPosition,                                    oldSel == null ? childrenTop : oldSel.getTop());                        } else if (mFirstPosition < mItemCount) {                            // 首个显示的View的位置小于adapter中数据的条数                            sel = fillSpecific(mFirstPosition,                                    oldFirst == null ? childrenTop : oldFirst.getTop());                        } else {                            sel = fillSpecific(0, childrenTop);                        }                    }                    break;            }            ......        }    }

从上面看下来,第一次布局的时候会执行fillFromTop(childrenTop),这里就是对ListView中的子view进行布局,跟着进去瞧一瞧,看看他是如何实现的:

/**     * Fills the list from top to bottom, starting with mFirstPosition     *     * @param nextTop The location where the top of the first item should be     *        drawn     *     * @return The view that is currently selected     */    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);    }

这个方法中没有什么逻辑,就是先判断第一个显示view的位置的合法性,从注释中可以知道,这里的功能是自顶部到底部开始填充,看来具体的实现逻辑要去看看fillDown()这个方法了:

private View fillDown(int pos, int nextTop) {        View selectedView = null;        // 底部距离减去顶部距离就是可以用来填充view的像素值        int end = (mBottom - mTop);        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {            // 如果设置了padding,这里还需要减去padding的距离            end -= mListPadding.bottom;        }        // nextTop是第一个view在屏幕显示的位置,传进来的pos是显示在屏幕上的第一        // 个view在adapter中的位置,没循环一次,这个值会加1        while (nextTop < end && pos < mItemCount) {            // is this the selected item?            boolean selected = pos == mSelectedPosition;            // 这里就是创建或获取view            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);            // 计算下一个view在屏幕所处的位置,以确定是否填满了屏幕,填满了屏幕就会跳出这个循环            nextTop = child.getBottom() + mDividerHeight;            if (selected) {                selectedView = child;            }            pos++;        }        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);        return selectedView;    }

 在这个循环中,主要的还是获取view,其他的逻辑也是基于这个view的,那这里就去看看makeAndAddView()方法了:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,                                boolean selected) {        if (!mDataChanged) {            // 这里是看是否能拿到一个可以复用的view,前面有提到在第二次布局的时候会将第一次的view             // 清除掉,这里就是再次去拿清除掉的view            final View activeView = mRecycler.getActiveView(position);            if (activeView != null) {                // 当有复用的view返回时执行到这这里                setupChild(activeView, position, y, flow, childrenLeft, selected, true);                return activeView;            }        }        // 当没有复用的view时,就是通过这个方法去创建view        final View child = obtainView(position, mIsScrap);        // 在拿到view之后,还没有将view添加到ListView中,那这个方法就是测量view并添加到ListView中去        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);        return child;    }

这里获取view有两种方式,一种是拿之前缓存的view,如果之前没有缓存,那么就会重新创建一个view,接下来就去看看obtainView()是如何去创建view的:

View obtainView(int position, boolean[] outMetadata) {        ......        // 下面这两个方法可以说是ListView缓存的主要逻辑了,mRecycler.getScrapView(position)是获取缓存的view,        // 而mAdapter.getView(position, scrapView, this)这个方法是不是很熟悉,就是我们重新Adapter中getView()方法,        // 一开始没有缓存时,scrapView返回的是null,所以getView()中传进去的view参数就为null,这是就需要我们创建view了,        // 而当scrapView返回不为null时,这时传进去的view我们就可以直接复用了,这也就是为什么我们一般在getView()中要        // 对传进去的view进行判断,如果为null就创建,不为null就直接使用了,        final View scrapView = mRecycler.getScrapView(position);        final View child = mAdapter.getView(position, scrapView, this);        if (scrapView != null) {            if (child != scrapView) {                // 如果传进去的View没被复用,而是重新创建了view,那么会将传进去的view再次添加进复用池中                mRecycler.addScrapView(scrapView, position);            } else if (child.isTemporarilyDetached()) {                outMetadata[0] = true;                // Finish the temporary detach started in addScrapView().                child.dispatchFinishTemporaryDetach();            }        }        if (mCacheColorHint != 0) {            child.setDrawingCacheBackgroundColor(mCacheColorHint);        }        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);        }        // 这个方法中就是给生成的view设置一些参数,这下参数中就包括对view的分类        setItemViewLayoutParams(child, position);        ......        return child;    }

这个方法中的主要逻辑就是生成view还是复用view,当然setItemViewLayoutParams()这个方法也是挺有意思的,它主要是对view的一些参数进行设置,这里去看下它对view的那些参数进行了设置:

private void setItemViewLayoutParams(View child, int position) {        final ViewGroup.LayoutParams vlp = child.getLayoutParams();        LayoutParams lp;        if (vlp == null) {            lp = (LayoutParams) generateDefaultLayoutParams();        } else if (!checkLayoutParams(vlp)) {            lp = (LayoutParams) generateLayoutParams(vlp);        } else {            lp = (LayoutParams) vlp;        }        if (mAdapterHasStableIds) {            lp.itemId = mAdapter.getItemId(position);        }        // 看这里,还记得getItemViewType()这个Adapter中方法么,这个方法返回的就是view属于哪一种类型,        // 当我们在RecycleBin这个类中对view进行缓存的时候用到的就是view的这个viewType        lp.viewType = mAdapter.getItemViewType(position);        lp.isEnabled = mAdapter.isEnabled(position);        if (lp != vlp) {            child.setLayoutParams(lp);        }    }

这个方法中我们主要看对viewType的赋值,这个值对于view的缓存很重要,它区分生成的view时属于哪一类的(item中有多种布局),这也说明了Adapter中getItemViewType()这个方法的作用了。获取view就分析到这了,接下来让我们返回到makeAndAddView()方法,接下来我们再看下它的setupChild()方法做了些什么东西:

/**     * Adds a view as a child and make sure it is measured (if necessary) and     * positioned properly.     * 添加一个view作为子view确保它被测量和放置到合适的位置     */    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,                            boolean selected, boolean isAttachedToWindow) {        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");        final boolean isSelected = selected && shouldShowSelector();        final boolean updateChildSelected = isSelected != child.isSelected();        final int mode = mTouchMode;        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL                && mMotionPosition == position;        final boolean updateChildPressed = isPressed != child.isPressed();        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected                || child.isLayoutRequested();        // Respect layout params that are already in the view. Otherwise make        // some up...        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();        if (p == null) {            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();        }        p.viewType = mAdapter.getItemViewType(position);        p.isEnabled = mAdapter.isEnabled(position);        // Set up view state before attaching the view, since we may need to        // rely on the jumpDrawablesToCurrentState() call that occurs as part        // of view attachment.        if (updateChildSelected) {            child.setSelected(isSelected);        }        if (updateChildPressed) {            child.setPressed(isPressed);        }        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {            if (child instanceof Checkable) {                ((Checkable) child).setChecked(mCheckStates.get(position));            } else if (getContext().getApplicationInfo().targetSdkVersion                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {                child.setActivated(mCheckStates.get(position));            }        }        if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {            // 第二次布局时会执行到这里,前面有提到会将第一次添加的view detach掉,            // 这时只需要将detach的view再次attachViewToParent()就可以了            attachViewToParent(child, flowDown ? -1 : 0, p);            // If the view was previously attached for a different position,            // then manually jump the drawables.            if (isAttachedToWindow                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)                    != position) {                child.jumpDrawablesToCurrentState();            }        } else {            p.forceAdd = false;            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {                p.recycledHeaderFooter = true;            }            // 将view添加进父布局中,这里其实就是添加到ListView中,第一次布局会执行到这里            addViewInLayout(child, flowDown ? -1 : 0, p, true);            // add view in layout will reset the RTL properties. We have to re-resolve them            child.resolveRtlPropertiesIfNeeded();        }        if (needToMeasure) {            final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,                    mListPadding.left + mListPadding.right, p.width);            final int lpHeight = p.height;            final int childHeightSpec;            if (lpHeight > 0) {                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);            } else {                childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),                        MeasureSpec.UNSPECIFIED);            }            // 对子view进行测量            child.measure(childWidthSpec, childHeightSpec);        } else {            cleanupLayoutState(child);        }        final int w = child.getMeasuredWidth();        final int h = child.getMeasuredHeight();        final int childTop = flowDown ? y : y - h;        if (needToMeasure) {            final int childRight = childrenLeft + w;            final int childBottom = childTop + h;            // 对子view进行布局            child.layout(childrenLeft, childTop, childRight, childBottom);        } else {            child.offsetLeftAndRight(childrenLeft - child.getLeft());            child.offsetTopAndBottom(childTop - child.getTop());        }        if (mCachingStarted && !child.isDrawingCacheEnabled()) {            child.setDrawingCacheEnabled(true);        }        Trace.traceEnd(Trace.TRACE_TAG_VIEW);    }

这里可以分为三个步骤,一是将已经生成的view添加进ListView;二是对添加进去的view进行测量;三是对添加进去的view进行布局,这样一次完整的流程就完成了。

转载地址:http://zkhai.baihongyu.com/

你可能感兴趣的文章
Source Insight 宏-单行注释
查看>>
levelDB源码分析-Arena
查看>>
levelDB源码分析-SSTable
查看>>
平滑升级Nginx的Shell脚本
查看>>
SSH远程会话管理工具
查看>>
canvas标签设长宽是在css中还是在标签中
查看>>
如何创建一个vue项目
查看>>
webpack和webpack-simple中如何引入css文件
查看>>
vue1.0和vue2.0的区别之路由
查看>>
关于vue-router2.0的学习笔记
查看>>
vue1.0与2.0区别之生命周期
查看>>
vue2.0之非父子组件通信
查看>>
如何建立svn版本库并运行它
查看>>
如何合并svn分支到主干上
查看>>
libusb源码学习:list_entry
查看>>
libusb源码学习:几个函数加载的宏(windows)
查看>>
MCU_如何通过硬件VID 查找生产厂家
查看>>
NCNN部署例程 mxnet-gluoncv之simple_pose
查看>>
Ubuntu18.04查看显卡信息并安装NVDIA显卡驱动driver + Cuda + Cudnn
查看>>
电子元件二极管封装SMA,SMB,SMC的区别
查看>>