浅析ListView实现原理
2015-12-04 00:00
309 查看
摘要: 为什么我们在使用ListView的时候,要使用getView()中的convertView? 还是通过源码来分析这个问题。
Fast Way
当时好奇为什么要使用 covertView , 所以结合源码看了下ListView的实现原理,几年过去了,已经忘的差不多了,最近整理一下,为什么我们在代码中要这么使用ListView?covertView又是什么东东?
这里调用了一个观察者模式的通知方法,那么你继续跟踪下去,将会在AbsListView中发现这个Observer的定义(注意这里有几个继承关系,所以你可能不是那么好找):
ViewGroup的绘制过程,大慨有三个阶段,onMeasure,onLayout, onDraw, onMeasuer阶段中,各个子View将会计计算自己在父View中占用的宽高, 父View通过测量阶段获取到的宽高信息,负责把子View放到合适的位置,具体的过程不在这里详谈,我们从ListView的Layout事件看起,ListView是继承至AbsListView,AbsListView onLayout()会调用一个方法叫layoutChildren()这也是一个抽象方法,ListView实现了这个方法,下面我们将截取一段代码来分析:
上面出现了RecycleBin, ScrapView,ActiveView,这是什么东西?我们打开RecycleBin的源码看一看。
上面说的很清楚,RecycleBin的作用是帮助布局中的View的重用,它存储了两种类型的View:
mActiveViews 可以理解为现在显示在屏幕上的View
mScrapViews 我们终于找到了,这就是传回getView中covertView的来源,上面的作用也说的很清楚了,避免不必要的分配Views,优化性能。
Ok,已经找到根本原因了,那么ListView又是在什么时候调用的getView方法了,上面说了我们将从fillUp方法看起:
在这段代码中,又看到了makeAndAddView,该方法就是把View加到ListView中最终调用的方法,我们来看代码:
那么我们只有接着看obtainView,该方法中就调用了getView方法,注意看:
Ok,大概就这样,ListView个人觉得他的实现是Android中一个比较经典的东西,如果这的看懂的话,还有其他touch事件分发,ViewGroup绘制原理还是挺多的,有兴趣的同学可以深入的去看一下源码,绝对受益匪浅.
浅析ListView实现原理
--为什么要使用getview()中的convertView
2009的Google Io大会中有一个专门培训ListView使用的课程说到,使用ListView 最快最优化(Fast Way)的方式如下这段代码:Fast Way
``` public View getView(int position, View convertView, ViewGroup parent){ ViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.list_item, parent, false); holder = new ViewHolder(); holder.text = (TextView)convertView.findViewById(R.id.text); holder.icon = (ImageView)convertView.findViewById(R.id.icon); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.text.setText(DATA[position]); holder.icon.setImageBitmap(Icons[position]); return convertView; } public static class ViewHolder{ TextView text; ImageView icon; } ```
当时好奇为什么要使用 covertView , 所以结合源码看了下ListView的实现原理,几年过去了,已经忘的差不多了,最近整理一下,为什么我们在代码中要这么使用ListView?covertView又是什么东东?
为什么要使用covertView?
大家都知道,如果从网络,或者本地获取到了数据,我们通过调用**adapter.notifyDataSetChanaged()**来通知ListView数据已经发生改变,那么ListView就会把我们的数据更新到页面。我们将从这个方法看起,这个事件是怎么通知到ListView的?打开BaseAdapter的源码,我们看到notifyDataSetChanaged()的方法如下:/** * Notifies the attached observers that the underlying data has been changed * and any View reflecting the data set should refresh itself. */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); }
这里调用了一个观察者模式的通知方法,那么你继续跟踪下去,将会在AbsListView中发现这个Observer的定义(注意这里有几个继承关系,所以你可能不是那么好找):
class AdapterDataSetObserver extends DataSetObserver { private Parcelable mInstanceState = null; @Override public void onChanged() { mDataChanged = true; mOldItemCount = mItemCount; mItemCount = getAdapter().getCount(); // Detect the case where a cursor that was previously invalidated has // been repopulated with new data. if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null && mOldItemCount == 0 && mItemCount > 0) { AdapterView.this.onRestoreInstanceState(mInstanceState); mInstanceState = null; } else { rememberSyncState(); } checkFocus(); requestLayout(); //这里调用了requesetLayout 方法,那么ListView的绘制过程就开始了 } ... }
ViewGroup的绘制过程,大慨有三个阶段,onMeasure,onLayout, onDraw, onMeasuer阶段中,各个子View将会计计算自己在父View中占用的宽高, 父View通过测量阶段获取到的宽高信息,负责把子View放到合适的位置,具体的过程不在这里详谈,我们从ListView的Layout事件看起,ListView是继承至AbsListView,AbsListView onLayout()会调用一个方法叫layoutChildren()这也是一个抽象方法,ListView实现了这个方法,下面我们将截取一段代码来分析:
@Override protected void layoutChildren() { ...省略的代码... try { ...省略的代码... boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); //数据变化的处理,这里决定了mLayoutMode } // Handle the empty set by removing all views that are visible // and calling it a day //留这段代码在这里,是因为这是一个ListView很少见的crash,平时开发过程可能不会现, //上线以后就会出现偶发的crash,其实记住一点就好,如果Adapter中的数据发生了改变,就必须调用notifiyDateSetChangaed, //如果不调用,这个crash就会出现,我以前在环信的SDK中也发现过这问题 if (mItemCount == 0) { resetList(); invokeOnItemScrollListener(); return; } else if (mItemCount != mAdapter.getCount()) { throw new IllegalStateException("The content of the adapter has changed but " + "ListView did not receive a notification. Make sure the content of " + "your adapter is not modified from a background thread, but only from " + "the UI thread. Make sure your adapter calls notifyDataSetChanged() " + "when its content changes. [in ListView(" + getId() + ", " + getClass() + ") with Adapter(" + mAdapter.getClass() + ")]"); } /*************重点来了******************/ //这里如果数据发生了改变,将把所有现在的child放到scrapView中, //如果数据没有发生改变,将把所有现在的child放到activeViews中, //RecyCleBin是什么鬼?scrapView是什么鬼?activieViews又是什么鬼? // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); //这里开始把子View加到ListView中,好多种模式,具体我没看,下面会选fillUp这个函数来分析。 switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; default: if (childCount == 0) { if (!mStackFromBottom) { final int position = lookForSelectablePosition(0, true); setSelectedPositionInt(position); sel = fillFromTop(childrenTop); } else { final int position = lookForSelectablePosition(mItemCount - 1, false); setSelectedPositionInt(position); sel = fillUp(mItemCount - 1, childrenBottom); } } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0, childrenTop); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); ...省略的代码... } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } }
上面出现了RecycleBin, ScrapView,ActiveView,这是什么东西?我们打开RecycleBin的源码看一看。
RecycleBin
源码如下:/** * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the * start of a layout. By construction, they are displaying current information. At the end of * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that * could potentially be used by the adapter to avoid allocating views unnecessarily. * * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * @see android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is populated at the start of * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. * Views in mActiveViews represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; private ArrayList<View> mSkippedScrap; private SparseArray<View> mTransientStateViews; private LongSparseArray<View> mTransientStateViewsById; ....省略的代码..... }
上面说的很清楚,RecycleBin的作用是帮助布局中的View的重用,它存储了两种类型的View:
mActiveViews 可以理解为现在显示在屏幕上的View
mScrapViews 我们终于找到了,这就是传回getView中covertView的来源,上面的作用也说的很清楚了,避免不必要的分配Views,优化性能。
Ok,已经找到根本原因了,那么ListView又是在什么时候调用的getView方法了,上面说了我们将从fillUp方法看起:
/** * Fills the list from pos up to the top of the list view. * * @param pos The first position to put in the list * * @param nextBottom The location where the bottom of the item associated * with pos should be drawn * * @return The view that is currently selected */ private View fillUp(int pos, int nextBottom) { View selectedView = null; int end = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end = mListPadding.top; } while (nextBottom > end && pos >= 0) { // is this the selected item? boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); nextBottom = child.getTop() - mDividerHeight; if (selected) { selectedView = child; } pos--; } mFirstPosition = pos + 1; setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }
在这段代码中,又看到了makeAndAddView,该方法就是把View加到ListView中最终调用的方法,我们来看代码:
/** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; //如果数据没有变化,直接去ActiveView中拿,这是最快的。 if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } //如果数据变化了,新建一个View,或则如果可能的话去ScrapView中拿一个缓存。 // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); //这个方法负责,把View放到ListView中合适的位置,具体就不看代码了,有兴趣自己去看源码 // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
那么我们只有接着看obtainView,该方法中就调用了getView方法,注意看:
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the scrap heap, false if otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; //这个瞬时状态的View,看了下说明,大概就是item有动画等效果的,我们不关心这个 // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } // Scrap view implies temporary detachment. isScrap[0] = true; return transientView; } //看这里,我们先去哪一个scrapView final View scrapView = mRecycler.getScrapView(position); //看到没,这里调用了getView方法,去初始化我们的covertView,知道covertView怎么来的吧。 final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { //如果不相等,这种情况就是我们covertView为null的情况 if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else { isScrap[0] = true; //这个方法没有看到说明,大概就是绘制相关的 child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }
Ok,大概就这样,ListView个人觉得他的实现是Android中一个比较经典的东西,如果这的看懂的话,还有其他touch事件分发,ViewGroup绘制原理还是挺多的,有兴趣的同学可以深入的去看一下源码,绝对受益匪浅.
相关文章推荐
- 从源码安装Mysql/Percona 5.5
- 深入理解PHP7内核之FAST_ZPP
- 完美实现Android ListView中的TextView的跑马灯效果
- android上改变listView的选中颜色
- Flex 性能优化常用手法总结
- Delphi7中Listview的常用功能汇总
- Delphi控件ListView的属性及使用方法详解
- oracle 性能优化建议小结
- Lua性能优化技巧(一):前言
- Lua性能优化技巧(五):削减、重用和回收
- Lua性能优化技巧(三):关于表
- Lua性能优化技巧(四):关于字符串
- 浅析Ruby的源代码布局及其编程风格
- MySQL性能优化 出题业务SQL优化
- PowerShell脚本性能优化技巧总结
- SQL SERVER性能优化综述(很好的总结,不要错过哦)第1/3页
- MySQL Index Condition Pushdown(ICP)性能优化方法实例
- Ajax无刷新分页的性能优化方法
- android中ListView数据刷新时的同步方法
- Android提高之ListView实现自适应表格的方法