带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析

在偶然的一次调试中,发现了RecyclerView的onCreateViewHolder和onBindViewHolder发生了多次调用:

带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析_第1张图片

而我的布局很简单:




    
    

    
        
    
 private void setRecyclerView() {
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(new RecyclerView.Adapter() {

        @Override
        public RecyclerView.ViewHolderonCreateViewHolder(ViewGroup parent, int viewType) {
            Log.d("NQG", "onCreateViewHolder: ");
            TextView textView =new TextView(MyActivity.this);
            textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200));
            return new RecyclerView.ViewHolder(textView) {};
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            Log.d("NQG", "onBindViewHolder: " + position);
            ((TextView) holder.itemView).setText("" + position);
        }

        @Override
        public int getItemCount() {
            return 20;
        }
    });
}

 这就很奇怪了,于是便从RecycleView开始下手,从onCreateViewHolder回溯,发现调用的地方在RecycleView.Recycler#tryGetViewHolderForPositionByDeadline():

    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                     boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        
        // 省略从缓存中取ViewHolder的相关代码
        ...
        
        if (holder == null) {
            long start = getNanoTime();
            if (deadlineNs != FOREVER_NS
                    && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                // abort - we have a deadline we can't meet
                return null;
            }
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
        
        // 省略部分代码
        ...
        
        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);
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }

        // 省略部分代码
        ...

        return holder;
    }

注意到在第一次布局的时候,ViewHolder没有成功从RecycleView的缓存中取到过一次,每次都是new出来的,RecycleView的缓存失效?不应该的,注意到在此时,onBindViewHolder的pos参数,显示每个item都执行了bind操作,猜想可能在RecycleView layout的时候,将所有item都进行了layout操作,虽然某些view是无法显示下的,再回溯注意到tryGetViewHolderForPositionByDeadline是由LinearLayoutManager#next方法调用的,而next是在LinearLayoutManager#layoutChunk中调用的:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                     LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        }

        //省略之后的代码
        ...
    }

很明显layoutChunk方法会根据一些参数,将item add到RecycleView中,而layoutChunk是由LinearLayoutManager#fill方法调用的:

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
             RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

其中while执行条件为:

1.layoutState.mInfinite为true或者RecycleView剩余的空间大于0

2.layoutState.hasMore(state)为true

先看第二个条件,layoutState.hasMore(state)代码如下:

    /**
     * @return true if there are more items in the data adapter
     */
    boolean hasMore(RecyclerView.State state) {
        return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
    }

很明显,判断是否到了最后一个item.

DeBug到此处:

带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析_第2张图片

注意while循环中的layoutState.mInfinite变量为true,这会造成将所有item view都add到RecycleView中,这就解释了为何调用

onCreateViewHolder和onBindViewHolder的次数与Adapter#getItemCount()值一致了.

那么接下来,问题来了,为何layoutState.mInfinite值为true呢?注意到LinearLayoutManager#fill方法由LinearLayoutManager#onLayoutChildren调用,其中有如下代码:

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 省略部分代码
        ...

        mLayoutState.mInfinite = resolveIsInfinite();

        // 省略部分代码
        ...
    }

    boolean resolveIsInfinite() {
        return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
                && mOrientationHelper.getEnd() == 0;
    }

最终是根据RecycleView.LayoutManager#mHeightMode == View.MeasureSpec.UNSPECIFIED判断的,而mHeightMode赋值的地方一处在设置LayoutManager的时候,另一处在调用RecycleView.LayoutManager#setMeasureSpecs的地方,在RecycleView#onMeasure中有如下代码:

带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析_第3张图片

很明显,传入onMeasure的heightSpec为0.

可能会奇怪了,为啥最终显示的效果是正确的呢?这是因为进行了多次测量后,skipMeasure变量值为true,便不会走RecycleView.LayoutManager#setMeasureSpecs流程,也不会再走onLayoutChildren等接下来的流程了.

带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析_第4张图片

再分析为何传入onMeasure的heightSpec为0,在布局中RecycleView的父布局为LinearLayout,recyclerview的heightSpec由父布局measure时传入, 在LinearLayout#measureHorizontal中有如下代码:

带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析_第5张图片

此时child就是RecycleView的父布局,注意到heightMeasureSpec,明显是有值的,但为何得到的值为0呢?那就要看View#makeSafeMeasureSpec方法了:

    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

很明显View#sUseZeroUnspecifiedMeasureSpec变量为true了,返回0了,注意到sUseZeroUnspecifiedMeasureSpec赋值的地方:

    // In M and newer, our widgets can pass a "hint" value in the size
    // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
    // know what the expected parent size is going to be, so e.g. list items can size
    // themselves at 1/3 the size of their container. It breaks older apps though,
    // specifically apps that use some popular open source libraries.
    sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;

 

噢,原来只要targetSdkVersion小于AndroidM就会为true,参考工程中AndroidManifest.xml:
uses-sdk android:minSdkVersion="17" android:targetSdkVersion="19"
的确为true,这下问题的来龙去脉就理清了.
解决这个问题,我目前想到了两个办法:
1).更改App的targetSdkVersion为M及以上(不适合我对应的场景)
2).将其LinearLayout父布局改为match_parent/match_parent(PS:RecycleView的直接或者间接LinearLayout父布局均不能带weight)

你可能感兴趣的:(带weight的LinearLayout嵌套RecyclerView导致RecycleView执行多次onCreateViewHolder和onBindViewHolder原因分析)