????在偶然的一次調試中,發現了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)