前些日子在github上遇到了一个有趣的问题,是在一个叫“xRecyclerView”的项目里的issue,一哥们说他给列表加两个head就会报错,而我则不会。随后又有两个哥们表示他们也有同样的问题:
java.lang.IllegalArgumentException: called detach on an already detached child ViewHolder{11ae0c3d position=2 id=-1, oldPos=-1, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}...
于是我贴上了我的代码:
View view1 = LayoutInflater.from(activity).inflate(layoutId1, null);
View view2 = LayoutInflater.from(activity).inflate(layoutId2, null);
xRecyclerView.addHeaderView(view1);
xRecyclerView.addHeaderView(view2);
而其中两个哥们的错误代码如下:
headerRecommend=LayoutInflater.from(getActivity()).inflate(R.layout.fragment_find_item_recommend, (ViewGroup) mView.findViewById(android.R.id.content), false);
headerRank=LayoutInflater.from(getActivity()).inflate(R.layout.fragment_find_item_rank, (ViewGroup) mView.findViewById(android.R.id.content), false);
rvTipsRecommendList.addHeaderView(headerRecommend);
rvTipsRecommendList.addHeaderView(headerRank);
headerview = new CustomizeHeaderLayout(this);
headerview1 = new CustomizeHeader1Layout(this);
headerview2 = new CustomizeHeader2Layout(this);
listview.addHeaderView(headerview);
listview.addHeaderView(headerview1);
listview.addHeaderView(headerview2);
public CustomizeHeaderLayout(Context context) {
super(context);
init();
}
private void init() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.layout_customize_headview, null);
...
}
先看源码,recyclerView和XRecyclerView的源码。在recyclerView的源码里抛出这个异常的地方是这样判断的:
public void detachViewFromParent(int offset) {
if (vh != null) {
if (vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("called detach on an already"
+ " detached child " + vh);
}...
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
}
这个vh.isTmpDetached()和!vh.shouldIgnore()是一些c里面的二进制运算,先放在一边。我们回过头看log,可以看到vh对象打印出的东西有点不一样,作者应该是重写了toString,一找果然是,
@Override public String toString() { final StringBuilder sb = new StringBuilder("ViewHolder{" + Integer.toHexString(hashCode()) + " position=" + mPosition + "id="+ mItemId + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); if (isScrap()) { sb.append(" scrap ") .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); } if (isInvalid()) sb.append(" invalid"); if (!isBound()) sb.append(" unbound"); if (needsUpdate()) sb.append(" update"); if (isRemoved()) sb.append(" removed"); if (shouldIgnore()) sb.append(" ignored"); if (isTmpDetached()) sb.append(" tmpDetached"); if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")"); if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); if (itemView.getParent() == null) sb.append(" no parent"); sb.append("}"); return sb.toString(); }
我的正常的log打出的是
ViewHolder{335f5b75 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} ViewHolder{1e435ef3 position=1 id=-1, oldPos=-1, pLpos:-1 no parent} ViewHolder{f598161 position=2 id=-1, oldPos=-1, pLpos:-1 no parent}
对比可以得出结论是,我的在isScrap()和isTmpDetached()这两个判断是始终没有走的,而他们两个都走到这个判断里面去了。那就是这个isScrap()和isTmpDetached()里有问题!再结合上文判断异常部分的代码,isTmpDetached()这个方法显得诡异了许多。
通过我的有道神器,知道了Detached是‘分离’的意思,那detachViewFromParent就可以理解为‘从父view中分离子view’吧,如果不报错,还会addFlags(ViewHolder.FLAG_TMP_DETACHED),虽然我看不懂大神的二进制算法,但是现在我好像可以从字里行间里隐约感觉到是怎么回事了。那就是在调用了detachViewFromParent之后,使isTmpDetached()成立,又一次调用了这个方法,就报了这个错误!想明白的瞬间背上似乎有了层凉气,在凌晨2点的夜里还是蛮吓人的...但我还是不明白detachViewFromParent的调用机制,归根结底我还是没有明白这个bug出现的原因,只能明天再做调查了。
ps:有个挪威小哥也有同样的问题:
https://github.com/martijnvdwoude/recycler-view-merge-adapter/issues/4
但我没有看太懂
——2017-3-1 2:32