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

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

而我的布局很简单:


    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

        android:id="@+id/first_column"

        android:layout_width="100dp"

        android:layout_height="match_parent"

        android:text="first column"/>

        android:id="@+id/second_column"

        android:layout_width="0dp"

        android:layout_height="match_parent"

        android:layout_weight="1"

        android:orientation="vertical">

            android:id="@+id/recyclerview"

            android:layout_width="match_parent"

            android:layout_height="match_parent"/>

```

```

public class MyActivityextends Activity {

private RecyclerViewmRecyclerView;

    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(R.layout.my_layout);

        initView();

        setRecyclerView();

    }

private void initView() {

mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);

    }

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():

```

ViewHoldertryGetViewHolderForPositionByDeadline(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);

    if (ALLOW_THREAD_GAP_WORK) {

// only bother finding nested RV if prefetching

        RecyclerView innerView =findNestedRecyclerView(holder.itemView);

        if (innerView !=null) {

holder.mNestedRecyclerView =new WeakReference<>(innerView);

        }

}

long end = getNanoTime();

    mRecyclerPool.factorInCreateTime(type, end - start);

    if (DEBUG) {

Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");

    }

}

// 省略部分代码

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

+ exceptionLabel());

    }

final int offsetPosition =mAdapterHelper.findPositionOffset(position);

    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

}

```

注意到在第一次布局的时候,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;

    }

LayoutParams params = (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 (VERBOSE_TRACING) {

TraceCompat.beginSection("LLM LayoutChunk");

        }

layoutChunk(recycler, state, layoutState, layoutChunkResult);

        if (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到此处:

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

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

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

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中有如下代码:

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

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

也不会再走onLayoutChildren等接下来的流程了.


再分析为何传入onMeasure的heightSpec为0,在布局中RecycleView的父布局为LinearLayout,recyclerview的heightSpec由父布局measure时传入,

在LinearLayout#measureHorizontal中有如下代码:


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

/**

* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED

* will automatically get a size of 0. Older apps expect this.

*

* @hide internal use only for compatibility with system widgets and older apps

*/

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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容