Android常考问题(5)-ListView及其复用

12 篇文章 0 订阅
10 篇文章 0 订阅

ListView虽然已经几乎被RecycleView取代,但是其复用的核心思想还是很棒的,而且也经常在入门级面试中被提问。在看RecycleView的时候云里雾里的,就先理清ListView。这次的起因是因为RecycleView复用出现的严重bug。其实很久之前就捣鼓过一次ListView的复用问题,现在回过头看看真的是不可思议,我当时是怎么捣鼓出的多层嵌套的ListView的同时还解决了复用引发的问题?当年的我真厉害,连adapter都不知道是啥还弄了出来这些?。扯远了。

总而言之,这次干脆把两块的源码一起学习了。从简单点的ListView开始。

之前记录过ListView的控件使用,adapter的相关内容也比较简单,就不赘述了。

首先要知道ListView是个啥。android常常会出现OOM或者崩溃之类的情况。究其原因是因为内存溢出了。在一个列表项中,可能有成百上千个列表项,如果全部放进内存,为每一个列表项加载一个新的列表单元,内存吃不住,程序猿也吃不消啊。因此就有了ListView。它的主要目的就是不断复用减少相同单元占用的内存。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。

ListView最大的优点就在于复用过程。也就是RecycleBin。这个类里面的几个主要方法:

fillActiveViews(int childCount, int firstActivePosition)
第一个参数是view的数量,第二个参数是开始的view的位置。这个方法就用来将指定位置的元素放到ListView 中相应位置上。

void fillActiveViews(int childCount, int firstActivePosition) {
		if (mActiveViews.length < childCount) {
			mActiveViews = new View[childCount];
		}
		mFirstActivePosition = firstActivePosition;
		final View[] activeViews = mActiveViews;
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
			// Don't put header or footer views into the scrap heap
			if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
				// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
				// active views.
				// However, we will NOT place them into scrap views.
				activeViews[i] = child;
			}
		}
	}

getActiveView(int position) 
这个方法和前一个是对应的,用于获取position位置的元素,然后移除掉获取过的元素。

View getActiveView(int position) {
		int index = position - mFirstActivePosition;
		final View[] activeViews = mActiveViews;
		if (index >= 0 && index < activeViews.length) {
			final View match = activeViews[index];
			activeViews[index] = null;
			return match;
		}
		return null;
	}

addScrapView(View scrap)
当一个View确定要废弃掉的时候(比如滚动出了屏幕),这个方法就把View拿过来缓存。

void addScrapView(View scrap) {
		AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
		if (lp == null) {
			return;
		}
		// Don't put header or footer views or views that should be ignored
		// into the scrap heap
		int viewType = lp.viewType;
		if (!shouldRecycleViewType(viewType)) {
			if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
				removeDetachedView(scrap, false);
			}
			return;
		}
		if (mViewTypeCount == 1) {
			dispatchFinishTemporaryDetach(scrap);
			mCurrentScrap.add(scrap);
		} else {
			dispatchFinishTemporaryDetach(scrap);
			mScrapViews[viewType].add(scrap);
		}
 
		if (mRecyclerListener != null) {
			mRecyclerListener.onMovedToScrapHeap(scrap);
		}
	}

getScrapView(int position) 
就取出被废弃的view,废弃缓存中的View是没有顺序的,因此getScrapView()方法就直接获取尾部的一个scrap view进行返回。

View getScrapView(int position) {
		ArrayList<View> scrapViews;
		if (mViewTypeCount == 1) {
			scrapViews = mCurrentScrap;
			int size = scrapViews.size();
			if (size > 0) {
				return scrapViews.remove(size - 1);
			} else {
				return null;
			}
		} else {
			int whichScrap = mAdapter.getItemViewType(position);
			if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
				scrapViews = mScrapViews[whichScrap];
				int size = scrapViews.size();
				if (size > 0) {
					return scrapViews.remove(size - 1);
				}
			}
		}
		return null;
	}

setViewTypeCount()
这个方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。重写后有机会解决复用问题。

这是复用部分类的代码。接下来是ListView的绘制过程,这部分主要是布局的时候有别于其他view,因此其onLayout()方法很关键。它的布局方法继承了父类AbsListView。

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

逻辑上就是判断如果ListView发生了变化,changed就变为true,就强制重绘全部的子布局。在下面有一个layoutChildren()方法,然后在ListView中实现了这个方法。在layoutChildren()方法中主要工作就是子布局的绘制。然后就在setupChild()方法里面自顶部向下绘制,等到布局超出ListView的范围的时候就主动停止布局。因此不多解释代码了。最关键的一段代码在其中的obtainView()方法中。

/**
 * 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) {
	isScrap[0] = false;
	View scrapView;
	scrapView = mRecycler.getScrapView(position);
	View child;
	if (scrapView != null) {
		child = mAdapter.getView(position, scrapView, this);
		if (child != scrapView) {
			mRecycler.addScrapView(scrapView);
			if (mCacheColorHint != 0) {
				child.setDrawingCacheBackgroundColor(mCacheColorHint);
			}
		} else {
			isScrap[0] = true;
			dispatchFinishTemporaryDetach(child);
		}
	} else {
		child = mAdapter.getView(position, null, this);
		if (mCacheColorHint != 0) {
			child.setDrawingCacheBackgroundColor(mCacheColorHint);
		}
	}

可以看到一开始是从getScrapView()方法中获取一个View。在初始化整个ListView的时候肯定是没有废弃的view的,因此这里会拿到空值。于是就会去调用mAdapter里面的内容,也就是我们的adapter的getView()方法。

public View getView(int position, View convertView, ViewGroup parent){
    View view;
	if (convertView == null) {
		view = LayoutInflater.from(getContext()).inflate(resourceId, null);
	} else {
		view = convertView;
	}
}

我们的getView方法有三个参数:position,view和父控件。而在上面getView()方法的第二个参数是null,也就是没有布局,因此就调用LayoutInflater的inflate()方法加载一个布局,然后返回view。这样就说明第一次进入的时候每一个item都是初始化加载出来的。

接下来就是复用逻辑了

复用过程和之前区别不大,一个是加载的时候,会从当前位置开始加载,然后再加载它上面和下面的其他item,另一个就是不会再调用inflate方法再次布局了。这次就会返回一个true到setupChild()方法里面,告诉setupChild()方法这个view是已经被布局过的了,于是将一个之前detach的View重新attach到ViewGroup上。

这样复用过程就结束了。(算了,这次又没说清楚,怕是又连自己都看不懂了)

在图中会发现,下滑的时候元素0的item中把数据丢出去,装入了元素6的数据,就成了复用的过程。

这时候,复用的问题就出现了。假设元素中有个checkBox,我勾选了元素0,然后下滑,结果会发现元素6被勾上了?因为这里用的是元素0的checkBox,所以元素6同时也被勾选了。这是比较常见的问题,还记得几年前我就被这个搞懵了。解决方法有几个:

1.重写你的adapter,这是最麻烦的,但也是最实用最稳定的方法,把checkBox这种属性全部都和viewHolder绑定在一起,而不是和view绑定在一起,这样就能够实现在更改数据的时候checkBox属性随之更改。一般用setTag()方法绑定。

2.永不复用。别笑,这真的是一个方法,因为列表的item不多,干脆不再复用,这样基本上是靠损失性能换取bug的修复。当然,一般能不这么干还是别这么做了,确实太有风险了。

3.多重背景。这种checkBox就不适合这个方法了,也就是比如有三种类型的item的时候就干错做三种view,每次在adapter里判断情况选择不同的view来应对,另几个就隐藏起来。

顺便说一下Listview的优化方式:优化一:convertView的使用,主要优化加载布局问题,从上面分析就知道,多了convertView就实现了复用的关键之一。优化二;viewHolder的使用,缓存控件实例。(主要是减少findViewById()的开销)。优化三:滑动不载入图片(个人感觉不太好)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值