ListView乱谈之ListView中View重用的简单解析

来源:互联网 发布:360截图软件 编辑:程序博客网 时间:2024/05/02 14:07
在《ListView乱谈之ListView的滚动原理 》 一篇中算是详细的介绍了ListView的滚动原理,一句话简单的总结就是:在合适的时机适时地添加新的ItemView和删除原来旧的ItemView,同时对新的把新的View布局在紧邻的旧的ItemView之下或者之上。在这里进行删除原来的ItemView的时候对这些删除的ItemView进行了回收操作,以便下次反向滚动回来的时候重用之,提高ListView等的性能!下面一小段代码就是在向上滚动的时候,对上面滚动后而看不见的ItemView进行删除和回收。

 
 // 随着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 possiblefinal 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的实现方法》



0 0