ListView适配器BaseAdapter之getView的原理--convertView源码分析Android5.1

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_COUNTRecycler会创建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        }
6492行,如果只有一种item布局,则直接返回缓存的View;6494行,先得到item的type,6494行,然后从mScrapViews中取出对应对应的缓存View的集合,至此,已经完全证明后来的猜想是正确的。这里6469行,调用了retrieveFromScrap
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的作用:

  
  
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;
		}
	}
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值