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; }
- RecyclerView 的坑 1 Added View has RecyclerView as parent but view is not a real child. Unfiltered in
- Added View has RecyclerView as parent but view is not a real child. Unfiltered index:0
- RecycleView异常Added View has RecyclerView as parent but view is not a real child. Unfiltered index:0
- RecyclerView The specified child already has a parent
- 关于RecyclerView的java.lang.IllegalStateException: The specified child already has a parent. You must c
- error:Parent view is not a TextView
- Parent view is not a TextViewd的解决办法
- RecyclerView The specified child already has a parent. You must call removeView() on the child's pa
- 关于addView方法的使用--Exception:the special child alread has a parent,please call remove view
- 关于向父view添加相同的view问题!The specified child already has a parent. You must call removeView() on the chil
- 实习杂记(22)being added, but it already has a parent
- Android-View-RecyclerView
- TableRow中child view的match parent失效的问题
- java.lang.IllegalStateException: Could not find method onClickcrea(View) in a parent or ancestor Con
- Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has be
- Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has be
- Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has be
- 使用RecyclerView实现多样的View
- 632. Smallest Range
- Javascript函数表达式
- css属性
- 求大神指导
- kmp理解篇
- RecyclerView 的坑 1 Added View has RecyclerView as parent but view is not a real child. Unfiltered in
- shell命令之echo
- 将手机页面左右滚动固定
- secondarynamenode
- 区间调度问题(贪心)
- MVC与单元测试实践之健身网站(八)-统计分析
- Qt 工程 pro文件
- L1-023. 输出GPLT
- ubuntu screen 实用命令