RecyclerView与ListView对比浅析(二):View缓存篇

来源:互联网 发布:cdn加端口 编辑:程序博客网 时间:2024/05/09 04:54

上一篇链接:RecyclerView与ListView对比浅析(一):初始化篇

(二)View缓存篇

1. AbsListView(源码版本4.4)

RecyclerBin是AbsListView中专门处理View缓存的类,官方注释中说明其存有两组View——ActiveViews和ScrapViews,前者是当前Layout中正在显示的View,后者是已在屏幕范围外可重用的View,还有一组TransientStateViews属于ScrapViews的特殊情况,在查找View时会用到。

变量:

mRecyclerListener:当发生View回收时,mRecyclerListener若有注册,则会通知给注册者.RecyclerListener接口只有一个函数onMovedToScrapHeap,指明某个view被回收到了scrapheap. 该view不再被显示,任何相关的昂贵资源应该被丢弃。该函数是处理回收时view中的资源释放。

mFirstActivePosition:存储在mActiveViews中的第一个view的位置,即getFirstVisiblePosition。

mActiveViews:布局开始时屏幕显示的view,这个数组会在布局开始时填充,布局结束后所有view被移至mScrapViews。

mScrapViews:可以被适配器用作convertview的无序view数组。这个ArrayList就是adapter中getView方法中的参数convertView的来源。注意:这里是一个数组,因为如果adapter中数据有多种类型,那么就会有多个ScrapViews。

mViewTypeCount:view类型总数,列表中可能有多种数据类型,比如内容数据和分割符。

mCurrentScrap:跟mScrapViews的区别是,mScrapViews是个队列数组,ArrayList<View>[]类型,数组长度为mViewTypeCount,而默认ViewTypeCount= 1的情况下mCurrentScrap=mScrapViews[0]。

下面三个参数分别对应addScrapView中scrapHasTransientState的三个情况(这种情况的View一般用于动画播放):

mTransientStateViews:如果数据未变化,则可在老位置进行复用

mTransientStateViewsById:如果Adapter有StableID,则可以对相同的数据复用View

mSkippedScrap:其他情况下,只能Remove掉再Create了。

 

    下面分析下RecyclerBin在AbsListListView的调用场景:

   (1)onLayout中,如果changed为真,会调markChildrenDirty():为每个ScrapView和TransientStateView调用forceLayout()。forceLayout()是将mScrapView中回收回来的View设置一样标志,在下次被复用到ListView中时,告诉viewroot重新layout该view。forceLayout()方法只是设置标志,并不会通知其parent来重新layout。

        public void markChildrenDirty() {            if (mViewTypeCount == 1) {                final ArrayList<View> scrap = mCurrentScrap;                final int scrapCount = scrap.size();                for (int i = 0; i < scrapCount; i++) {                    scrap.get(i).forceLayout();                }            } else {                final int typeCount = mViewTypeCount;                for (int i = 0; i < typeCount; i++) {                    final ArrayList<View> scrap = mScrapViews[i];                    final int scrapCount = scrap.size();                    for (int j = 0; j < scrapCount; j++) {                        scrap.get(j).forceLayout();                    }                }            }            if (mTransientStateViews != null) {                final int count = mTransientStateViews.size();                for (int i = 0; i < count; i++) {                    mTransientStateViews.valueAt(i).forceLayout();                }            }            if (mTransientStateViewsById != null) {                final int count = mTransientStateViewsById.size();                for (int i = 0; i < count; i++) {                    mTransientStateViewsById.valueAt(i).forceLayout();                }            }        }

   (2)onDetachedFromWindow中,会调clear():移除所有mScrapViews和mTransientViews里的缓存View

   (3)检测滑动函数trackMotionScroll中,调用addScrapView(child,position)把滑动时离开屏幕范围的View加入到相应类型的ScrapViews中,当View有TransientState的时候,根据上面变量描述中说明的情况分别存到不同List中。执行完add的循环后,如果有add操作即count>0,调removeSkippedScrap():移除SkippedScrap中所有Detached的View,然后clear掉SkippedScrap。

   (4)handleDataChanged中,调clearTransientStateViews:清掉mTransientStateViews和mTransientStateViewsById里存的View。(这里有个TODO,可以用带有StableID的Adapter来取代两个List)

   (5)obtainView,这是RecyclerBin的主要使用场景,用于获取符合条件的缓存View,传入Adapter的getView进行处理。先看源码

    View obtainView(int position, boolean[] isScrap) {        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");        isScrap[0] = false;        View scrapView;        scrapView = mRecycler.getTransientStateView(position);        if (scrapView == null) {            scrapView = mRecycler.getScrapView(position);        }        View child;        if (scrapView != null) {            child = mAdapter.getView(position, scrapView, this);            if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {                child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);            }            if (child != scrapView) {                mRecycler.addScrapView(scrapView, position);                if (mCacheColorHint != 0) {                    child.setDrawingCacheBackgroundColor(mCacheColorHint);                }            } else {                isScrap[0] = true;                // 清除所有系统管理的Transient状态以便回收并绑定到其他数据上                if (child.isAccessibilityFocused()) {                    child.clearAccessibilityFocus();                }                child.dispatchFinishTemporaryDetach();            }        } else {            child = mAdapter.getView(position, null, this);            if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {                child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);            }            if (mCacheColorHint != 0) {                child.setDrawingCacheBackgroundColor(mCacheColorHint);            }        }        if (mAdapterHasStableIds) {            final ViewGroup.LayoutParams vlp = child.getLayoutParams();            LayoutParams lp;            if (vlp == null) {                lp = (LayoutParams) generateDefaultLayoutParams();            } else if (!checkLayoutParams(vlp)) {                lp = (LayoutParams) generateLayoutParams(vlp);            } else {                lp = (LayoutParams) vlp;            }            lp.itemId = mAdapter.getItemId(position);            child.setLayoutParams(lp);        }        if (AccessibilityManager.getInstance(mContext).isEnabled()) {            if (mAccessibilityDelegate == null) {                mAccessibilityDelegate = new ListItemAccessibilityDelegate();            }            if (child.getAccessibilityDelegate() == null) {                child.setAccessibilityDelegate(mAccessibilityDelegate);            }        }        Trace.traceEnd(Trace.TRACE_TAG_VIEW);        return child;    }

具体过程如下:

a. 先调getTransientStateView(position)找TransientStateView,看Adapter有没有StableId,有就从byId的List里找,没有就从mTransientStateView里找。

b. 如果拿到了scrapView,连同position传入Adapter的getView,拿到一个回调的View——child。如果child不等于scrapView,则把它add进ScrapViews,再设下CacheColorHint;如果相等,移除它的TransientState,以便进行回收

c. 如果没拿到scrapView,给Adapter的getView传个null进去,让其在内部新建View并返回,拿到返回View——child,设下CacheColor。

d. 如果Adapter有StableId,再设下child的LayoutParams,这里ItemId也作为LayoutParams的变量存入。

e. 返回child。


2. ListView(源码版本 4.4)

我们还是从RecyclerBin的调用来看ListView里的View缓存部分:

   (1)Adapter变更后,Recycler就要重置,setAdapter中,先调clear()重置Recycler,调完super.setAdapter后,调setViewTypeCount(typeCount)初始化:有几种type,就new几个ArrayList<View>()。

        public void setViewTypeCount(int viewTypeCount) {            if (viewTypeCount < 1) {                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");            }            //noinspection unchecked            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];            for (int i = 0; i < viewTypeCount; i++) {                scrapViews[i] = new ArrayList<View>();            }            mViewTypeCount = viewTypeCount;            mCurrentScrap = scrapViews[0];            mScrapViews = scrapViews;        }


   (2) LayoutChildren中分三步,第一步:如果数据改变,则把当前所有childView加进ScrapView,如果没改变,那就用当前的View填满ActiveViews;第二步:添加View到ListView;第三步:回收ActiveViews中未用到的View到ScrapView中。在第一步中,如果是addScrapView,则所有的view将会detach,如果是fillActiveViews,则不会detach,只有在第三步中,未用到的view才会detach。

    @Override    protected void layoutChildren() {        final boolean blockLayoutRequests = mBlockLayoutRequests;        if (blockLayoutRequests) {            return;        }        mBlockLayoutRequests = true;        try {            super.layoutChildren();            invalidate();            if (mAdapter == null) {                resetList();                invokeOnItemScrollListener();                return;            }            final int childrenTop = mListPadding.top;            final int childrenBottom = mBottom - mTop - mListPadding.bottom;            final int childCount = getChildCount();            int index = 0;            int delta = 0;            ……            // 第一步:所有子View放入RecycleBin,可能的话会进行重用            final int firstPosition = mFirstPosition;            final RecycleBin recycleBin = mRecycler;            if (dataChanged) {                for (int i = 0; i < childCount; i++) {                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);                }            } else {                recycleBin.fillActiveViews(childCount, firstPosition);            }            //清除旧View            detachAllViewsFromParent();            recycleBin.removeSkippedScrap();            switch (mLayoutMode) {             ……            default:            // 第二步,默认LayoutMode下的填充操作                if (childCount == 0) {                    if (!mStackFromBottom) {                        final int position = lookForSelectablePosition(0, true);                        setSelectedPositionInt(position);                        sel = fillFromTop(childrenTop);                    } else {                        final int position = lookForSelectablePosition(mItemCount - 1, false);                        setSelectedPositionInt(position);                        sel = fillUp(mItemCount - 1, childrenBottom);                    }                } else {                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {                        sel = fillSpecific(mSelectedPosition,                                oldSel == null ? childrenTop : oldSel.getTop());                    } else if (mFirstPosition < mItemCount) {                        sel = fillSpecific(mFirstPosition,                                oldFirst == null ? childrenTop : oldFirst.getTop());                    } else {                        sel = fillSpecific(0, childrenTop);                    }                }                break;            }            // 第三步:把上面没用到的ActiveView都丢到ScrapViews中            recycleBin.scrapActiveViews();           ……    }

   (3)makeAndAddView,这个函数拿到View放到ChildView的List中。如果数据没有更新,使用ActiveViews里的View,这些View不需要重新Measure即可使用。如果数据改变了,调父类的obtainView拿View,这里会new或者重用。

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,            boolean selected) {        View child;        if (!mDataChanged) {            // 首先看能不能用现成的View            child = mRecycler.getActiveView(position);            if (child != null) {                // 找到,剩下就只需要设它的position了                setupChild(child, position, y, flow, childrenLeft, selected, true);                return child;            }        }        // 调obtainView,有可能新建View或在缓存View中重用        child = obtainView(position, mIsScrap);        // 这种View不仅需要设position,还需要measure        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);        return child;    }

   (4)scrollListItemsBy,这个函数用于对子view滑动一定距离,添加view到底部或者移除顶部的不可见view。当可见的第一个ItemView划出List顶或最后一个itemView划出List底时,调shouldRecycleViewType判断是否需要回收,如果需要就调addScrapView进行回收。

    private void scrollListItemsBy(int amount) {        offsetChildrenTopAndBottom(amount);        final int listBottom = getHeight() - mListPadding.bottom;        final int listTop = mListPadding.top;        final AbsListView.RecycleBin recycleBin = mRecycler;       if (amount < 0) {        ……            // 首个View超出顶部的情况            View first = getChildAt(0);            while (first.getBottom() < listTop) {                AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {                    recycleBin.addScrapView(first, mFirstPosition);                }                detachViewFromParent(first);                first = getChildAt(0);                mFirstPosition++;            }        } else {            ……            // 末位View超出底部的情况            while (last.getTop() > listBottom) {                AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {                    recycleBin.addScrapView(last, mFirstPosition+lastIndex);                }                detachViewFromParent(last);                last = getChildAt(--lastIndex);            }        }    }

    从上面可以看出,Android中view回收的计算是其父view中不再显示的,如果scrollview中包含了一个wrap_content属性的listview,里面的内容并不会有任何回收,引起listview 的getheight函数获取的是一个足以显示所有内容的高度。


3. RecyclerView(源码版本 5.1.1)

RecyclerView直接继承的ViewGroup,没有使用ListVIew的RecyclerBin,而是重新定义了一个自己的回收类Recycler,里面存储的不是View,而是ViewHolder。下面来详细分析一下:

3.3.1 变量:

mAttachedScrap:这里并没有ActvieViews的概念,而是用AttachedScrap来代替,AttachedScrap也没有用ArrayList数组来存储不同类型的ViewHolder,只用了一个ArrayList<ViewHolder>。

mChangedScrap:与AttachedScrap对应,表示数据已改变的ViewHolder。

mCachedViews:这是RecyclerView缓存的第二层,在内存中存储待重用的缓存ViewHolder,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可动态设置。

mUnmodifiableAttachedScrap:只在getScrapList作为返回量返回,看字面意思是不可变的AttachedScrap。

mRecyclerPool:这是RecyclerView缓存的第三层,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入RecyclerViewPool中,其中用SparseArray<ArrayList<ViewHolder>>的结构分viewType存储ViewHolder。

mViewCacheExtension:这是开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现函数getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。

3.3.2 mRecycler的关键调用场景:

   (1)setAdapter中,如果与原Adapter不兼容或需强制重置,LayoutManager的实例mLayout会调removeAndRecycleAllViews(mRecycler)和removeAndRecycleScrapInt(mRecycler)重置mRecycler,mRecycler再调clear()重置。不管是否执行前面这步,mRecycler都会调onAdapterChanged进行处理,这个函数里会先调clear():清理mAttachedScraps、mCachedViews,把mCachedViews中的View都存入RecyclerPool;然后mRecyclerPool也会调onAdapterChanged,根据条件判断选择调用detach()、clear()和attach()。

    private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,            boolean removeAndRecycleViews) {        if (mAdapter != null) {            mAdapter.unregisterAdapterDataObserver(mObserver);            mAdapter.onDetachedFromRecyclerView(this);        }        if (!compatibleWithPrevious || removeAndRecycleViews) {            // 动画终止            if (mItemAnimator != null) {                mItemAnimator.endAnimations();            }            // 此时mLayout.children应该等于recyclerView.children            if (mLayout != null) {                mLayout.removeAndRecycleAllViews(mRecycler);                mLayout.removeAndRecycleScrapInt(mRecycler);            }            // 为保证回调正确,需清理            mRecycler.clear();        }        mAdapterHelper.reset();        final Adapter oldAdapter = mAdapter;        mAdapter = adapter;        if (adapter != null) {            adapter.registerAdapterDataObserver(mObserver);            adapter.onAttachedToRecyclerView(this);        }        if (mLayout != null) {            mLayout.onAdapterChanged(oldAdapter, mAdapter);        }        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);        mState.mStructureChanged = true;        markKnownViewsInvalid();    }

   (2)setLayoutManager中,如果是更换LayoutManager,则调mLayout的onDetachedFromWIndow进行分离,入参包括mRecycler。再调mRecycler的clear()重置。

   (3)addAnimatingView中,在存储View到mChildHelper之前,会对其所属ViewHolder进行unscrap操作,调unscrapView:根据holder是否已变等参数,在mAttachedScrap或mChangedScrap中移除holder,并清除Holder中“来自Scrap”的标志位returnedFromScrapFlag。removeAnimatingView中,若View变为hidden,则,调unscrapView处理Viewholder,然后调recycleViewHolderInternal(viewHolder):如果mCachedViews没满,则add进去,如果满了,就放到RecyclerPool。这是常见的缓存ViewHolder使用流程

    private void addAnimatingView(ViewHolder viewHolder) {        final View view = viewHolder.itemView;        final boolean alreadyParented = view.getParent() == this;        mRecycler.unscrapView(getChildViewHolder(view));        if (viewHolder.isTmpDetached()) {            //重新关联            mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);        } else if(!alreadyParented) {            mChildHelper.addView(view, true);        } else {            mChildHelper.hide(view);        }    }

3.3.3 getViewForPosition(position)

这个函数在RecyclerView的trace中占比较高,仅低于LayoutManager的操作和RecyclerView的Touch和Scroll,是寻找缓存View的主要过程。下面详细分析下:

 (1)如果mChangedScrap不为空,遍历mChangedScrap寻找合适的Holder(条件是mState.isPreLayout为真)

 (2)根据position,在Scrap里寻找。先从mAttachedScrap中遍历寻找合适的Holder,若未找到,则遍历mCachedViews寻找合适的holder,若都没有则返null。找到合适的Holder后,验证它的position和type,若验证失败,两种情况,如果isScrap为true,进行removeDetach操作,然后再unScrap,如果wasReturnedFromScrap,清除相关标志位,最后对Holder进行Recycle操作(先对holder进行一系列验证后,若符合条件:如果当前mCachedViews的size等于上限,那回收位置为0的holder,也就是最老的,回收后放入RecycledViewPool中;若不符合条件,直接放入RecycledViewPool);若验证成功,fromScrap置true。

  (3)如果上面没找到合适的,holder依旧为null,再看Adapter有没有StableId,有就根据Id在Scrap里找(这时ID和position相等)。

  (4)如果还没有合适的,看mViewCacheExtension,看解释像是开发者可以自定义的Cache,有就调它的getViewForPositionAndType方法找,这里需要开发者自己定义。

  (5)还没找到,就准备在RecyclerPool里找,如果找到,重置这个holder的多个标志位(如mPosition、mItemId等),如果FORCE_INVALIDATE_DISPLAY_LIST为true(SDK是19或20则为true),就invalidate一下ViewGroup

  (6)如果还没找到,就调Adapter的createViewHolder再BindViewHolder了。

View getViewForPosition(int position, boolean dryRun) {            if (position < 0 || position >= mState.getItemCount()) {                throw new IndexOutOfBoundsException("Invalid item position " + position                        + "(" + position + "). Item count:" + mState.getItemCount());            }            boolean fromScrap = false;            ViewHolder holder = null;            //有预Layout的话,先从ChangedScrap里找ViewHolder            if (mState.isPreLayout()) {                holder = getChangedScrapViewForPosition(position);                fromScrap = holder != null;            }            //在Scrap中根据位置找            if (holder == null) {                holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);                if (holder != null) {                    if (!validateViewHolderForOffsetPosition(holder)) {                        // recycle this scrap                        if (!dryRun) {                            // we would like to recycle this but need to make sure it is not used by                            // animation logic etc.                            holder.addFlags(ViewHolder.FLAG_INVALID);                            if (holder.isScrap()) {                                removeDetachedView(holder.itemView, false);                                holder.unScrap();                            } else if (holder.wasReturnedFromScrap()) {                                holder.clearReturnedFromScrapFlag();                            }                            recycleViewHolderInternal(holder);                        }                        holder = null;                    } else {                        fromScrap = true;                    }                }            }            if (holder == null) {                final int offsetPosition = mAdapterHelper.findPositionOffset(position);                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "                            + "position " + position + "(offset:" + offsetPosition + ")."                            + "state:" + mState.getItemCount());                }                final int type = mAdapter.getItemViewType(offsetPosition);                //如果Adapter有StableId,试着用ID找                if (mAdapter.hasStableIds()) {                    holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);                    if (holder != null) {                        // 更新位置                        holder.mPosition = offsetPosition;                        fromScrap = true;                    }                }                // 如果有自定义缓存,再在这里找                if (holder == null && mViewCacheExtension != null) {                    final View view = mViewCacheExtension                            .getViewForPositionAndType(this, position, type);                    if (view != null) {                        holder = getChildViewHolder(view);                        if (holder == null) {                            throw new IllegalArgumentException("getViewForPositionAndType returned"                                    + " a view which does not have a ViewHolder");                        } else if (holder.shouldIgnore()) {                            throw new IllegalArgumentException("getViewForPositionAndType returned"                                    + " a view that is ignored. You must call stopIgnoring before"                                    + " returning this view.");                        }                    }                }                if (holder == null) { // 回到Recycler中寻找                    //在回收池里寻找                                        if (DEBUG) {                        Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "                                + "pool");                    }                    holder = getRecycledViewPool()                            .getRecycledView(mAdapter.getItemViewType(offsetPosition));                    if (holder != null) {                        holder.resetInternal();                        if (FORCE_INVALIDATE_DISPLAY_LIST) {                            invalidateDisplayListInt(holder);                        }                    }                }                //都没找到,就只能Create了                if (holder == null) {                    holder = mAdapter.createViewHolder(RecyclerView.this,                            mAdapter.getItemViewType(offsetPosition));                    if (DEBUG) {                        Log.d(TAG, "getViewForPosition created new ViewHolder");                    }                }            }            boolean bound = false;            if (mState.isPreLayout() && holder.isBound()) {                // do not update unless we absolutely have to.                holder.mPreLayoutPosition = position;            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {                if (DEBUG && holder.isRemoved()) {                    throw new IllegalStateException("Removed holder should be bound and it should"                            + " come here only in pre-layout. Holder: " + holder);                }                final int offsetPosition = mAdapterHelper.findPositionOffset(position);                //绑定数据,执行更新                mAdapter.bindViewHolder(holder, offsetPosition);                attachAccessibilityDelegate(holder.itemView);                bound = true;                if (mState.isPreLayout()) {                    holder.mPreLayoutPosition = position;                }            }            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();            final LayoutParams rvLayoutParams;            if (lp == null) {                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();                holder.itemView.setLayoutParams(rvLayoutParams);            } else if (!checkLayoutParams(lp)) {                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);                holder.itemView.setLayoutParams(rvLayoutParams);            } else {                rvLayoutParams = (LayoutParams) lp;            }            rvLayoutParams.mViewHolder = holder;            rvLayoutParams.mPendingInvalidate = fromScrap && bound;            return holder.itemView;        }

3.3.4 缓存规则

   当一个ViewHolder需要缓存时一般需要下面几个判断步骤:

  (1)if(viewHolder.shouldIgnore()),如果为true则直接返回。

  (2)if(viewHolder.isInvalid()&& !viewHolder.isRemoved() && !viewHolder.isChanged()&& !mRecyclerView.mAdapter.hasStableIds()),如果为true,则走Recycle操作,如果为false,则走Scrap操作。

  (3)Recycle操作先判断(!holder.isInvalid()&& (mState.mInPreLayout || !holder.isRemoved())&&!holder.isChanged()),为true则判断mCachedViews是否已满,满了就退休调位置为0的View存入Recycler Pool,没满就把holder存入;为false就直接存Recycler Pool。

  (4)Scrap操作,先调holder.setScrapContainer(this),在自己的mScrapContainer中记录下自己已Scrap的状态,然后判断(!holder.isChanged()|| !supportsChangeAnimations()):true后再判断(holder.isInvalid() && !holder.isRemoved() &&!mAdapter.hasStableIds()),若满足则抛出异常,不满足则加进mAttachedScrap;false后,加进mChangedScrap。

4 0