您的位置:首页 > 其它

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

2015-10-10 11:16 357 查看
在《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 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的实现方法》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: