今天项目中遇到了一个需求,就是listView中有多选功能,能实现礼品的批量赠送。自己写完后发现,由于listView的复用问题,导致多选实现不正常。所以在查了很多的资料和看了一些高手的博客后明白了原理和解决方式。在此总结。
listView的复用问题
先说listView的复用吧,由于个人的总结与理解不是很到位,这里引用查资料过程中看到的一个解释listView复用觉得很好的博客。
参考的listview复用分析原文
由原理图可以看出,listview的复用就是:
为了让listview减少在时间和内存上的消耗,Android提供了convertView参数(getView方法的第二个参数,不一定都叫convertView,个人认为叫rowView更好)来实现这一优化。当用户滑动列表时,原先可视的item被滚出屏幕变得不可视,而代表该行的java对象可以被新的可视行复用。也就是说如果列表在手机屏幕中一屏可见的行有7行,当第一行滑出屏幕时,底部新滑出来的第8行可以复用第1行的java对象(即通过item布局inflate出来的view),Android已经把第一行的布局缓存起来,作为可以复用的rowview。
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
ViewHolder viewHolder = null;
if(null == convertView){
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.item_testdisorderitem,null);
}
TextView brandChNameTv = (TextView) convertView.findViewById(R.id.item_chName_txt);
TextView brandEnNameTv= (TextView) convertView.findViewById(R.id.item_enName_txt);
//set data
return convertView;
}
代码片段1.3
之前我们说过除了inlating 布局文件和创建java对象对时间和内存的消耗都是昂贵的。使用findViewById()方法也相对地耗时。我们知道每个列表项都会调用getView方法,如果执行<代码片段1.3>那么每显示一个列表项rowView就要调用findViewById来查找各个view。多次findViewById不仅增加了时间消耗,也创建了更多的java对象,从而造成了expensive with regards to time and memory consumption。
为了解决这个问题,引入了View Holder Pattern。
以下对ViewHolder的描述来自于该文章Using lists in Android(ListView) 8.4.节的翻译:
View Holder pattern的使用减少了adapter中对findViewById()的调用。
ViewHoler类是adapter里自定义的一个(静态)内部类,他持有布局文件中相关view的引用。该ViewHolder的引用通过setTag()方法作为一个tag被指派给row view(convertView)。
如果我们接受了一个convertview对象,我们可以通过getTag()方法获取到ViewHolder的实例,然后经由该ViewHolder的引用指定新的属性给Views。
虽然这听起来很复杂,但是使用这种方式比使用findViewById在速度上提高了大约15%。
view holder pattern的目的即为减少findViewById()的调用次数,因为findViewById()这个方法比较耗时,至于两者之间的比较可以自行测试,测试方法可以参考农民伯伯的
因此有了现在的这种模型:
private class ViewHolder{
private TextView brandEnNameTv;
private TextView brandChNameTv;
private CheckBox followCheckBox;
}
@Override
public View getView(int i, View viewrow, ViewGroup viewGroup) {
ViewHolder viewHolder = null;
if(null == viewrow){
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
viewrow = inflater.inflate(R.layout.item_testdisorderitem,null);
viewHolder = new ViewHolder();
viewHolder.brandChNameTv = (TextView) viewrow.findViewById(R.id.item_chName_txt);
viewHolder.brandEnNameTv= (TextView) viewrow.findViewById(R.id.item_enName_txt);
viewrow.setTag(viewHolder);
}else {
viewHolder = (ViewHolder) viewrow.getTag();
}
BrandItemInfo brandItemInfo = (BrandItemInfo) getItem(i);
viewHolder.brandChNameTv.setText(brandItemInfo.getBrandChName());
viewHolder.brandEnNameTv.setText(brandItemInfo.getBrandEnName());
return viewrow;
}
代码片段1.4
当convertView为空时,创建ViewHolder,通过viewHolder.brandChNameTv = (TextView) convertView.findViewById(R.id.item_chName_txt);来持有布局文件中view的引用。通过setTag(Object)方法把convertView和viewHolder关联起来。当接收到一个converView后,通过getTag(Object)方法获取到viewHolder。这样不仅使用convertView做到了复用现有的views的目的,同时和convertView“绑定的”viewHolder也做到了被复用。
对于ListView、GridView相信大家都不陌生,重写个BaseView,实现对于的几个方法,然后就完成了我们的界面展示,并且在大部分情况下,我们加载特别多的Item也不会发生OOM,大家也都明白内部有缓存机制,都遇到过ItemView复用带来的一些问题,比如异步加载图片,最终造成界面显示的混乱,我们一般会使用setTag,然后回调显示时,避免造成混乱。
设想1:拿ListView为例,如果ListView的ItemView复用机制,所有的ItemView复用同一个,如果在多线程下载图片的情况下,可能最终只有最后一个View显示图片吧,因为你前面的设置setTag(url),后面马上就会将你的Tag的值覆盖掉,最终findViewByTag找到的都是最后一个。由此可见ListView缓存的不是一个,至少是一屏幕可显示的数量。也就是说ListView维护着一个ItemView的池子。
跟大家解释下,为啥缓存了一个屏幕的可显示最大的ItemView数量的池子,我们可能上千个ItemView,仅依靠Tag就能实现不混乱呢。
情景:屏幕每次显示7个Item,ListView一共1000个Item,每个Item上显示一张从网络下载的图片。
getView的代码大概是这样的:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
final String url = getItem(position);
View view;
if (convertView == null)
{
view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);
} else
{
view = convertView;
}
final ImageView photo = (ImageView) view.findViewById(R.id.photo);
// 给ImageView设置一个Tag,保证异步加载图片时不会乱序
photo.setTag(url);
new LoadImgTask(photo).execute(url);
return view;
}
下载完成图片,进行photo.getTag().equals(url)来防止图片显示的混乱。
如果我们打开界面,开启了7个线程去下载,此时缓存了这7个ItemView,现在滑动屏幕显示另外下一屏,此时7个ItemView都会复用,会把第一屏设置的Tag全部覆盖掉,没错就是覆盖掉了,又开启7个线程去下载图片,当第一屏的ItemView的图片下载完成后,如果直接findViewByTag然后设置图片会显示在第二屏上,就混乱了,所以一般在显示前都会判断photo.getTag().equals(url);确定了再显示,也就是说第一屏的ItemView图片下载完了,但是Tag被覆盖了,所以即使下载完成了,也不会有任何显示。这就解释了为什么我们防止混乱的代码需要那样去写。
好了,下面从源码角度看一眼ListView内部到底是如何进行缓存的:
跟着ListView,进入父类AbsList,会发现这样一个变量:
/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/
final RecycleBin mRecycler = new RecycleBin();
注释的意思上用一个数据集来存储应当在下一个布局重用的View,避免重新创建新的布局。这个对象应该就是对我们缓存管理的核心类了。继续看这个类,这是一个内部类:
/**
* 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 View[] mActiveViews = new View[0]; private ArrayList<View>[] mScrapViews; <span style="white-space:pre"> </span> .... }
大概意思:这个类是用来帮助在滑动布局时重用View的,RecycleBin包含了两个级别的存储,ActiveViews和ScrapViews,ActiveViews存储的是第一次显示在屏幕上的View;所有的ActiveViews最终都会被移到ScrapViews,ScrapViews存储的是有可能被adapter复用的View。
现在很明确了AbsListView缓存依赖于两个数组,一个数组存储屏幕上当前现实的ItemView,一个显示从屏幕下移除的且可能会被复用的ItemView。下面看ListView里面的代码:
@Override
protected void layoutChildren()
{
if (dataChanged)
{
for (int i = 0; i < childCount; i++)
{
recycleBin.addScrapView(getChildAt(i));
}
} else
{
recycleBin.fillActiveViews(childCount, firstPosition);
}
....
}
[java] view plaincopy在CODE上查看代码片派生到我的代码片
/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount The minimum number of views mActiveViews should hold
* @param firstActivePosition The position of the first view that will be stored in
* mActiveViews
*/
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);
activeViews[i] = child;
}
}
可以看出,如果数据发生变化则把当前的ItemView放入ScrapViews中,否则把当前显示的ItemView放入ActiveViews中。那么咱们关键的getView方法到底是在哪调用呢,下面看RecycleBin中的方法:
/*
* 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);
} else
{
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
}
} else
{
child = mAdapter.getView(position, null, this);
}
return child;
}
可以看到,这个方法就是返回当前一个布局用户当前Item的显示,首先根据position去ScrapView中找,找到后调用我们的getView,此时getView里面的convertView!=null了,然后getView如果返回的View发生变化,缓存下来,否则convertView==null了。
大部分都是引用的别人的博文,在这里仅作整理和记录。感谢博主的分享。