1、出現(xiàn)的3個(gè)異常
1.1、第一個(gè),第二個(gè)
java.lang.IllegalArgumentException: Called attach on a child which is not detached: ViewHolder{3d34d42 position=4 id=-1, oldPos=-1, pLpos:-1} hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{12f2c6 VFED..... ......ID 0,427-720,1430 #7f0903c6 app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@58e3153, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@9c47a90, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@561df6
at androidx.recyclerview.widget.RecyclerView$5.attachViewToParent(RecyclerView.java:931)
at androidx.recyclerview.widget.ChildHelper.attachViewToParent(ChildHelper.java:241)
at androidx.recyclerview.widget.RecyclerView.addAnimatingView(RecyclerView.java:1443)
at androidx.recyclerview.widget.RecyclerView.animateDisappearance(RecyclerView.java:4371)
at androidx.recyclerview.widget.RecyclerView$4.processDisappeared(RecyclerView.java:617)
at androidx.recyclerview.widget.ViewInfoStore.process(ViewInfoStore.java:245)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4208)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3862)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView.onLayout(HyRecyclerView.java:1055)
java.lang.IllegalArgumentException: Called removeDetachedView with a view which"
+ " is not flagged as tmp detached." + vh + exceptionLabel()
出現(xiàn)位置與原因: ChildHelper
當(dāng)向RecyclerView添加一個(gè)child時(shí),這個(gè)child已經(jīng)有一個(gè)parent。
// mChildHelper = new ChildHelper
public void attachViewToParent(View child, int index,
ViewGroup.LayoutParams layoutParams) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("Called attach on a child which is not"
+ " detached: " + vh + exceptionLabel());
}
if (DEBUG) {
Log.d(TAG, "reAttach " + vh);
}
vh.clearTmpDetachFlag();
}
RecyclerView.this.attachViewToParent(child, index, layoutParams);
}
1.2、第三個(gè)、Cannot call this method while RecyclerView is computing a layout or scrolling HyRecyclerView。
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{62a0245 VFED..... ........ 0,131-1080,1996 #7f0903cd app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@2f22d9a, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@a1e86cb, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@8fe39bf
at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:3062)
at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5558)
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HeaderAndFooter.HeaderAndFooterRecyclerView$DataObserver.onItemRangeInserted(HeaderAndFooterRecyclerView.java:348)
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
at hy.sohu.com.ui_lib.hyrecyclerview.hyadapter.HyBaseNormalAdapter.addData(HyBaseNormalAdapter.java:163)
at hy.sohu.com.app.chat.view.message.ChatMsgBaseActivity.onSaveLocalSuccess(ChatMsgBaseActivity.kt:910)
出現(xiàn)位置與原因:
當(dāng)調(diào)用notifyItemChanged,notifyItemRemove時(shí)檢測(cè)mLayoutOrScrollCounter是否> 0 拋出異常。
private class RecyclerViewDataObserver extends AdapterDataObserver {
RecyclerViewDataObserver() {
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
}
void assertNotInLayoutOrScroll(String message) {
if (isComputingLayout()) {
if (message == null) {
throw new IllegalStateException("Cannot call this method while RecyclerView is "
+ "computing a layout or scrolling" + exceptionLabel());
}
throw new IllegalStateException(message);
}
}
public boolean isComputingLayout() {
return mLayoutOrScrollCounter > 0;
}
2、產(chǎn)生原因
為什么mLayoutOrScrollCounter > 0
2.1、recyclerView的onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
}
void dispatchLayout() {
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
2.2、看下 dispatchLayoutStep1/2
private void dispatchLayoutStep 1/2 () {
onEnterLayoutOrScroll();
mLayout.onLayoutChildren(mRecycler, mState);
onExitLayoutOrScroll();
}
void onEnterLayoutOrScroll() {
mLayoutOrScrollCounter++;
}
void onExitLayoutOrScroll(boolean enableChangeEvents) {
mLayoutOrScrollCounter--;
}
??從上面看出每次執(zhí)行 dispatchLayoutStep1/2開(kāi)始mLayoutOrScrollCounter++,執(zhí)行完成mLayoutOrScrollCounter--就等于0,期間執(zhí)行 mLayout.onLayoutChildren()操作。如果onLayoutChildren出現(xiàn)異常,則mLayoutOrScrollCounter就會(huì)大于0。
??而java.lang.IllegalArgumentException就是在mLayout.onLayoutChildren方法執(zhí)行過(guò)程中調(diào)用的,所以由于第一個(gè)異常未處理,產(chǎn)生了第二個(gè)異常,實(shí)際不可能同時(shí)出現(xiàn)兩個(gè)crash異常。
?? 原因 : 自定義的HyLinearLayoutManager被try——catch,導(dǎo)致第一個(gè)異常沒(méi)有導(dǎo)致app閃退,當(dāng)再次執(zhí)行onItemRangeRemoved時(shí),產(chǎn)生了第二個(gè)異常。所以只需要找到第一個(gè)問(wèn)題的原因則可。
// HyLinearLayoutManager
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
//try catch一下
super.onLayoutChildren(recycler, state);
} catch (Exception e) {
e.printStackTrace();
}
}
3、分析mLayout.onLayoutChildren(mRecycler, mState);
以dispatchLayoutStep2中onLayoutChildren方法為例介紹。

3.1、 notifyItemRemove(7)
正常情況下,是不會(huì)出現(xiàn)crash,下面是寫(xiě)demo可以驗(yàn)證。
btn1.setOnClickListener {
var stringList = (recyclerView.adapter as ItemAdapter).stringList as LinkedList
stringList.removeAt(1)
(recyclerView?.adapter as ItemAdapter).notifyItemRemoved(2)
Log.d(TAG, "onCreate:--after-- " + recyclerView.isComputingLayout)
}
btn2.setOnClickListener {
adapter.notifyDataSetChanged()
}
當(dāng)調(diào)用notifyItemRemove(7)時(shí),dispatchLayoutStep2會(huì)從mAttachedScrap加載混存的holder。
??3.11、尋找到第0個(gè),加入RecyclerView第0個(gè)。
??3.12、尋找到第1個(gè),加入RecyclerView第1個(gè)。
??3.13、尋找到第2個(gè),加入RecyclerView第2個(gè)。
??3.14、尋找到第3個(gè),此時(shí)第三個(gè)item的type和mAdapter返回的type不一致。所以找不到,會(huì)依據(jù)type創(chuàng)建一個(gè)holder。
//getScrapOrHiddenOrCachedHolderForPosition
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
//創(chuàng)建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
此時(shí)按照type,倒數(shù)第二個(gè)type為Item_Type_load,會(huì)返回一個(gè)holder,這個(gè)holder的itemview為mLoadView。加入RecyclerView為第三個(gè)holder。
@Override
public int getItemViewType(int position) {
if (isLoadPosition(position)) {
return ITEM_TYPE_LOAD;
} else if (isBottomPosition(position)) {
return ITEM_TYPE_BOTTOM;
}
return super.getItemViewType(position);
}
此時(shí)mAttachedScrap中有一個(gè)持有mLoadView的ViewHolder,RecyclerView中也加入了一個(gè)持有LoadView的ViewHolder。
??3.15、尋找第4個(gè),此時(shí)找到的事mLoadView的holder,但是這個(gè)holder的type和mAdapter返回type不一致,此時(shí)需要把holder移除。出現(xiàn)如下crash。因?yàn)閯偛盼覀円呀?jīng)加入到recyclerView,移除報(bào)錯(cuò)。
protected void removeDetachedView(View child, boolean animate) {
ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (vh.isTmpDetached()) {
vh.clearTmpDetachFlag();
} else if (!vh.shouldIgnore()) {
throw new IllegalArgumentException("Called removeDetachedView with a view which"
+ " is not flagged as tmp detached." + vh + exceptionLabel());
}
}
}
3.2、notifyItemRemove(3)
刪除3,但是我們通知3時(shí),當(dāng)notifyItemRemove(3)。

??3.21、尋找到第0個(gè),加入RecyclerView第0個(gè)。
??3.22、尋找到第1個(gè),加入RecyclerView第1個(gè)。
??3.23、尋找到第2個(gè),加入RecyclerView第2個(gè)。
??3.24、尋找到第3個(gè),此時(shí)mAttachedScrap中有個(gè)2個(gè)位置為3個(gè)holder,一個(gè)是移除holder,一個(gè)mLoadView(位置已經(jīng)做了正確的偏移),找到了holder。
??3.25、尋找到第4個(gè),也找到已經(jīng)偏移位置的line。加入到RecyclerView中,不會(huì)出現(xiàn)任何問(wèn)題。
3.3、notifyItemRemove(4)
??3.3.1、尋找到第0個(gè),加入RecyclerView第0個(gè)。
??3.3.2、尋找到第1個(gè),加入RecyclerView第1個(gè)。
??3.3.3、尋找到第2個(gè),加入RecyclerView第2個(gè)。
??3.3.4、尋找到第3個(gè),此時(shí)mAttachedScrap中找到的holder的type和mAdapter需要的type不一致,所以會(huì)創(chuàng)建一個(gè)holder加入進(jìn)去。我們上面知道加入的事mLoadView。
?? 此時(shí)mAttachedScrap和RecyclerView中各有一個(gè)持有同一個(gè)mLoadView的ViewHolder。
??3.3.5、尋找到第4個(gè),也找到已經(jīng)偏移位置的line。加入到RecyclerView中,不會(huì)出現(xiàn)任何問(wèn)題。因?yàn)樵谶@個(gè)位置找到了holder,不需要移除持有mLoadView的viewHolder,所以不會(huì)報(bào)第一個(gè)中的異常。
??3.3.6、執(zhí)行動(dòng)畫(huà)。

對(duì)于loadViewHolder執(zhí)行消失動(dòng)畫(huà)。
void animateDisappearance(@NonNull ViewHolder holder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
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);
}
}
mChildHelper.attachViewToParent就拋出了crash。因?yàn)閘oadViewHolder所持有的ItemView已經(jīng)被加入到了RecyclerView。
4、解決方案

public void removeData(String msgId) {
int index = -1;
for (Iterator iterable = getDatas().iterator(); iterable.hasNext(); ) {
ChatMsgBean chatMsgBaseBean = (ChatMsgBean) iterable.next();
index++;
if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBaseBean.msgId)) {
try {
iterable.remove();
notifyItemRemoved(index);
if (index > 0) {
notifyItemChanged(index - 1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
上述代碼執(zhí)行
??notifyItemRemoved(0) 執(zhí)行完只剩1條數(shù)據(jù)。
??notifyItemRemoved(1);
當(dāng)執(zhí)行notifyItemRemoved(1)時(shí),此時(shí)list只有一條數(shù)據(jù),就會(huì)導(dǎo)致上面notifyItemRemoved(4)的場(chǎng)景。
這里只列舉刪除,當(dāng)然增刪改查都可能導(dǎo)致這個(gè)問(wèn)題。
public void removeData(String msgId) {
int index = -1;
List<ChatMsgBean> datas = getDatas();
for (int i = 0; i < datas.size(); i++) {
ChatMsgBean chatMsgBean = datas.get(i);
if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBean.msgId)) {
index = i;
break;
}
}
if (index != -1) {
datas.remove(index);
notifyItemRemoved(index);
}
}
??4.1、避免每次創(chuàng)建的viewholder和緩存中的ViewHolder持有同一個(gè)View.
//PullToLoadAdapter
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_LOAD) {
mRealLoadView = createWrapView(mLoadView);
return new RecyclerView.ViewHolder(mRealLoadView) {
};
} else if (viewType == ITEM_TYPE_BOTTOM) {
mRealBottomView = createWrapView(mBottomView);
return new RecyclerView.ViewHolder(mRealBottomView) {
};
}
return super.onCreateViewHolder(parent, viewType);
}
// HeaderAndFooterAdapter
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 如果是頭部
if (isHeaderType(viewType)) {
int headerPosition = mHeaderViews.indexOfKey(viewType);
View headerView = mHeaderViews.valueAt(headerPosition);
// if (mOnCreateHeaderViewHolderListener != null && headerView != null
// && headerView.getTag() != null && headerView.getTag() instanceof Integer) {
// if ((int) headerView.getTag() > 0) {
// return mOnCreateHeaderViewHolderListener.onCreateHeaderViewHolder(parent, viewType, (int) headerView
// .getTag());
// }
// }
return createHeaderAndFooterViewHolder(createWrapView(headerView));
}
// 如果是placeHolder
if (isPlaceHolderType(viewType)) {
View view = LayoutInflater.from(mContext.getApplicationContext()).inflate(R.layout.placeholer_recyclerview, null,
false);
return new HyPlaceHolderView(view);
}
// 如果是尾部
if (isFooterType(viewType)) {
int footerPosition = mFooterViews.indexOfKey(viewType);
View footerView = mFooterViews.valueAt(footerPosition);
return createHeaderAndFooterViewHolder(createWrapView(footerView));
}
return mRealAdapter.onCreateViewHolder(parent, viewType);
}
protected FrameLayout createWrapView(View view) {
FrameLayout frameLayout = new FrameLayout(mContext);
ViewParent parent = view.getParent();
LogUtil.d("HyRecyclerView",
"onCreateViewHolder_0: " + parent + " " + view.getTag() + " params.h =" + view.getLayoutParams() + " " +
"visiable= " + view.getVisibility());
if (parent != null && parent instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) parent;
viewGroup.removeView(view);
}
frameLayout.setVisibility(view.getVisibility());
if (view.getLayoutParams() != null) {
ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
view.getLayoutParams().height);
frameLayout.setLayoutParams(frlayoutParams);
} else {
ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
frameLayout.setLayoutParams(frlayoutParams);
}
frameLayout.addView(view);
return frameLayout;
}
因?yàn)閠opView和mRefreshView用于刷新,bottomView和loadView用于加載,因此
添加了一層,在PullToRefreshRecyclerView和HyRecyclerView里面都需要替換控件,并且將bottomView等屬性賦值包裹的View。
??4.2、從上面可知,因?yàn)樵趯?duì)應(yīng)position沒(méi)有獲取到holder,所以新創(chuàng)建了一個(gè)。想辦法從mAttachscrap中獲取緩存。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
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 {
fromScrapOrHiddenOrCache = 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() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
}
處理給adapter的item設(shè)置唯一的ID,重寫(xiě)geiItemID方法。
mAdapter.setHasStableIds(true);
@Override
public long getItemId(int position) {
return super.getItemId(position);
}
總結(jié)
??添加header和footer時(shí),由于緩存復(fù)用,避免創(chuàng)建的Viewholder持有同一個(gè)View。