帶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閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容