PS:满打满算,差不多三个月没写博客了... 前一阵忙的不可开交... 总算是可以抽出时间研究研究其他事情了...
学习内容:
1.ListView 的复用机制
2.ViewHolder 的概念
1.ListView 的复用机制
ListView 是我们经常使用的一个控件, 虽然说都会用,但是却并不一定完全清楚 ListView 的复用机制,虽然在 Android 5.0 版本之后提供了 RecycleView 去替代 ListView 和 GridView,提供了一种插拔式的体验,也就是所谓的模块化。本篇主要针对 ListView 的复用机制进行探讨,因此就 提 RecycleView。昨天看了一下郭霖大神的 ListView 原理深度解析的一篇博客,因此学习了一段时间,自己也说一下自己的理解。
i.RecycleBin 的基本原理
首先需要说一下 RecycleBin 的基本原理,这个类也是实现复用的关键类。接着我们需要明确 ActiveView 的概念,ActivityView 其实就是在 UI 屏幕上可见的视图 (onScreenView),也是与用户进行交互的 View,那么这些 View 会通过 RecycleBin 直接存储到 mActivityView 数组当中,以便为了直接复用,那么当我们滑动 ListView 的时候,有些 View 被滑动到屏幕之外 (offScreen) View,那么这些 View 就成为了 ScrapView,也就是废弃的 View,已经无法与用户进行交互了,这样在 UI 视图改变的时候就没有绘制这些无用视图的必要了。他将会被 RecycleBin 存储到 mScrapView 数组当中,但是没有被销毁掉,目的是为了二次复用,也就是间接复用。当新的 View 需要显示的时候,先判断 mActivityView 中是否存在,如果存在那么我们就可以从 mActivityView 数组当中直接取出复用,也就是直接复用,否则的话从 mScrapView 数组当中进行判断,如果存在,那么二次复用当前的视图,如果不存在,那么就需要 inflate View 了。
这是一个总体的流程图,复用机制就是这样的。那么我们先来理解一下 ListView 第一次加载的时候都做了哪些工作,首先会执行 onLayout 方法。。
- /**
- * Subclasses should NOT override this method but {@link #layoutChildren()}
- * instead.
- */
- @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- mInLayout = true;
- if (changed) {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).forceLayout();
- }
- mRecycler.markChildrenDirty();
- }
- layoutChildren();
- mInLayout = false;
- }
这里可以看到 onLayout 方法会调用 layoutChildren() 方法,也就是对 item 进行布局的流程,layoutChildren() 方法就不进行粘贴了,代码量过长我们只需要知道,这是对 ListView 中的子 View 进行布局的一个方式就可以了,在我们第一次加载 ListView 的时候,RecycleBin 中的数组都没有任何的数据,因此第一次加载都需要 inflate View,也就是创建新的 View。并且第一次加载的时候是自顶向下对数据进行加载的,因此在 layoutChildren() 会执行 fillFromTop() 方法。fillFromTop() 会执行 filleDown() 方法。
- /**
- * Fills the list from pos down to the end of the list view.
- *
- * @param pos The first position to put in the list
- *
- * @param nextTop The location where the top of the item associated with pos
- * should be drawn
- *
- * @return The view that is currently selected, if it happens to be in the
- * range that we draw.
- *
- * @param pos:列表中的一个绘制的Item在Adapter数据源中对应的位置
- * @param nextTop:表示当前绘制的Item在ListView中的实际位置..
- */
- private View fillDown(int pos, int nextTop) {
- View selectedView = null;
- /**
- * end用来判断Item是否已经将ListView填充满
- */
- int end = (getBottom() - getTop()) - mListPadding.bottom;
- while (nextTop < end && pos < mItemCount) {
- /**
- * nextTop < end确保了我们只要将新增的子View能够覆盖ListView的界面就可以了
- *pos < mItemCount确保了我们新增的子View在Adapter中都有对应的数据源item
- */
- // is this the selected item?
- boolean selected = pos == mSelectedPosition;
- View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
- /**
- *将最新child的bottom值作为下一个child的top值,存储在nextTop中
- */
- nextTop = child.getBottom() + mDividerHeight;
- if (selected) {
- selectedView = child;
- }
- pos++;
- }
- return selectedView;
- }
这里存在一个关键方法,也就是 makeAndAddView() 方法,这是 ListView 将 Item 显示出来的核心部分,也是这个部分涉及到了 ListView 的复用
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
- View child;
- //判断数据源是否发生了变化.
- if (!mDataChanged) {
- // Try to use an exsiting view for this position
- //如果mActivityView[]数组中存在可以直接复用的View,那么直接获取,然后重新布局.
- 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;
- }
- }
- // Make a new view for this position, or convert an unused view if possible
- /**
- *如果mActivityView[]数组中没有可用的View,那么尝试从mScrapView数组中读取.然后重新布局.
- *如果可以从mScrapView数组中可以获取到,那么直接返回调用mAdapter.getView(position,scrapView,this);
- *如果获取不到那么执行mAdapter.getView(position,null,this)方法.
- */
- child = obtainView(position, mIsScrap);
- // This needs to be positioned and measured
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
这里可以看到如果数据源没有变化的时候,会从 mActivityView 数组中判断是否存在可以直接复用的 View,可能很多读者都不太明白直接复用到底是怎么个过程,举个例子,比如说我们 ListView 一页可以显示 10 条数据,那么我们在这个时候滑动一个 Item 的距离,也就是说把 position = 0 的 Item 移除屏幕,将 position = 10 的 Item 移入屏幕,那么 position = 1 的 Item 是不是就直接能够从 mActivityView 数组中拿到呢?这是可以的,我们在第一次加载 Item 数据的时候,已经将 position = 0~9 的 Item 加入到了 mActivityView 数组当中,那么在第二次加载的时候,由于 position = 1 的 Item 还是 ActivityView,那么这里就可以直接从数组中获取,然后重新布局。这里也就表示的是 Item 的直接复用。
如果我们在 mActivityView 数组中获取不到 position 对应的 View,那么就尝试从 mScrapView 废弃 View 数组中尝试去获取,还拿刚才的例子来说当 position = 0 的 Item 被移除屏幕的时候,首先会 Detach 让 View 和视图进行分离,清空 children,然后将废弃 View 添加到 mScrapView 数组当中,当加载 position = 10 的 Item 时,mActivityView 数组肯定是没有的,也就无法获取到,同样 mScrapView 中也是不存在 postion = 10 与之对应的废弃 View,说白了就是 mScrapView 数组只有 mScrapView[0] 这一项数据,肯定是没有 mScrapView[10] 这项数据的,那么我们就会这样想,肯定是从 Adapter 中的 getView 方法获取新的数据喽,其实并不是这样,虽然 mScrapView 中虽然没有与之对应的废弃 View,但是会返回最后一个缓存的 View 传递给 convertview。那么也就是将 mScrapView[0] 对应的 View 返回。总体的流程就是这样。
这里我们可以看到,ListView 始终只会在 getView 方法中 inflate 一页的 Item,也就是 new View 只会执行一页 Item 的次数。后续的 Item 通过直接复用和间接复用完成。
注意一种情况:比如说还是一页的 Item,但是 position = 0 的 Item 没有完全滑动出 UI,position = 10 的 Item 没有完全进入到 UI 的时候,那么 position = 0 的 Item 不会被 detach 掉,同样不会被加入到废弃 View 数组,这时 mScrapView 是空的,没有任何数据,那么 position = 10 的 Item 即无法从 mActivityView 中直接复用 View,因为是第一次加载。mActivityView[10] 是不存在的,同时 mScrapView 是空的,因此 position = 10 的 Item 只能重新生成 View,也就是从 getView 方法中 inflate。这里 obtainView 方法没有具体贴出,大家可以自己进去看看。obtainView 其实就是判断能否从废弃 View 中获取到 View, 获取到了则执行:
- if (scrapView != null) {
- child = mAdapter.getView(position, scrapView, this);
- }
这里是可以获取到,那么 getView 会传递 scrapView。否则的话:
- else {
- child = mAdapter.getView(position, null, this);
- }
获取不到就传递 null,这样就会执行我们定义的 Adapter 中的方法。
- @Override public View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = View.inflate(context, R.layout.list_item_layout, null);
- }
- return convertView;
- }
至于向上滑动会执行其他的一些方法,也就是自底向上铺满 ListView,同样也会直接或者间接复用控件。理解了复用的机制才是关键,因此向上滑基本就不难理解了。补充一点,RecycleBin 中还存在一个方法,setViewTypeCount() 方法。这个是针对 Adapter 中的 getViewTypeCount() 设定的。针对每一种数据类型,setViewTypeCount() 会为每种数据类型开启一个单独的 RecycleBin 回收机制。这里我们只需要知道就可以了。至于在郭神博客中看到 ListView 会 onLayout 多次,这是肯定的,由于 Android View 加载机制问题,子控件需要根据父控件的大小要重新测量大小,经过多次测量才能够显示在 UI 上。这是 View 测量多次的原因。至于 ListView 在多次布局的问题我就不进行赘余了,总之无论几次测量,ListView 是不会多次执行重复的逻辑的,也就是说数据不会有多份,只会存在一份数据。
这里也就是 ListView 复用的基本原理和 RecycleBin 的回收机制了。代码贴的很少,都是一些关键代码,没必要去一行一行的研究代码,毕竟和大神还差很大的一个档次。我们只需要知道这个执行过程和原理就可以了。
2.ViewHolder
最后说一说 ViewHolder 这个东西,很多 Android 学习者会把这个东西和 ListView 的复用机制搞混。这里 ViewHolder 也是在复用的时候进行使用,但是和复用机制是没太大关系的。
- @Override public View getView(int position, View convertView, ViewGroup parent) {
- final ViewHolder holder;
- ListViewItem itemData = items.get(position);
- if (convertView == null) {
- convertView = View.inflate(context, R.layout.list_item_layout, null);
- holder = new ViewHolder();
- holder.userImg = (ImageView) convertView.findViewById(R.id.user_header_img);
- holder.userName = (TextView) convertView.findViewById(R.id.user_name);
- holder.userComment = (TextView) convertView.findViewById(R.id.user_coomment);
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder) convertView.getTag();
- }
- holder.userImg.setImageResource(itemData.getUserImg());
- holder.userName.setText(itemData.getUserName());
- holder.userComment.setText(itemData.getUserComment());
- return convertView;
- }
- static class ViewHolder {
- ImageView userImg;
- TextView userName;
- TextView userComment;
- }
在实现 Adapter 的时候,我们一般会加上 ViewHolder 这个东西,ViewHolder 和复用机制和原理是无关的,他的主要目的是持有 Item 中控件的引用,从而减少 findViewById() 的次数,因为 findViewById() 方法也是会影响效率的,因此在复用的时候他起的作用是这个,减少方法执行次数增加效率。这里做个简单的提醒,别弄混就行。
来源: http://www.cnblogs.com/RGogoing/p/5554086.html