前言
本文从源码角度出发学习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<View>[] mScrapViews;
private ArrayList<View> mCurrentScrap = mScrapViews[0];;
为什么是个数组呢?数组的每一项都是个ArrayList<View>,代表着某个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.
* <p>
* 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<View>[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList<View> 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