RecyclerView 的坑 1 Added View has RecyclerView as parent but view is not a real child. Unfiltered in

来源:互联网 发布:快递业务考试软件 编辑:程序博客网 时间:2024/06/06 01:30

前言

最近在用ReyclerView写模块化页面,每个模块视图部分作为一个小的Aapter,会发现一些RecyclerView的坑,在博客中进行一些总结,保持更新。

1、问题出现

打开RecyclerView页面,快速滚动crash Added View has RecyclerView as parent **

“Added View has RecyclerView as parent but view is not a real child. Unfiltered index: xx ” 使用的是LinearLayoutManger。

直接打开RecyclerView源码(24.2.1),在它的内部类LayoutManger中,可以搜到这个Crash,在addViewInt方法中报的crash,addViewInt方法是将childview添加到RecyclerView中,在添加之前要检查获取的childview是否合法,来看下面源码。

private void addViewInt(View child, int index, boolean disappearing) {            final ViewHolder holder = getChildViewHolderInt(child);            ************            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            // child 如果是从复用池里捞出来重用走这个逻辑            if (holder.wasReturnedFromScrap() || holder.isScrap()) {                if (holder.isScrap()) {                    holder.unScrap();                } else {                    holder.clearReturnedFromScrapFlag();                }                mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);                if (DISPATCH_TEMP_DETACH) {                    ViewCompat.dispatchFinishTemporaryDetach(child);                }            } else if (child.getParent() == mRecyclerView) {                 int currentIndex = mChildHelper.indexOfChild(child);                if (index == -1) {                    index = mChildHelper.getChildCount();                }                if (currentIndex == -1) {                    throw new IllegalStateException("Added View has RecyclerView as parent but"                            + " view is not a real child. Unfiltered index:"                            + mRecyclerView.indexOfChild(child));                }                if (currentIndex != index) {                    mRecyclerView.mLayout.moveView(currentIndex, index);                }            } else {                mChildHelper.addView(child, index, false);                lp.mInsetsDirty = true;                if (mSmoothScroller != null && mSmoothScroller.isRunning()) {                    mSmoothScroller.onChildAttachedToWindow(child);                }            }            if (lp.mPendingInvalidate) {                if (DEBUG) {                    Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);                }                holder.itemView.invalidate();                lp.mPendingInvalidate = false;            }        }

这个crash产生的直接原因很简单,就是持有的childview的父view已经是RecyclerView了,换句话说这个View已经在RecyclerView上,并且这个View的mChildHelper.indexOfChild(child)==-1。
mChildHelper.indexOfChild这个方法干了什么呢?看下面代码,首先会找到这个View在RecyclerView中的index,如果没找到直接返回-1;如果有这个View,再去mBuket中寻找,如果发现这个View是个隐藏View,则返回-1. Buket中记录了隐藏的View的index,什么View需要被隐藏呢?看下一个代码块,在开始做动画之前,这个“隐藏”并不是把View移除掉,而是把这些需要做动画的View做记录,并存在一个数组中,在做动画时特殊处理(脑补)。

int indexOfChild(View child) {        final int index = mCallback.indexOfChild(child);        if (index == -1) {            return -1;        }        if (mBucket.get(index)) {            if (DEBUG) {                throw new IllegalArgumentException("cannot get index of a hidden child");            } else {                return -1;            }        }        // reverse the index        return index - mBucket.countOnesBefore(index);    }

ChildHelper 利用Buket隐藏View

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

所以我们看到这个Crash如果满足上述几个条件就会发生:1、ChildView的Holder是新创建的,不是从复用池里捞出来;2、ChildView的父View是RecyclerView;3、ChildView 还在做动画。
为什么一个新创建的ViewHolder的View的Parent是RecyclerView,做着动画,还在被复用?按道理做着动画不能被复用啊!!

2 原因

原因肯定是我们用错了,写法有问题啊,分析一下RecyclerView的复用机制:

(1) ArrayList mChangedScrap:在视图范围内,且正在做动画的holder
(2)ArrayList mAttachedScrap:在视图范围内,除去做动画的Holder
(3)ArrayList mCachedViews:默认最大值为2,存储的是最近移出屏幕的ViewHolder,这里面的ViewHolder的属性全部保留,当读取缓存ViewHolder时优先从这里取,如果取到,可以避免再计算位置,LayoutParams。
(4)RecycledViewPool mRecyclerPool:主要用来缓存重置ViewHolder的对象

如果addViewInt这个函数接收的child是从mCachedViews或mRecyclerPool中取的,不会有问题,但偏偏是mAttachedScrap中的View,正常情况下addViewInt不会添加视图范围内部的View,所以addViewInt传入的View应该是调用onCreateViewHolder获取的。通过debug这个crash,查找调用堆栈,发现确实在复用池中没有找到对应类型的View,重新调用了onCreateViewHolder。

Crash产生原因是,当RecyclerView调用OnCreateViewHolder时,我们并没有重新生成一个View和ViewHolder,我们返回的View可能是一个之前已经存在的View,把这个View存储成了一个全局变量,在OnCreateViewHolder没有重新生成,而是把上次的View又返回了,导致出现这种问题。类似与下面写法,所以在RecyclerView的Adapter中,如果调用OnCreateViewHolder,一定不要偷懒,返回一个全新的对象吧

public View onCreateView(ViewGroup parent, int viewType) {            if (mRootView == null) {                mRootView = LayoutInflater.from(getContext()).inflate(R.layout.xxxxxx, null, false);                mContainer = (LinearLayout) mRootView.findViewById( R.id.container )            }            return mRootView ;        }

(3)你以为这就结束了吗

当我们写法有问题的时候:
为什么需要刚打开页面快速滚动才能复现?为什么网上一些其他的解决方法貌似也很管用:把Adapter的hasStableID设置false或者setItemAnimation为null。我们既然找到了真实原因,就再聊聊这些治标不治本的方法为什么管用。

解决以上问题的根源在于:当我们的ItemCount没发生改变时,为什么会重复调用OnCreateViewHolder,复用池为什么找不到这个View??如果复用池中可以找到,就不会有这个问题了。

原因是:当RecyclerView每次onLayout时,会将他的View都回收一遍,然后再重新计算添加,关键的步骤在LinearLayoutManger中scrapOrRecycleView方法里,这个方法负责回收子View。

里面有个条件判断非常关键——mAdapter.hasStableIds(),removeViewAt(index)和detachViewAt(index)两个处理方法不同。RemoveViewAt(index)会将View在RecyclerView中移除,并会回收到复用池中;detachViewAt(index)会把View放到 mAttachedScrap或mAttachedScrap中。

什么时候会放到复用池呢?当动画mItemAnimator null会立即放入,如果itemAnimation不为null,会在动画执行完毕后放入复用池。

执行动画的条件是什么?两个必要条件是mItemAnimator不为null,hasStabelID = true。mItemAnimator默认是DefaultItemAnimator,不为null;Adapter hasStableID默认应该是false。

所以为啥设置hasStabelID为false或者设置mItemAnimator为null会有奇效,但是这绝对不是永久解决方案,最好的方案是改变写法,把bug解除。

为啥当打开页面快速滑动才容易复现这个问题呢,因为动画执行时间很短,只有在做动画时,不断Scroll,让RecyclerView重新去计算视图,加载需要的View,才会走到addViewInt逻辑,才能触发问题。

private void scrapOrRecycleView(Recycler recycler, int index, View view) {            final ViewHolder viewHolder = getChildViewHolderInt(view);            if (viewHolder.shouldIgnore()) {                return;            }            if (viewHolder.isInvalid() && !viewHolder.isRemoved() &&                    !mRecyclerView.mAdapter.hasStableIds()) {                    // hasStableIds很关键                removeViewAt(index);                 recycler.recycleViewHolderInternal(viewHolder);            } else {                detachViewAt(index);                recycler.scrapView(view);                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);            }        }

ReyclerView当添加动画Finish时,会调用removeAnimation方法,这个时候View会被加入到复用池汇中。

private boolean removeAnimatingView(View view) {        eatRequestLayout();        final boolean removed = mChildHelper.removeViewIfHidden(view);        if (removed) {            final ViewHolder viewHolder = getChildViewHolderInt(view);            mRecycler.unscrapView(viewHolder);            mRecycler.recycleViewHolderInternal(viewHolder);        }        resumeRequestLayout(false);        return removed;    }
阅读全文
1 0
原创粉丝点击