图片 1

ListView的缓存是存在哪

概述

虽然现在5.0后Google推出了RecycleView,但在5.0
Lollipop普及前Listview仍会被广泛使用,所以打算再次探究一下Listview的源码,了解一下Listview
的构成及加载机制。

忽然,发现,网上的公开资料都是教你怎么继承一个baseadapter,然后重写那几个方法,再调用相关view的 setAdpater()方法, 接着,你的item 就显示在手机屏幕上了。很少有人关注android adpater模式机制的实现原理,比较深入的也不过是说说adapter getview()中的回收情况。今天把相关的源码看了一遍,把自己的理解记录下来。

我们都知道ListView的baseAdapter中,使用了一个view的缓存回收机制,我们经常被告知会把不可见的view缓存起来,并且在新的view显示时会重用之前回收的view,实际中在开发时会使用convertView去进行相关处理。那么我们不禁会好奇,这个缓存回收机制到底是怎么实现的?我们不该仅仅会使用BaseAdapter重写各个方法就够了,我们需要往深处去挖点宝藏。今天我们就来看看listView和adapter在视图回收和缓存环节是怎么做到的。

探究

图片 1

上图简单梳理了Listview的构成及与其相关类之间的关系,并简要地列出了些重要的方法和内部类。

1. AdpaterView 概览

AdpaterView

api手册的说明:An AdapterView is a view whose children are
determined by an Adapter.

实际上android里面ListView, GridView, Spinner , Gallery等view都是基于设计模式上的设配器模式实现的,只要熟悉设配器模式的相关知识,就知道如何从源码里面找到相关的实现线索。

AbsListView

ListView是一个继承自AbsListView的类,要想深入这一部分,我们需要看看AbsListView的源码。AbsListView还是比较复杂的,但是我们可以在ListView的scrollListItemsBy,layoutChildren等方法中看到几个叫mRecycler
和recycleBin的对象,看命名似乎是和视图回收机制重用等有关,mRecycler就是来自AbsListView的,我们可以继续看下去。

/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/
final RecycleBin mRecycler = new RecycleBin();

涉及到这个功能,recycleBin是一个RecycleBin对象,RecycleBin是AbsListView的内部类。那我们就来研究一下RecycleBin这个类。
我们先看看这个注释。

/**
* 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实现了布局中view的重用。RecycleBin有两个层级的存储。

  1. ActiveViews , 布局开始时要在屏幕中显示的view
  2. ScrapViews,
    布局结束后所有的ActiveViews就降级为ScrapViews。ScrapViews就是旧view,主要是可能被adapter为了避免不必要的视图分配空间而重用。
    好了,RecycleBin的结构我们搞懂了,那么关于ListView的核心问题就变成了RecycleBin是怎么运用ActiveViews和ScrapViews的了。换句话说就是ActiveViews和ScrapViews是怎么产生、怎么添加、怎么交换的?

嗯,继续看源码。
private View[] mActiveViews = new View[0];
private ArrayList<View>[] mScrapViews;

mActiveViews就是一个ActiveViews堆,可以看到mActiveViews是一个View数组。
mScrapViews是一个ScrapViews堆,是一个ArrayList<View>数组。这里为什么要这样设计存储?我们先卖个关子。
在研究这两个不同层级的View堆前,我们先看看在ListView中怎么使用RecycleBin的。

  1. setAdapter方法中,mRecycler.clear();
  2. setAdapter方法中,设计到了viewType的操作,因为会有不同的视图结构,mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
  3. onMeasure方法中,

if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {
    mRecycler.addScrapView(child, 0);
}
  1. makeAndAddView方法中,mRecycler.getActiveView(position);
  2. layoutChildren方法中,

    final RecycleBin recycleBin = mRecycler;
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }

    // Clear out old views
    detachAllViewsFromParent();
    recycleBin.removeSkippedScrap();
    ……
    // Flush any cached views that did not get reused above
    recycleBin.scrapActiveViews();

其中在layoutChildren中的用法特别典型,我们具体来看一看。
以上的这段代码可以大概看出一些逻辑思路:

  1. 先判断数据是否有改变,如果改变了就将当前的children加到ScrapViews中,否则加到ActiveViews中。
  2. removeSkippedScrap,把旧的view都删掉。
  3. 最后将以上没有被重用的缓存的view都回收掉。将当前的ActiveVies 移动到
    ScrapViews。

以上是我们通过这一段代码的一个猜测分析,现在一步步看看源码。
dataChanged是一个AdapterView的boolean变量。
其中ListView 继承自AbsListView, AbsListView
继承自AdapterView,AdapterView继承自ViewGroup.
对dataChanged的赋值主要是在AdapterView中的内部类AdapterDataSetObserver中进行的。我们知道listView的adapter使用了观察者模式。这个是怎么做到的?
我们先看看AdapterDataSetObserver的源码:

    class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            if (AdapterView.this.getAdapter().hasStableIds()) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = AdapterView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;

            checkFocus();
            requestLayout();
        }

        public void clearSavedState() {
            mInstanceState = null;
        }
    }

代码很简单,继承自DataSetObserver,重写了onChanged和onInvalidated两个方法,对mDataChanged的操作都是在数据发生改变后将mDataChanged设为true,那么在哪里会变成false呢?AdapterView中已经没有了。我们还需要回到ListView中继续看。

在ListView中搜索这个变量会发现,将其变为false还是同样的在layoutChildren中,并且是完成了对mActiveViews和mScrapViews的各种操作之后才变为false。
并且在makeAndView中使用了false时的值。

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

        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

makeAndAddView能够获取一个view并且把它添加到child的list中,并且返回了这个child,这个child可以是新view,也可以是没有使用过的view
convert过来的,或者说是从缓存中重用的view。
这当中有一个getActiveView方法,就是我们在之前提到的RecycleBin的第四个用法,也是在mDataChanged为false时一个处理方法。
好了,对mDataChanged的分析和“寻找”先到这里,我们接着看看RecycleBin的用法。

既然是我们之前讲过的处理流程,我们先看看最初的一个RecycleBin使用情况。

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

    lp.scrappedFromPosition = position;
    ......

    //当一个view有瞬态时不用被废弃
    final boolean scrapHasTransientState = scrap.hasTransientState();
    if (scrapHasTransientState) {
        if (mAdapter != null && mAdapterHasStableIds) {
            // 如果adapter有稳定的ids,那就能对相同的数据进行view的重用
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<View>();
            }
            mTransientStateViewsById.put(lp.itemId, scrap);
        } else if (!mDataChanged) {
            // 如果绑定的数据没有改变,就能在旧位置重用view
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<View>();
            }
            mTransientStateViews.put(position, scrap);
        } else {
            // 其他情况只能移除view并且从头来过
            if (mSkippedScrap == null) {
                mSkippedScrap = new ArrayList<View>();
            }
            mSkippedScrap.add(scrap);
        }
    }else{
        if (mViewTypeCount == 1) {
            //这里的mCurrentScrap就是mScrapViews[0]
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }

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

我们可以看到,这里的逻辑还算很清晰,在关于view的重用的判断时,涉及到一个概念还是需要解释一下,就是view的瞬态。
View.hasTransientState()的代码就不贴了,主要是几个flag的运算,虽然就一行,但是需要前后联系,感兴趣的朋友可以去看看源码。
我们这里主要理解瞬态,当我们说一个view有瞬态时,我们指app无需再关心这个view的保存与恢复,注释指出一般用来播放动画或者记录选择的位置等相似的行为。
当一个view标记位有瞬态时,在RecycleBin中,就有可能不会降级到ScrapView,而是mTransientStateViews或者mTransientStateViewsById将其保存起来,以便于
后来的重用。

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

在RecycleBin中涉及到瞬态的存储结构是上述代码中的两个,其实就是SparseArray,一个存的是position的key,一个存的是id。

这里介绍了mTransientStateViews的写,我们再看看mTransientStateViews的读。这里的读也是读的SparseArray,虽然功能类似于HashMap,但是读数据时使用
的是valueAt方法。

View getTransientStateView(int position) {
    if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
        long id = mAdapter.getItemId(position);
        View result = mTransientStateViewsById.get(id);
        mTransientStateViewsById.remove(id);
        return result;
    }
    if (mTransientStateViews != null) {
        final int index = mTransientStateViews.indexOfKey(position);
        if (index >= 0) {
            View result = mTransientStateViews.valueAt(index);
            mTransientStateViews.removeAt(index);
            return result;
        }
    }
    return null;
}

上面这份代码就是用来获取瞬时态的view的,首先从mTransientStateViewsById中读取,如果没有就从mTransientStateViews中读取。
这个方法是属于RecycleBin的,但是这个是在哪里调用的呢?答案是AbsListView的obtainView方法。

这里是一个比较关键的地方了。

    View obtainView(int position, boolean[] isScrap) {
        ......

        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // 重新绑定数据失败,就废弃获取到的view
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }
            ......
            return transientView;
        }

        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 {
                isScrap[0] = true;

                child.dispatchFinishTemporaryDetach();
            }
        }

        ......

        setItemViewLayoutParams(child, position);

        ......

        return child;
    }

阅读代码可以发现,在obtainView中,最重要的一个方法就是调用了adapter的getView方法,这个方法也是我们平常重写的方法,getView返回的是一个view。
而在对这个view的获取过程,有一个很明显的两层处理,首先就是尝试获取我们之前分析介绍的transientView,也就是拥有瞬时态的view,通过transientView
以完成复用。如果这一步走的失败了,也就是transientView为null时,或者说这个view并不拥有瞬时态,那么就从ScrapView中获取一个scrapView,这里可能要对
scrapView多做一些处理,我在上面都省略了,不影响逻辑大局,感兴趣的朋友可以去看看源码。最后返回的是scrapView。

getTransientStateView我们已经介绍过了,现在来看看失败之后的getScrapView。

View getScrapView(int position) {
    if (mViewTypeCount == 1) {
        return retrieveFromScrap(mCurrentScrap, position);
    } else {
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
    }
    return null;
}

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // 检查对某一个position或者id是否还有一个对应的view
        for (int i = 0; i < size; 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);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            }
        }
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}

上面这份代码还是写的很清楚了,根据不同的viewType来进行不同策略的取scrapView,但是实质上都是走到了retrieveFromScrap。
思路还是比较清晰,但是需要注意的是在get到scrapView后,在scrapView堆中这个view就会被移除掉。以便以后不停的循环往复的重用。

以上的分析都是在layoutChildren中判断数据改变后的过程,当数据没有改变时,会走到fillActiveViews方法。这个方法能够将AbsListView的所有子view都
装到activeViews中。代码很简单,就是一个for循环,将listView的所有子view一一存到activeViews中。

接着我们最初的分析,这些都走完之后,会执行recycleBin的removeSkippedScrap方法。还记得我们介绍的addScrapView方法吗,当一个view是有瞬时态的,但是
却没有一个保存到mTransientStateViewsById或者mTransientStateViews中时,会存在mSkippedScrap中。这是都会通过removeSkippedScrap全部清空。

在layoutChildren中,涉及到RecycleBin的最后还有一个方法就是scrapActiveViews。因为已经完成了children的布局layout位置的摆放等,所以这个时候需要刷新
缓存,scrapActiveViews这部分代码可能处理的过程有点多,但是最重要的一件事就是将现在mActiveViews还剩下的views都会移到mScrapViews中。

这个迁移过程也是和addScrapView的过程差不多。一开始是先对mActiveViews遍历,每次保存当前结点victim,并将mActiveViews[i]置空,然后对victim进行迁移
操作,如果符合条件就加到mTransientStateViewsById或者mTransientStateViews中,否则就加到mScrapViews中。

好了,至此在addScrapView中对RecycleBin进行的一系列操作就讲完了,我们看看下一步是什么。

ListView的makeAndAddView方法。
makeAndAddView的代码我们之前也贴过了,当判断数据没有发生改变时,会走到RecycleBin的getActiveView方法。
那我们来看看getActiveView。

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

getActiveView的代码就很简单了,根据丢过来的position计算出一个真实有效的index,然后从activeViews中获取相应的view。没什么可讲的。

ok,关于RecycleBin的几个主要方法和执行流程就介绍完了。

我们可以暂时回顾小结一下,实际上RecycleBin的主要结构就是三个,一个是activeView堆,结构是一个View数组,另一个是scrapView堆,结构是一个
ArrayList<View>数组。还有一个是transientViews,结构是SparseArray,主要通过Id或者position存取。
几个结构可以理解为层级不同,activeView比scrapView高一点,当触发了某种条件或者机制后,child的view就会从activieView中移到transientViews或者scrapView中进行缓存。
当ListView需要obtainView时,会先从有瞬时态的sparseArray中获取view,当失败时就会去scrapViews中获取view。
当然,这些过程又是和一个boolean变量mDataChanged进行配合的,具体的过程在上面的源码分析中已经解释过了,诸位可以回过去看看。
基本思路是在给子view布局时,如果数据没有发生改变,就使用当前已经存在ActiveViews的view。
在obtainView时,如果发生了改变,就addScrapView.否则就fill with
activeView..

再次说到addScrapView,由于这是一个比较重要的方法,这里小结时我们也来看看哪些地方调用了addScrapView.
我们可以在ListView源码中搜索addScrapView看看。

  1. onMeasure
  2. measureHeightOfChildren , measure listView指定范围的高度,
    在onMeasure中调用
  3. layoutChildren
  4. scrollListItemsBy ,
    以一定child数目滑动List,需要将滑出的child删掉,在最后添加view

其实前三个中用到的addScrapView我们之前也已经都讲到了,addScrapView的实现过程也不算复杂,主要是和activeView以及有瞬态的view的配合使用。
第四个我们接下来讲一下。

既然了解了RecycleBin的缓存结构和基本方法后,我们来实战看看,在一个Listview滑动过程中,到底是怎么实现view的回收的吧。

现在考虑滑动一个ListView的情况,也就是scrollListItemsBy方法。

private void scrollListItemsBy(int amount) {
    ...
    final AbsListView.RecycleBin recycleBin = mRecycler;
    if (amount < 0) { //上滑
        ...
        View last = getChildAt(numChildren - 1);
        while (last.getBottom() < listBottom) {
            final int lastVisiblePosition = mFirstPosition + numChildren - 1;
            if (lastVisiblePosition < mItemCount - 1) {
                last = addViewBelow(last, lastVisiblePosition);
                numChildren++;
            }
            ...
        }
        ...
        View first = getChildAt(0);
        while (first.getBottom() < listTop) {
            AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
            if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                recycleBin.addScrapView(first, mFirstPosition);
            }
            ...
        }
        ...
    }else{ //往下滑
        ...
        View last = getChildAt(lastIndex);
        ...
        if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
            recycleBin.addScrapView(last, mFirstPosition + lastIndex);
        }
        ...
    }
}

可以看到,每次ListView的滑动事件要将一个view滑出屏幕时,会将头部的或者尾部的(视方向而定)childView通过addScrapView缓存起来,
缓存的流程就是我们一开始分析的了,先保存有瞬时态的view,然后视情况存到scrapViews中。

等到每次obtainView时再依序从缓存的view中取出来。

这样就完成了一个滑动的缓存与回收。

好了,关于ListView的回收机制到这里就讲的差不多了,本质上就是对AbsListView的内部类RecycleBin的操作。
当我们弄懂了这个机制,才能更好的思考更多的问题。

例如,最后我问大家一个问题:

根据我之前所讲的,当一个ListView的有若干个viewType,
当滑出的view和滑入添加的view的type不一样,比如说滑出了一个TextView的item,
滑入了一个ImageView的Item, 那这种情况下还能复用刚才的view吗?

AdapterView

从上图可以清晰的看出Listview归根究底是继承自AdapterView。AdaterView是一个抽象类,一些最基本和通用方法或接口都是在此定义或声明的,其中一些更是开发者所常用的,诸如:

//Item Click 监听接口
/**
 * Interface definition for a callback to be invoked when an item in this
 * AdapterView has been clicked.
 */
public interface OnItemClickListener {
    ... ...
    void onItemClick(AdapterView<?> parent, View view, int position, long id);
}

//设置Adapter抽象方法
/**
 * Sets the adapter that provides the data and the views to represent the data
 * in this widget.
 *
 * @param adapter The adapter to use to create this view's content.
 */
public abstract void setAdapter(T adapter);

此外在AdapterView中实现了DataSetObserver抽象类,我们一般调用mAdapter.notifyChanged()所触发的就是DataSetObserver的onChanged()方法。关键源码如下:

class AdapterDataSetObserver extends DataSetObserver {

    private Parcelable mInstanceState = null;

    @Override
    public void onChanged() {
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();
        ... ...
    }

    @Override
    public void onInvalidated() {
        mDataChanged = true;
        ... ...
    }
    ... ...
}

认识AdapterView

源码链接

要理解listview等的实现,其父类是不得不看。源码有1200多行。阅读完AdapterView,能搞明白以下问题

  1. 响应数据的更改。

    (793 – 842)

  2. 知道点击view的时候,获得对应的位置.

    (593 – 615)

AbsListView

AbsListView是继承自AdapterView,在该类中实现了一个非常重要的内部类RecycleBin,内部类RecycleBin其实就是AbsListView缓存机制的核心类,它的作用是管理AbsListView的item存储和取得。AbsListview的缓存分为两级,第一级为activeView,第二级为scrapview。二者的间的转换主要是在layoutChildren()方法进行(该抽象方法在LisView中实现),具体分析见如下源码:

@Override
protected void layoutChildren() {
... ...
//说明RecycleBin并不缓存HeadView和FooterView
// Don't put header or footer views into the Recycler. 
//Those are already cached in mHeaderViews;
        if (dataChanged) {
            //如果data改变了,则当前所有childView都添加至mScrapViews;
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            //若data未改变,即第一次加载时,根据当前childCount数量对mArchiveViews赋值。
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        ... ...
         switch (mLayoutMode) {
         ... ...(在switch条件中执行makeAndAddView函数)
         }
         // Flush any cached views that did not get reused above
         //执行makeAndAddView函数后将需要显示的item view已添加至ListView中,
         //所以跳出siwtch后会将缓存的mActiveViews全部转换为mScrapViews。
        recycleBin.scrapActiveViews();
        ... ...
}

同时AbsListview中定义了一个ObtainView方法,一般地当Listview加载时若发现没有可复用的itemView时要么从RecycleBin中转换ScrapView都要么是通过mAdapter.getView()获取新的itemView,ObtainView方法就是专门用来处理上述的两种情况,具体分析如下:

View obtainView(int position, boolean[] isScrap) {
    ... ...
    scrapView = mRecycler.getScrapView(position);
    View child;
    //若scrapView不为空,则将scrapView转换为可复用的itemView
    if (scrapView != null) {
       ... ...
        child = mAdapter.getView(position, scrapView, this);
        ... ...
     }else{
     //若scrapView为空,则通过adapter.getView()函数获取新的ItemView
      child = mAdapter.getView(position, null, this);
      ... ...
     }
}

响应数据的更改

这里假设你已经打开了AdpaterView 的 793
到 842 行。。

在我刚开始用adapterview 的时候,最让我费劲的就是,为什么我调用adpater 的 notifyDataSetChanged() 就能更新view 的状态了呢,然后跟调用notifyDataSetInvalidated() 两者之间又有什么区别呢?以前,找了一下资料,没找到很详细的说明,现在从源码里面找答案的话,就很清晰了。

首先,我们要明白一种设计模式:观察者设计模式。

我相信你,应该能明白观察者模式是个什么样的实现了。。。

AdapterView 之所以能对Adapter 的数据更新进行响应,就是因为其在Adapter上注册了一个数据观察者(AdapterDataSetObserver(793 – 842
))的内部类,所以,我们只要对adpater 状态的改变发送一个通知,就能让AdapterView调用相应的方法了。

DataSetObservable 的源码,记得要把其父类也看了。 https://github.com/android/platform*frameworks*base/blob/master/core/java/android/database/DataSetObservable.java

现在我们就能解决我们一开始的疑问notifyDataSetChanged() 与notifyDataSetInvalidated() 具体回到AdapterView 产生什么影响?

我们对比一下onChange() 与 onInvalidated() 方法,就能对比得出,前者会对当前位置的状态进行同步,而后者会重置所有位置的状态。从代码的注释里面还可以获取得到更多的信息。

这样,我们以后调用notifyDataSetChanged()和notifyDataSetInvalidated() 就更加明白会发生什么情况了。

结语

OK,今天就先总结这么多了,不足之处欢迎指出。当然今后使用RecycleView会是一种趋势,和AS一样,找机会要研究一下。

点击item 怎么能够获取到当前的位置

这里假设你已经打开了AdpaterView 的 593 – 615
行。。

对于getPositionForView() 这个方法,你肯定没用过,要搞明白为什么我们能够获取到adapterView 里面item view对应的位置,我们需要看 其直接子类:AbsListView.class

源码相关:(2130-2197) (2196 – 2279)

这里又用到一种设计模式:委托模式

假设你已经搞懂委托模式的概念,首先我们来看源码(2130 – 2197)。

从obtainView() 方法名中我们可以知道,这是一个用于生成itemView的方法。把这块代码看完,以后,会不会有个疑问呢(先不用管回收那块)? position 到哪里了?我们可以看到这个方法实际上并没有对我们的itemview 设置了任何的监听器,那为什么最后能对我们的itemview的动作进行反应呢?

接下来我们看:源码(2196 – 2279)

从代码里面我们可以看出这是一个委托类,对item 的动作进行初始化,以及响应对应的操作,从源码里面我们可以获知得到,一个item view 为什么能对click,longclick,select 动作进行响应,然后,通过调用performItemClick() 最终把事件调用到AdapterView(292-303)的performItemClick() 里面的监听器方法.

如果,你对委托模式不熟的话,要明白这里的话,需要花点时间。

1. 认识 AbsListView 回收机制

源码: AbsListView.class

长期以来,都有这么一个说法,listview 会自动把不可见的view进行回收,但是长期以来,我都没看到有人对其回收机制进行分析说明

回收执行者:RecycleBin

我们回到之前看过的AbsListView.class

obtainView()(2130-2197)

你会看到一个

mRecycler 的变量。

接下来,通过搜索我们可以得知这个变量是在(308)进行初始化,这是一个内部类的

RecycleBin的实例(6139 – 6507)

看到这类,我们大致可以知道,这个类是这个absListView 回收机制的实现者。

请 跳转到(6139)

现在,我们来看一下这个类的注释,大体的意思这个类是用来帮助复用view的,用2个不同级别的方式进行存储(The RecycleBin has two levels of
storage)(个人感觉描述得挺变扭的,还是看原文好了。。)

  1. ActiveViews : 一开始显示在屏幕的view
  2. ScrapViews: 潜在的一些可以让adpater 使用的old views。

然后,注释里面已经说了,ActiveViews 怎么变成 ScrapViews。就注释提供的信息这里我们有两个疑问。

  1. 什么时候产生 ActiveViews。
  2. 什么时候产生 ScrapViews。

这要把这两点搞清楚了,整个回收体系也就清楚了。

1. AbsListView的回收机制具体实现

从RecycleBin类的注释里面我们获知,回收机制的第一步就是屏幕的view 放在ActiveViews,然后通过对ActiveViews进行降级变成ScrapViews,然后通过scrapViews 进行view 的复用

通过,一番的检索,我们在Listview.class(1562行里面找到fillActiveViews()的调用)。

我们观察一下Listview.class(1460 – 1713) 看一下layoutChildren()这个方法是干嘛用的。

当我们看到(1550)行的时候,就会发现了这个回收类的赋值。接下来我们看下 listview是如何利用回收机制:

  1. 当数据发生改变的时候,把当前的view放到scrapviews里面,否则标记为activeViews(1557 – 1562)
  2. recycleBin.removeSkippedScrap();
    移除所有old views
  3. recycleBin.scrapActiveViews();
    刷新缓存,将当前的ActiveVies 移动到 ScrapViews。

这里干了些事情呢?我们回到(1557 – 1562) 我们可以看到一个变量dataChanged,从单词的意思我们就可以,这里的优化规则就是基于数据是否有变化,我们通过搜索成员变量mDataChanged在 (1693) 的时候变成了false 接着我们在makeAndAddView(1751 – 1775)发现了这个变量的使用。

阅读(1756 – 1766) 我们可以看到回收机制的第一次使用,如果数据没有发生改变,通过判断ActiveViews(这些些view来自(1557 – 1562)) 列表里面有没有当前 活动view,有的话直接复用已经存在的view。这样的好处就是直接复用当前已经存在的view,不需要通过adapter.getview()里面获取子view。

好了,接下来我们来看下makeAndAddView(1751 – 1775) 是如何通过adapter.getview()中 获取到view。我们回到AbsListView.class(2130 – 2194)

在 (2134) 中我们看到一个很神秘的方法scrapView =
mRecycler.getTransientStateView(position); 从单词的意思里面我们可以得知这是获取一个瞬间状态的view,这里就有个疑问什么是瞬间状态的view?通过对源码的层层分析终于在View 类的 hasTransientState()方法里面找到描述。从描述中我们得知这个方法是用来标记这个view的瞬时状态,用来告诉app无需关心其保存和恢复。从注释中,官方告诉我这种具有瞬时状态的view,用于在view动画播放等情况中。

那么,我们就可以明白这句话优化的是absListView 的列表动画.

接着阅读到一下代码的时候,我就困惑了

scrapView =
mRecycler.getScrapView(position);

从这行代码里面我们可知,复用的review是跟位置有关的,我们回去在看看(ListView 1557-1563)

if
(dataChanged) {

for (int
i = 0; i < childCount; i++) {

recycleBin.addScrapView(getChildAt(i), firstPosition+i);

}

} else {

recycleBin.fillActiveViews(childCount, firstPosition);

}

我们可以发现,实际上这里放进回收类里面的只有当前的显示的view,并没有产生当前屏幕没有的view,但是,实际使用中,当我们进行滚屏的时候,显示下个view的时候,就已经能发现getView 第二个参数已经不为null了,那实际实现在哪里了,我们通过搜索用到RecycleBin 的方法,找到

layoutChildren()

scrollListItemsBy()

onMeasure()

measureHeightOfChildren()

通过查看

scrollListItemsBy()

我们就能够明白,当我们进行滚屏的时候,在listview 移除item view 的时候,把移除的item view放进了

recycleBin.addScrapView(last,
mFirstPosition+lastIndex);

于是生成下一个view的时候就能够复用之前的view了,搞清楚这个机制以后我们回到

AbsListView.class(2139 – 2168)

接下来代码, 解答了我们一个经典的adapter 优化方法的由来

View
child;

if
(scrapView != null) {

child =
mAdapter.getView(position, scrapView, this);

if
(child.getImportantForAccessibility() ==
IMPORTANT_FOR_ACCESSIBILITY_AUTO) {

child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);

}

 

if (child
!= scrapView) {

mRecycler.addScrapView(scrapView, position);

if
(mCacheColorHint != 0) {

child.setDrawingCacheBackgroundColor(mCacheColorHint);

}

} else {

isScrap[0] = true;

child.dispatchFinishTemporaryDetach();

}

} else {

child =
mAdapter.getView(position, null, this);

 

if
(child.getImportantForAccessibility() ==
IMPORTANT_FOR_ACCESSIBILITY_AUTO) {

child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);

}

 

if
(mCacheColorHint != 0) {

child.setDrawingCacheBackgroundColor(mCacheColorHint);

}

}

实际上所谓的优化,就是通过利用已有产生的View进行复用,减少在Adapter.getView()进行类的实例化操作优化性能。

从某年google io的文档中我们得知这个回收机制的效率能够提供listview 300%的效率。

接着我们还明白了

getView(int position, View convertView,
ViewGroup parent) 这个三个参数的由来了。

通过,对回收机制的分析,我们可以查看

listview
scrollListItemsBy()

的时候应该注意到,实际上不可见的 item 是会被自动移除,那样为什么当滚动过多的item的时候会发生oom的情况了?

在我们阅读完整个回收机制的时候,我们会发现回收机制实际上是通过在内存里面缓存view对象,让listview能够快速的获取view使listview的显示流畅。而导致OOM的问题也出在这里,由于整个回收机制把所有的imageview中的bitmap对象也保存下来,在进行不断的滑屏操作中,RecycleBin 类越来越大,最终导致OOM 的发生。

当然,根据整个思路,要避免OOM实际上也很简单,我们只需要在虚拟机中开辟一个内存块,专门用于保存bitmap对象的 map对象(一般而言用LRU算法实现),所有的imageview的应用都通过整个map 对象进行引用,当这个map对象大于一定程度的时候释放部分bitmap,这就可以保证RecycleBin在保存这些imageview的时候,而这些imageview里面的bitmap对象时通过一个固定的内存块里面获取,只要我们开辟的用于引用的bitmap 的内存块的大小合理,那样就永远也不会发生oom了。

至于其他继承自AbsListView 的View 其回收机制都一样。。

感想

花了,几个小时,把AdapterView 相关源码看完,大致计算了行数有3w 来行代码了,当然,不会是一行不漏的看过去。 这里分享一个看源码的方法。首先,有接口和,抽象类的地方,一定要把所有方法看全,这一块基本上是属于要一行不漏的看完。实际上这些接口,和抽象类是我们看源码重要的索引,那些4,5k行的代码,实际上,里面的关键,都是这些接口,和相应的抽象类的扩展。

发表评论

电子邮件地址不会被公开。 必填项已用*标注