listview源码学习

前言

本文从源码角度出发学习listview,主要分析首次RecycleBin的组成,layout的过程,滑动过程,item的点击实现,如何支持Header,notifyDataSetChanged原理。

问题

用了好几年的listview,有几个问题却一直不清楚
1、如何让一个itemview不被回收,比如我的listview里有个viewpager比较复杂,不想让他被回收又重新创建。
2、head和foot的原理是怎么样的,会被回收吗?
3、scrap里的view会被进一步回收掉吗?

基础知识

读了3遍郭神的http://blog.csdn.net/sinyu890807/article/details/44996879 真是受益匪浅,原来listview是这么实现的。
listview的实现方法跟scrollview完全不同,scrollview是内部实例化了所有的view,在滚动的时候只是改变可见的部分,scrollview的高度可能是几千几万。如果item数很多的话,必然会oom。
而listview是首先画出listview的壳,然后去adapter里取数据,取到数据inflate为view,填到listview里面去,填满了就好了。即使adapter里有1万个数据,第一次layout的时候取的也是很少的数据(看当前屏幕需要,假设10个)。然后在上滑的过程中,首先用offsetTopAndBottom对所有child进行移动,此时顶部view就会滑出部分,那么底部会出现gap,再去adapter里面捞数据,填到底部;然后顶部的view逐渐的被完全移出屏幕,先detach,然后把这个view丢到scrap里面去,继续滑动底部又出现了gap,就去scrap里面拿现成的view。如此往复循环,这就是listview的原理。
和scrollview对比,listview的滑动过程中伴随着view的detach,attach,但是这些都不是耗时的东西,时间上没什么损失,但是空间上减少了大量的内存开销。先分析下layout过程和滑动过程。
listview内的缓存主要就是scrap,离屏就会进入scrap。scrap在layout的时候会进行裁剪,去调尾部的一些view,但是实际上这种情况发生的不多,后边会详细说。

layout过程

我测试了下layout的次数,郭神文章说的是2次,我这里会有3次。

第一次layout

onLayout -> layoutChildren -> fillFromTop-> fillDown-> while() makeAndAddView

makeAndAddView ->  
                    1、obtainView  -> getView -> inflate       
                    2、setupChild  -> addViewInLayout
                                   ->child.measure
                                   ->child.layout

第二次layout

onLayout -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent     

第三次layout

onLayout -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent    
                                       ->child.measure
                                       ->child.layout  

可以看到后2次基本差不多,区别在于在setupChild内是否要执行child的measure和layout。为什么第3次layout会调用measure和layout,而第二次不会呢?看下边的代码,差别就在于第三次child.isLayoutRequested()变为了true。

//setupChild
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

我在重新捋一下流程
1、ViewRootImpl#dispatchResized被调用,发出MSG_RESIZED_REPORT消息
2、第一次ListView:layoutChildren
3、第二次ListView:layoutChildren
4、收到发出MSG_RESIZED_REPORT消息,ViewRootImpl#forceLayout
5、第三次ListView:layoutChildren

所以第三次ListView:layoutChildren的时候会触发child.measure和child.layout。奇怪的是,每次都是第2次layout之后收到MSG_RESIZED_REPORT消息

滑动

对移除屏幕的view addScrapView、detachViewsFromParent
对屏幕内的view offsetChildrenTopAndBottom
对屏幕内空白的地方 fillGap -> fillDown->while() makeAndAddView
makeAndAddView -> obtainView、setupChild
obtainView-》getScrapView-》adapter.getView(convertview....)

void fillGap(boolean down) {  
    final int count = getChildCount();  
    if (down) {  
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :  
                getListPaddingTop();  
        //手指向上滑动,所以需要填充底部        
        fillDown(mFirstPosition + count, startOffset);  
        correctTooHigh(getChildCount());  
    } else {  
        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :  
                getHeight() - getListPaddingBottom();  
        fillUp(mFirstPosition - 1, startOffset);  
        correctTooLow(getChildCount());  
    }  
}  

滑动过程中不会调用onMeasure或者onLayout

RecycleBin基本成员与方法

view的回收复用主要就依靠RecycleBin,所以重点分析下RecycleBin

mScrapViews

RecycleBin内有个垃圾箱,mScrapViews用来存放移除屏幕的view。

 private ArrayList[] mScrapViews;
 private ArrayList mCurrentScrap = mScrapViews[0];;

为什么是个数组呢?数组的每一项都是个ArrayList,代表着某个type的垃圾view集合.如果只有一种type,那么垃圾都存在mScrapViews[0]内,mCurrentScrap = scrapViews[0];如果只有一个类型,我们直接操作mCurrentScrap即可

addScrapView

addScrapView就是把一个view加入到垃圾箱内,一般在view离开屏幕的时候调用。如果数据未变,adapter有stable IDs,有暂态,那就不会被收到垃圾箱里,会存着备用。。如果是header、footer那么就放入mSkippedScrap内,不放入mScrapViews。如果是暂态而且有有stable IDs,就丢到mTransientStateViewsById里面去。如果不需要stable IDs,数据未变可以丢到mTransientStateViews

    /**
         * Puts a view into the list of scrap views.
         * 

* If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * * @param scrap The view to add * @param position The view's position within its parent */ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { // Can't recycle, but we don't know anything about the view. // Ignore it completely. return; } lp.scrappedFromPosition = position; // Remove but don't scrap header or footer views, or views that // should otherwise not be recycled. final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { // Can't recycle. If it's not a header or footer, which have // special handling and should be ignored, then skip the scrap // heap and we'll fully detach the view later. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); // The the accessibility state of the view may change while temporary // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. 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. //是暂态view,并且需要stable ID就丢到mTransientStateViewsById里面去 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 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); } } }

retrieveFromScrap

从scrap里取view,核心代码如下,如果是固定id的,那就根据adapter的id来找,否则就根据scrappedFromPosition 来找,比如第7个item被回收到scrap里了,记下这个view的scrappedFromPosition为7, 那下次滑回第7个item,就尽量给scrappedFromPosition为7的view给他,简单的说就是从哪里回收来的,还回哪里去。如果根据scrappedFromPosition找不到,那就直接取scrap的最后一个

          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);            
        return scrap;

activeViews

这个有什么意义,没看懂。根据上面的分析,在第二次layout的过程中,首先会把当前屏幕的itemview给detach掉,扔到activeViews内,然后又把他们抓出来,给attach上,此时activeViews必定为空,如果不为空,把残余的view丢到mScrapViews内(scrapActiveViews) 我实在不明白这么搞有什么意义。

shouldRecycleViewType

根据type类型来确定这个view是否能回收,type类型一般可以在adapter里指定,但是系统默认提供了2个类型,一个是ITEM_VIEW_TYPE_IGNORE=-1,一个是ITEM_VIEW_TYPE_HEADER_OR_FOOTER=-2。第二个很明显就是listview的头和尾。第一个是什么呢?如果我们希望某个view不被回收的话,可以设置ITEM_VIEW_TYPE_IGNORE,这样就可以了。(recyclerView有类似的吗?)

        public boolean shouldRecycleViewType(int viewType) {
            return viewType >= 0;
        }

mRecyclerListener

当发生View回收时,mRecyclerListener若有注册,则会通知给注册者.RecyclerListener接口只有一个函数onMovedToScrapHeap,指明某个view被回收到了scrap heap.可以在这个接口回调里进行昂贵资源的回收(比如bitmap)。可以直接用listview来注册监听者.

        listview.setRecyclerListener(new AbsListView.RecyclerListener() {
            @Override
            public void onMovedToScrapHeap(View view) {
            }
        });

点击item

点击一个item,是和第二次layout类似的,会调用layoutChildren,然后把界面上的view抓起来丢到activeViews内,然后又重新填充,setupChild内不会调用measure和layout

onTouchUp -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent    
                                      

跟第二次layout的区别就是没有调用child的onMeasure和onLayout,关键代码如下,这里needToMeasure为false

final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

Header

我们可以轻易的给使用addHeaderView一个listview加上header。
我知道addHeaderView必须在setAdapter之前,可以add多个head
那么问题来了,为什么addHeaderView必须在setAdapter之前?
看setAdapter的部分代码可以明白,如果之前设置了header,那mAdapter将会被包装起来HeaderViewListAdapter

//setAdapter
        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

再看看HeaderViewListAdapter,看下边的代码可以看到实际上HeaderViewListAdapter实现了Adapter的各种接口,比如getCount,getItem,getItemViewType,getView,这就是把原来的adapter进行包装,然后实现对应接口,把Header作为一种特殊类型AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER,在ListView看来,他就是一个普通的adapter。

//HeaderViewListAdapter
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {

    public int getCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }
    
    public Object getItem(int position) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).data;
        }

        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItem(adjPosition);
            }
        }

        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).data;
    }
    public View getView(int position, View convertView, ViewGroup parent) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }

        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }

    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }

        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }
}

我们在分析下header是否会被多次创建,是否会被丢到scrap里去

首先看,滑动的时候会不会回收header,这里明显可以看到position的限制,header和footer是不会被回收的。既然不会回收,那下次再滑到header的时候还是找adapter要,看上边的adapte的getView代码,mHeaderViewInfos.get(position).view,只是从mHeaderViewInfos.get内取,所以header是不会被回收的,永远存在mHeaderViewInfos里面。这里可以得到启发,Recyclerview是不支持header,footer的,那我们是不是可以针对Recyclerview来一次类似的包装,让他支持header,footer

//trackMotionScroll
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }

notifyDataSetChanged

之前一直没有说过,数据发生变化的情况会怎么样,我们都知道,数据发生变化调用adpater的notifyDataSetChanged就会刷新界面。这里面的原理是什么? 这里面有个观察者模式,BaseAdapter内有个mDataSetObservable,AbsListView在onAttachedToWindow的时候会注册观察者,代码如下,这样就注册了一个观察者mDataSetObserver

//AbsListView#onAttachedToWindow
   if (mAdapter != null && mDataSetObserver == null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            // Data may have changed while we were detached. Refresh.
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
        }

notifyDataSetChanged会调用mDataSetObserver.onChanged,里面更新了mItemCount,然后调用了rememberSyncState和requestLayout。

        @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();
        }

这里rememberSyncState比较陌生,实际上他做的事情也很少,主要就2行代码,设置mSyncMode和mSyncPosition。重新布局的时候默认有个原则,之前谁在第一个,那么这次谁还是在第一个.

      mSyncPosition = mFirstPosition;
      mSyncMode = SYNC_FIRST_POSITION;

然后我们看又一次layout的过程
首先,handleDataChanged会定下mSyncPosition,然后把
mLayoutMode = LAYOUT_SYNC;,这个mLayoutMode后边会用到
第二步,因为dataChanged所以这里直接把所有的界面上的view丢到scrap里,不像以前放在activeViews里
第三步,detachAllViewsFromParent
第四步,fillSpecific是因为mLayoutMode是LAYOUT_SYNC所以直接调用fillSpecific。
里面的getView是adapter的getView一般在这里设置实际view的内容(比如文本图片)。所以view一般都会设置为PFLAG_FORCE_LAYOUT,所以会重新measure、layout。(这里可以再思考下,其实大部分情况下,重用view,并不用重新measure,而layout的时候只要把item往listview的框里丢就可以了,item内部也不需要layout,这样应该能够提供效率,但是看了代码后发现setupChild内是根据needToMeasure来决定是否measure、layout的,不能分别对待,哎。)

onLayout -> layoutChildren -> 
1、handleDataChanged:定个mSyncPosition、mLayoutMode = LAYOUT_SYNC;
2、for() addScrapView
3、detachAllViewsFromParent  
4、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> obtainView->getView
                          ->setupChild -> attachViewToParent    
                                       ->child.measure
                                       ->child.layout  

这次layout跟之前的区别主要是第二步和第四步。

listview动画错乱

listview的item如果在执行动画的同时,listview在滑动,我们知道listview滑动过程中,是会重用view的,所以可能本来针对position 为1的动画,跑到position为11的地方去了,所以我们得禁止这个view进入scrap,如何禁止?
setHasTransientState(true),让view进入暂态
setHasTransientState是API16引入的函数,在View里,下边是对他的介绍,主要是用于动画开始和结束,在开始的时候setHasTransientState(true),结束的时候setHasTransientState(false),在这之间就是暂态的。

常见用法如下,在动画开始的时候进入暂态,动画结束退出暂态。我们再对比listview的代码可以发现,进入暂态的view不会进入scrap,而是进入mTransientStateViewsById这个LongSparseArray内,这样就不会被重用而导致动画错乱了。

//Listview 的 OnItemClickListener 的内容
//本范例点了 item 后会淡出并删除该 item
public void onItemClick(AdapterView
 
  parent, final View view, int position, long id) {
    final String item = (String) parent.getItemAtPosition(position);
    ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
    anim.setDuration(500);
    view.setHasTransientState(true); //设为 true 宣告 item 要被追踪
    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            myListview.remove(item);
            adapter.notifyDataSetChanged(); //重新整理 listview
            view.setAlpha(1);
            view.setHasTransientState(false); //完成后设定回 false
        }
    });
    anim.start();
}

不可否认这是一种解决方法,但并不是好的解决方法,因为item都滑出去了,还在搞动画,没啥意义,真正好的方法是什么,如下所示,在onMovedToScrapHeap里面停止动画,这才是最合适的。

listView.setRecyclerListener(new RecyclerListener() {
        @Override
        public void onMovedToScrapHeap(View view) {
            // Stop animation on this view
        }
});

hasStableIds

adapter有个接口叫hasStableIds,这个有什么意义,我查了资料和代码。发现hasStableIds一般情况下都是false,只有2个情况是true。
什么样的adapter是stable的,个人以为是里面的数据的id不变化,数据可以变化,但是id不能变化。
首先,如果要用到listview的选中功能时,只有hasStableIds返回true,才能通过getCheckedItemIds方法才能正常获取用户选中的选项的id(当然adapter内必须复写getItemId)。
还有个地方就是CursorAdapter,因为cursor是sql查询的结果,所以说是stable的无可厚非。CursorAdapter里面的hasStableIds就是返回true的。
总的来说hasStableIds没啥用,我也没看到改为true能优化什么。

问题

addViewInLayout和attachViewToParent有什么区别呢

addViewInLayout和attachViewToParent两者接收的参数是一样的,主要功能也相似,也就是往ViewGroup的view数组里添加View, 但是调用addViewInLayout会使被添加的View在界面上添加时会有动画效果呈现。两者的使用场景差别也很明显了:一般来说某一个View第一次添加进ViewGroup时比较适合调用addViewInLayout,而以后同一个View再次被添加时则适合使用attachViewToParent。因为一般情况想我们会希望进入的动画效果执行一次就够了,而不需要多次执行。
具体可参考http://www.itdadao.com/articles/c15a444236p0.html

scap有数量限制吗

在滑动过程中scrap是没限制的,但是在layout的过程中调用scrapActiveViews->pruneScrapViews,在这里会把mScrapViews内的每组缓存,都限制在mActiveViews.length大小。

scrap里的view会被去掉吗

为什么要考虑这个问题呢?因为有的view创建成本很高,我们不希望重复创建,什么情况下会重复创建呢?那就是view离屏,进scrap,scrap裁剪,被裁剪的view就没有了,下次必须重新inflate出来。
我看了下要想remove scrap里的view,只有pruneScrapViews和clear方法,clear在设置setAdapter(ListAdapter)和onDetachedFromWindow()时会被调用。而pruneScrapViews是在layout过程中被调的。所以主要看pruneScrapViews。这里可以看到其实处理scrap长度的方法是比较粗暴的,查一下mActiveViews.length,任何一个scrap堆都不准超过这个长度,否则直接截尾。于是我又看了下mActiveViews,每次显示在界面上的view都会丢到mActiveViews里,又发现mActiveViews只会变长不会变短。这就有意思了,比如当前页面有5个item,那mActiveViews.length就是5,待会当前页面有10个item了,那mActiveViews.length就是10,再过一块又只有3个item了,那mActiveViews.length还是10(只增不减)。要注意一点只有在layout的时候才会更新mActiveViews.length,如果只是滑来滑去是不会触发layout的。所以如果,mActiveViews.length值比较小,而scrap的item又很多的话,会进入到L10,进行裁剪(一般会发生在header高度比较大的情况下),这种裁剪方式其实是比较奇怪的,凭什么根据mActiveViews.length来裁剪。
所以scrap里的view是有可能被丢弃的,但是如果某个scap堆里只有一个view,那放心,他绝不会被丢弃。
另外如果我们希望缓存的view数量多一些的话,我们可以在view比较多的时候掉一遍requestLayout,这样让他更新mActiveViews.length

     final int maxViews = mActiveViews.length;
            final int viewTypeCount = mViewTypeCount;
            final ArrayList[] scrapViews = mScrapViews;
            for (int i = 0; i < viewTypeCount; ++i) {
                final ArrayList scrapPile = scrapViews[i];
                int size = scrapPile.size();
                final int extras = size - maxViews;
                size--;
                for (int j = 0; j < extras; j++) {
                    removeDetachedView(scrapPile.remove(size--), false);
                }
            }

其他

1、layoutChildren必定调用invalidate
2、initAbsListView内设置ListView本身可以点击即可以消耗父View分发的事件: setClickable(true);
3、我们常常用的convertView实际上来自scrapView

ref

http://blog.csdn.net/sinyu890807/article/details/44996879
https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/ListView%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md
http://www.itdadao.com/articles/c15a444236p0.html
http://www.cnblogs.com/qiengo/p/3628235.html

http://www.eoeandroid.com/thread-303373-1-1.html?_dsign=6a0c274f
http://edscb.blogspot.com/2013/09/animation-listview-animations.html

ListView单选和多选模式完全解析

你可能感兴趣的:(listview源码学习)