ListView.setAdapter后, List中每个item,要求 adapter 调用返回一个View 。如果我们有成千上万的item要显示怎么办?为每个item创建一个新视图是不可取的!实际上Android为为我们缓存了视图。Android中有个叫做Recycler的构件,下图是他的工作原理:
做了一个测试:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
int type = getItemViewType(position);
System.out.println("getView " + position + " " + convertView + " type = " + type);
if (convertView == null) {
holder = new ViewHolder();
switch (type) {
case TYPE_1:
convertView = mInflater.inflate(R.layout.item1, null);
holder.textView = (TextView)convertView.findViewById(R.id.text_1);
break;
case TYPE_2:
convertView = mInflater.inflate(R.layout.item2, null);
holder.textView = (TextView)convertView.findViewById(R.id.text_2);
break;
}
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.textView.setText(mData.get(position));
return convertView;
}
使用了两种布局,每隔20条TYPE_1的item, 加一个TYPE_2的item,并且屏幕中只能显示10条。
当position=0~9, 此时getView中执行的是的if(convertView == null)代码块中的case TYPE_1;当向上滑动, 出现position为10直到19,getView执行的是else代码块;但是,当执行当position = 20, 也就是出现TYPE_2的item的时候,初步猜测convertView应该还不为null,这样的话,就不会执行到case_2,导致界面不能出现预期效果,或者直接导致程序崩溃, 然而,此时convertView实实在在的为null. 接下来探究其原因:
根据结论继续猜测:在调用getView方法之前,会先调用getViewTypeCount() 和 getItemViewType方法,
@Override
public int getItemViewType(int position) {
return mDatas.get(position).getType() == 1 ? TYPE_1 : TYPE_2;
}
@Override
public int getViewTypeCount() {
return TYPE_COUNT;
}
getViewTypeCount()返回item布局类别个数TYPE_COUNT,Recycler会创建TYPE_COUNT个View缓存区域, 然后根据getItemViewType的返回值,把需要缓存的View放到哪个缓存区域, 以此决定从哪个缓存区域去取缓存的View, 然后传给getView 赋值给convertView,然而当第一次加载TYPE_2的item是,TYPE_2对应的缓存区域是没有View的, 所以convertView赋值为空.
查源码验证:
在BaseAdapter及其父类以及父接口中没找到getView方法的相关证据,换做从ListView及其父类着手, 在ListView类的在setAdapter方法中:
464 public void setAdapter(ListAdapter adapter) {
465 if (mAdapter != null && mDataSetObserver != null) {
466 mAdapter.unregisterDataSetObserver(mDataSetObserver);
467 }
468
469 resetList();
470 mRecycler.clear();
471
472 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
473 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
474 } else {
475 mAdapter = adapter;
476 }
477
478 mOldSelectedPosition = INVALID_POSITION;
479 mOldSelectedRowId = INVALID_ROW_ID;
480
481 // AbsListView#setAdapter will update choice mode states.
482 super.setAdapter(adapter);
483
484 if (mAdapter != null) {
485 mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
486 mOldItemCount = mItemCount;
487 mItemCount = mAdapter.getCount();
488 checkFocus();
489
490 mDataSetObserver = new AdapterDataSetObserver();
491 mAdapter.registerDataSetObserver(mDataSetObserver);
492
493 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
494
495 int position;
496 if (mStackFromBottom) {
497 position = lookForSelectablePosition(mItemCount - 1, false);
498 } else {
499 position = lookForSelectablePosition(0, true);
500 }
501 setSelectedPositionInt(position);
502 setNextSelectedPositionInt(position);
503
504 if (mItemCount == 0) {
505 // Nothing selected
506 checkSelectionChanged();
507 }
508 } else {
509 mAreAllItemsSelectable = true;
510 checkFocus();
511 // Nothing selected
512 checkSelectionChanged();
513 }
514
515 requestLayout();
516 }
看ListView.java的470行mRecycler.clear(),mRecycler就是文章开始说的缓存移除屏幕的item的构件,Recycler是ListView父类AbsListView的内部类,是在AbsListView中初始化的, 再看ListView.java 493行mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 这个方法的声明:
6333 public void setViewTypeCount(int viewTypeCount) {
6334 if (viewTypeCount < 1) {
6335 throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
6336 }
6337 //noinspection unchecked
6338 ArrayList[] scrapViews = new ArrayList[viewTypeCount];
6339 for (int i = 0; i < viewTypeCount; i++) {
6340 scrapViews[i] = new ArrayList();
6341 }
6342 mViewTypeCount = viewTypeCount;
6343 mCurrentScrap = scrapViews[0];
6344 mScrapViews = scrapViews;
6345 }
这个方法的作用是,有几种item布局,就创建几个View缓存区即List集合,然后存到数组中。
继续寻找getView方法调用的位置,也存在于在ListView父类AbsListView中有个方法obtainView,obtainView方法调用的位置是在getHeightForPosition方法(6830行):
6820 int getHeightForPosition(int position) {
6821 final int firstVisiblePosition = getFirstVisiblePosition();
6822 final int childCount = getChildCount();
6823 final int index = position - firstVisiblePosition;
6824 if (index >= 0 && index < childCount) {
6825 // Position is on-screen, use existing view.
6826 final View view = getChildAt(index);
6827 return view.getHeight();
6828 } else {
6829 // Position is off-screen, obtain & recycle view.
6830 final View view = obtainView(position, mIsScrap);
6831 view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED);
6832 final int height = view.getMeasuredHeight();
6833 mRecycler.addScrapView(view, position);
6834 return height;
6835 }
6836 }
这个方法是返回对应位置View的高度
Returns the height of the view for the specified position.。
obtainView方法的声明:
2316 View obtainView(int position, boolean[] isScrap) {
2317 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
2318
2319 isScrap[0] = false;
2320
2321 // Check whether we have a transient state view. Attempt to re-bind the
2322 // data and discard the view if we fail.
2323 final View transientView = mRecycler.getTransientStateView(position);
2324 if (transientView != null) {
2325 final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
2326
2327 // If the view type hasn't changed, attempt to re-bind the data.
2328 if (params.viewType == mAdapter.getItemViewType(position)) {
2329 final View updatedView = mAdapter.getView(position, transientView, this);
2330
2331 // If we failed to re-bind the data, scrap the obtained view.
2332 if (updatedView != transientView) {
2333 setItemViewLayoutParams(updatedView, position);
2334 mRecycler.addScrapView(updatedView, position);
2335 }
2336 }
2337
2338 // Scrap view implies temporary detachment.
2339 isScrap[0] = true;
2340 return transientView;
2341 }
2342
2343 final View scrapView = mRecycler.getScrapView(position);
2344 final View child = mAdapter.getView(position, scrapView, this);
2345 if (scrapView != null) {
2346 if (child != scrapView) {
2347 // Failed to re-bind the data, return scrap to the heap.
2348 mRecycler.addScrapView(scrapView, position);
2349 } else {
2350 isScrap[0] = true;
2351
2352 child.dispatchFinishTemporaryDetach();
2353 }
2354 }
2355
2356 if (mCacheColorHint != 0) {
2357 child.setDrawingCacheBackgroundColor(mCacheColorHint);
2358 }
2359
2360 if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
2361 child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
2362 }
2363
2364 setItemViewLayoutParams(child, position);
2365
2366 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2367 if (mAccessibilityDelegate == null) {
2368 mAccessibilityDelegate = new ListItemAccessibilityDelegate();
2369 }
2370 if (child.getAccessibilityDelegate() == null) {
2371 child.setAccessibilityDelegate(mAccessibilityDelegate);
2372 }
2373 }
2374
2375 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
2376
2377 return child;
2378 }
这个方法的作用是获得一个View对象,也许是返回一个已存在的,也许是返回一个新new出来的。
getView方法中convertView的来源,是AbsListView.java中obtainView方法2329行传入的,再看2323行final View transientView = mRecycler.getTransientStateView(position); 然而这个方法得到的transientView最终来自于mRecycler.addScrapView(scrapView, position); 所以可以跳过transientView,直接看scrapView,即2343行和2344行,mRecycler.getScrapView()声明:
6490 View getScrapView(int position) {
6491 if (mViewTypeCount == 1) {
6492 return retrieveFromScrap(mCurrentScrap, position);
6493 } else {
6494 final int whichScrap = mAdapter.getItemViewType(position);
6495 if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
6496 return retrieveFromScrap(mScrapViews[whichScrap], position);
6497 }
6498 }
6499 return null;
6500 }
6743 private View retrieveFromScrap(ArrayList scrapViews, int position) {
6744 final int size = scrapViews.size();
6745 if (size > 0) {
6746 // See if we still have a view for this position or ID.
6747 for (int i = 0; i < size; i++) {
6748 final View view = scrapViews.get(i);
6749 final AbsListView.LayoutParams params =
6750 (AbsListView.LayoutParams) view.getLayoutParams();
6751
6752 if (mAdapterHasStableIds) {
6753 final long id = mAdapter.getItemId(position);
6754 if (id == params.itemId) {
6755 return scrapViews.remove(i);
6756 }
6757 } else if (params.scrappedFromPosition == position) {
6758 final View scrap = scrapViews.remove(i);
6759 clearAccessibilityFromScrap(scrap);
6760 return scrap;
6761 }
6762 }
6763 final View scrap = scrapViews.remove(size - 1);
6764 clearAccessibilityFromScrap(scrap);
6765 return scrap;
6766 } else {
6767 return null;
6768 }
6769 }
这里每次都是把缓存的View从集合remove并返回,这样做的目的是为了防止缓存View的集合过大,导致内存溢出。
测试代码:
public class ManiActivity extends ListActivity {
private static final int TYPE_1 = 0;
private static final int TYPE_2 = 1;
private static final int TYPE_COUNT = 2;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ArrayList datas = new ArrayList();
for (int i = 0; i < 80; i++) {
if (i % 20 == 0 && i != 0) {
datas.add(new MyData(TYPE_1, "item separator" + i));
} else {
datas.add(new MyData(TYPE_2, "item normal" + i));
}
}
MyAdapter adapter = new MyAdapter(datas);
setListAdapter(adapter);
}
private class MyAdapter extends BaseAdapter {
private ArrayList mDatas;
private LayoutInflater mInflater;
public MyAdapter(ArrayList datas) {
this.mDatas = datas;
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getItemViewType(int position) {
return mDatas.get(position).getType() == 1 ? TYPE_1 : TYPE_2;
}
@Override
public int getViewTypeCount() {
return TYPE_COUNT;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public MyData getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyData myData = mDatas.get(position);
ViewHolder holder = null;
int type = getItemViewType(position);
System.out.println("getView " + position + " " + convertView + " type = " + type);
if (convertView == null) {
holder = new ViewHolder();
switch (type) {
case TYPE_1:
convertView = mInflater.inflate(R.layout.item1, null);
holder.textView = (TextView) convertView.findViewById(R.id.text_1);
break;
case TYPE_2:
convertView = mInflater.inflate(R.layout.item2, null);
holder.textView = (TextView) convertView.findViewById(R.id.text_2);
break;
}
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.textView.setText(myData.getText());
return convertView;
}
}
public static class ViewHolder {
public TextView textView;
}
class MyData {
int type;
String text;
public MyData(int type, String text) {
super();
this.type = type;
this.text = text;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
}