ListView乱谈之ListView中View重用的简单解析
2015-10-10 11:16
357 查看
在《ListView乱谈之ListView的滚动原理 》 一篇中算是详细的介绍了ListView的滚动原理,一句话简单的总结就是:在合适的时机适时地添加新的ItemView和删除原来旧的ItemView,同时对新的把新的View布局在紧邻的旧的ItemView之下或者之上。在这里进行删除原来的ItemView的时候对这些删除的ItemView进行了回收操作,以便下次反向滚动回来的时候重用之,提高ListView等的性能!下面一小段代码就是在向上滚动的时候,对上面滚动后而看不见的ItemView进行删除和回收。
RecycleBin是ListView的父类AbsListView的一个内部类,从上面的代码看,在进行itemView回收之前调用了shouldRecycleViewType进行判断这个itemView值不值得回收;满足回收标准就是ItemView.getLayoutParams().viewType>=0,viewType值是什么呢?追踪代码可以发现源代码中的注释是:它的值等于Adapter中getItemViewType(int)方法的返回值。这个方法在BaseAdapter中有实现,就简单的返回了0.所以在我们开发过程中如果让我们的Adapter继承BaseAdapter并且没有重写getItemViewType(int)方法的话,我们的Adapter产生的itemView是全部满足回收条件的!
之后调用了addScrapView,让我们看看这个方法实现了什么
其实仔细考虑下来似乎还有一种情况要回收,也就是当Adapter调用notifyChange的时候,因为调用了notifyChange方法会强制ListView进行重新布局,没理由不会对已经解析过的itemView进行缓存。至于这个假设成不成立,通过下面的说明就可以为知道答案。
在ListView的layoutChildren方法中有这么一段代码
这段代码很好理解:当ListView的数据发生变化的时候,具体的说是与ListView绑定的Adapter调用了notifyChange方法的时候if(dataChanged)条件成立 ;在ListView乱谈之ListView的布局这篇文章中我们知道ListView把它的页面中能看到的itemView都保存在了数组View[]
mChildren中,上for循环中的childCount就是mChildren数组的长度;如果数据没有发生变化,那么就调用RecycleBin的fillActiveViews方法,注意该方法传的第二个参数值是ListView的可视范围内第一个可以看到的ItemView的position,所以去看看这个方法是干什么的:
进行了上述处理之后会会调用fillUp或者fillDown方法进行布局(详细分析见ListView布局),fillDown方法有这么一段核心代码:
可以看到makeAndAddView方法中第一次使用了回收机制,如果数据没有发生改变,通过判断ActiveViews 列表里面有没有当前 活动view,有的话直接复用已经存在的view。这样的好处就是直接复用当前已经存在的view,不需要通过adapter.getview()里面获取子view。
,然后调用 recycleBin.scrapActiveViews();该方法的作用是:刷新缓存,将当前的ActiveVies 移动到 ScrapViews。
第二次使用回收机制是obtainView方法,该方法调用了Adapter里面的getView对象进行组装,
注意obtainView的方法,先从Recycle中获取对应position位置的scrapView,然后把这个传入到Adapter 的getView方法中,这也解释了我们优化listView的一个经典方法的由来:
到此为止关于RecycleBin的机制基本上就算是说完了,花了好几个小时整理资料调理总体上有点乱,有不当或者错误的地方欢迎批评指正;
其实总体涞水回收机制有这么几步:
1)把屏幕中的ItemView放在activeViews数组中,并清空mChildren数组
2)调用fillDown方法通过makeAndAddView把新的ItemView放mChildren数组中去,并重用了activeViews数组中的ItemView
3)吧activeView的ItemView方法scrapViews中去,在obtainView方法中通过scrapViews 进行view 的复用
总之回收机制还有好多可以研究的地方,本篇就写到此处,下一篇关于ListView的博客很简单,就是分析一个实用的横向的ListView以及它的实现原理,具体见以一篇博客
《ListView乱谈之修改ListView使其变成横向ListView的实现方法》
// 随着ListView向上滚动,把已经看不见的ItemView从mChildView从mChildren中删除 View first = getChildAt(0); while (first.getBottom() < listTop) { AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); //为了之后在逆向滚动回来需要把这些删除的View都通过recycleBin保存起来供以后生使用 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(first, mFirstPosition); } //删除随着滚动不可见的View detachViewFromParent(first); first = getChildAt(0); mFirstPosition++; }
RecycleBin是ListView的父类AbsListView的一个内部类,从上面的代码看,在进行itemView回收之前调用了shouldRecycleViewType进行判断这个itemView值不值得回收;满足回收标准就是ItemView.getLayoutParams().viewType>=0,viewType值是什么呢?追踪代码可以发现源代码中的注释是:它的值等于Adapter中getItemViewType(int)方法的返回值。这个方法在BaseAdapter中有实现,就简单的返回了0.所以在我们开发过程中如果让我们的Adapter继承BaseAdapter并且没有重写getItemViewType(int)方法的话,我们的Adapter产生的itemView是全部满足回收条件的!
之后调用了addScrapView,让我们看看这个方法实现了什么
: /** * 把要删除的View scrap进行回收, * @param scrap 要回收的那个View * @param 要回收的那个View的在ListView的position,也是Adapter getView方法的position参数的值 */ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { 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)) { return; } //此处有省略代码 // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { //如果不重写BaseAdapter的hasStableIds方法的话,这个if条件不成立 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<View>(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) {//如果Adapter里面的数据没有发生变化,当然可以毫不犹豫的利用之前删除过的itemView // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } //把scrap保存到mTransientSateViews中去 mTransientStateViews.put(position, scrap); } else { // Otherwise, we'll have to remove the view and start over. //删除这个View,并重新开始 if (mSkippedScrap == null) { mSkippedScrap = new ArrayList<View>(); }//如果数据发生变换,那么就讲这个ItemView保存在mSkipped中去 mSkippedScrap.add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else {//mScrapViews[viewType]是一个ArrayList<View>集合对象, /**** *mScrapViews是个集合对象的引用,它以viewType为数组索引,用来保存具有相同viewType类型的ItemView,viewType的返回值在BaseAdapter中 */ mScrapViews[viewType].add(scrap); } // Clear any system-managed transient state. //此处省略部分代码 } }
其实仔细考虑下来似乎还有一种情况要回收,也就是当Adapter调用notifyChange的时候,因为调用了notifyChange方法会强制ListView进行重新布局,没理由不会对已经解析过的itemView进行缓存。至于这个假设成不成立,通过下面的说明就可以为知道答案。
在ListView的layoutChildren方法中有这么一段代码
// Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; //如果数据发生了变化,也就是调用了Adapter调用了notifyChange方法的时候dataChange会成立 if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); }
这段代码很好理解:当ListView的数据发生变化的时候,具体的说是与ListView绑定的Adapter调用了notifyChange方法的时候if(dataChanged)条件成立 ;在ListView乱谈之ListView的布局这篇文章中我们知道ListView把它的页面中能看到的itemView都保存在了数组View[]
mChildren中,上for循环中的childCount就是mChildren数组的长度;如果数据没有发生变化,那么就调用RecycleBin的fillActiveViews方法,注意该方法传的第二个参数值是ListView的可视范围内第一个可以看到的ItemView的position,所以去看看这个方法是干什么的:
/*** * @param childCount ListView在可视范围内ItemView的个数,也就是mChildren数组中的长度 * @param firstActivePosition ListView可视范围内第一个可以看到的ItemView的position **/ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; 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; } } }回收机制的第一步就是屏幕的view 放在ActiveViews,然后通过对ActiveViews进行降级变成ScrapViews,然后通过scrapViews 进行view 的复用!说白了mActiveViews数组里面保存的也是mChildren里面的View,只不过相比较mScrapViews而言多了对itemView进行了非ITEM_VIEW_TYPE_HEADER_OR_FOOTER判断而已。仔细阅读layoutChildren方法,可以发现在把itemView回收保存之后,也就是紧接着上面的会调用detachAllViewsFromParent方法清空mChilren数组连的ItemView,以便让mChildRen数组用来保存滚动或者notifyChange之后的那些能显示在ListView中的ItemView
protected void detachAllViewsFromParent() { final int count = mChildrenCount; if (count <= 0) { return; } final View[] children = mChildren; mChildrenCount = 0; for (int i = count - 1; i >= 0; i--) { children[i].mParent = null;//解除对父View的绑定 children[i] = null; } }
进行了上述处理之后会会调用fillUp或者fillDown方法进行布局(详细分析见ListView布局),fillDown方法有这么一段核心代码:
while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; //将此child添加并布局到ListView中 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); //计算下一个child的layout方法的top值 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } //该参数用来表示Adapter中下一个child的位置,也就是getView方法中第一个参数 pos++; }每次layout的时候都得进行while循环,哪怕滚动很小的距离在进行layout的时候,都要执行while循环来进行对整个ListView的布局,之前在ListView可视范围内的ItemView也需要重新布局,比如之前Listview显示了position为1到5的itemView,随着滚动,ListView显示position为2到6的itemView,position 为2 、3、4、5的itemView(这些View为active的View)也需要在上述for循环中重新layout;那么问题就来了,2、3、4、5这些item之前已经通过Adapter的getView方法获取到了一次,那么还需要在对这些ItemView重新通过getView方法进行解析获取么?显然肯定不行,在上述代码中可以发现有makeAndAddView方法:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; //这个是第一次使用RecycleBin的地方 if (!mDataChanged) { //获取回收的View进行重用 child = mRecycler.getActiveView(position); if (child != null) {//调用setupChild方法进行布局 // 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 //调用obtainView去构造一个新的View出来,具体下面有分析 child = obtainView(position, mIsScrap); // This needs to be positioned and measured //对child进行测量和布局 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
可以看到makeAndAddView方法中第一次使用了回收机制,如果数据没有发生改变,通过判断ActiveViews 列表里面有没有当前 活动view,有的话直接复用已经存在的view。这样的好处就是直接复用当前已经存在的view,不需要通过adapter.getview()里面获取子view。
,然后调用 recycleBin.scrapActiveViews();该方法的作用是:刷新缓存,将当前的ActiveVies 移动到 ScrapViews。
// Flush any cached views that did not get reused above //清除缓存起来的并且没有使用过的View //把mActiviews里面的itemView保存到mScrapView中,基本的逻辑跟addScrapView方法类似 void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { //victim百度翻译之居然是受害者 final View victim = activeViews[i]; if (victim != null) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams(); final int whichScrap = lp.viewType; //清空操作 activeViews[i] = null; if (victim.hasTransientState()) { // Store views with transient state for later use. victim.dispatchStartTemporaryDetach(); if (mAdapter != null && mAdapterHasStableIds) {//此条件不成立 if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<View>(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim); } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // The data has changed, we can't keep this view. removeDetachedView(victim, false); } } else if (!shouldRecycleViewType(whichScrap)) { // Discard non-recyclable views except headers/footers. if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(victim, false); } } else { // Store everything else on the appropriate scrap heap. if (multipleScraps) { scrapViews = mScrapViews[whichScrap]; } victim.dispatchStartTemporaryDetach(); lp.scrappedFromPosition = mFirstActivePosition + i; scrapViews.add(victim); victim.setAccessibilityDelegate(null); if (hasListener) { mRecyclerListener.onMovedToScrapHeap(victim); } } } } pruneScrapViews(); } /** *减少ScrapViews数组的容量,确保mScrapViews数组的容量不会超过mActivewView集合的大小。 *这种情况会发生在Adapter没有回收它的itemViews * Makes sure that the size of mScrapViews does not exceed the size of * mActiveViews, which can happen if an adapter does not recycle its * views. Removes cached transient state views that no longer have * transient state. */ private void pruneScrapViews() { 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++) { (scrapPile.remove(size--), false); } } final SparseArray<View> transViewsByPos = mTransientStateViews; if (transViewsByPos != null) { for (int i = 0; i < transViewsByPos.size(); i++) { final View v = transViewsByPos.valueAt(i); if (!v.hasTransientState()) { removeDetachedView(v, false); transViewsByPos.removeAt(i); i--; } } } final LongSparseArray<View> transViewsById = mTransientStateViewsById; if (transViewsById != null) { for (int i = 0; i < transViewsById.size(); i++) { final View v = transViewsById.valueAt(i); if (!v.hasTransientState()) { removeDetachedView(v, false); transViewsById.removeAt(i); i--; } } } }
第二次使用回收机制是obtainView方法,该方法调用了Adapter里面的getView对象进行组装,
**/ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. //获取之前回收过的itemView,避免对itemView配置文件的重新解析过程 final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) {//瞬态的View,这个if条件可以不看 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)) {//基本上这个条件都会成立 //重新获取view,并把transientView当做convertView传入进去,所以我们在getView的时候会做if(convertView==null)的判断 //如果不进行非空判断的话,还是会对xml文件进行解析,那么重用View就没什么大的作用了 final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. //如果不对convertView进行非空判断的话,该条件成立 if (updatedView != transientView) { mRecycler.addScrapView(updatedView, position); } } // Scrap view implies temporary detachment. //说明该View isScrap[0] = true; //返回重用的View return transientView; } //从mScrapViews集合里面获取对应位置的view final View scrapView = mRecycler.getScrapView(position); //把scrapView作为convertView参数传到Adapter的getView方法中去 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; // Clear any system-managed transient state so that we can // recycle this view and bind it to different data. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } child.dispatchFinishTemporaryDetach(); } } //此处有省略代码 ..... return child; }
注意obtainView的方法,先从Recycle中获取对应position位置的scrapView,然后把这个传入到Adapter 的getView方法中,这也解释了我们优化listView的一个经典方法的由来:
ViewHolder viewHolder; if(convertView==null){ convertView = inflate(xxxx) viewHolder = new ViewHolder(); convertView.setTag(viewHolder) }else{ viewHolder = (ViewHolder)convertView.getTag(); }
到此为止关于RecycleBin的机制基本上就算是说完了,花了好几个小时整理资料调理总体上有点乱,有不当或者错误的地方欢迎批评指正;
其实总体涞水回收机制有这么几步:
1)把屏幕中的ItemView放在activeViews数组中,并清空mChildren数组
2)调用fillDown方法通过makeAndAddView把新的ItemView放mChildren数组中去,并重用了activeViews数组中的ItemView
3)吧activeView的ItemView方法scrapViews中去,在obtainView方法中通过scrapViews 进行view 的复用
总之回收机制还有好多可以研究的地方,本篇就写到此处,下一篇关于ListView的博客很简单,就是分析一个实用的横向的ListView以及它的实现原理,具体见以一篇博客
《ListView乱谈之修改ListView使其变成横向ListView的实现方法》
相关文章推荐
- unity3d资源管理
- ios-AutoLayout(自动布局代码控制)简单总结
- MATLAB之flipdim函数
- Node出错导致运行崩溃的解决方案
- HTML5硕士学习笔记
- Android解析xml文件
- 集群LB-LVS
- 嵌入式Linux驱动开发的知识图谱
- MooseFS之分布式文件系统应用场景
- Scala学习笔记13【trait之多重继承、AOP实战】
- android 修改主题背景色
- 在多文档中分割窗口添加滚动条并载入图片
- 高中毕业20周年(聚会 江城子*初见)
- 测试2
- HDU - 1495 非常可乐(BFS)
- 《深入理解计算机系统》读书笔记6--- 信号
- linux下查看cpu、内存和硬盘大小
- Chrome 中的 JavaScript 断点设置和调试技巧
- 运煤问题
- iOS 通过URL获取图片,并保存到本地