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方法,
<pre name="code" class="java"> @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<View>[] scrapViews = new ArrayList[viewTypeCount];
6339 for (int i = 0; i < viewTypeCount; i++) {
6340 scrapViews[i] = new ArrayList<View>();
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<View> 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的集合过大,导致内存溢出。
Viewholder是用来缓存控件对象,降低findViewById的次数( findViewById是很消耗内存的);每一个ViewHolder对象的字段都和convertView对应的布局里的空间一一对应, convertView又有系统的Recycler替我们存着, 所以, getView里拿出来的时候,布局对应着convertView, 控件又一一对应着ViewHolder的字段对象。
测试代码:
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<MyData> datas = new ArrayList<MyData>(); 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<MyData> mDatas; private LayoutInflater mInflater; public MyAdapter(ArrayList<MyData> 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; } } }