浅析ListView实现原理

浅析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:

  1. mActiveViews 可以理解为现在显示在屏幕上的View
  2. 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绘制原理还是挺多的,有兴趣的同学可以深入的去看一下源码,绝对受益匪浅.

转载于:https://my.oschina.net/lorcan/blog/539215

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值