Android ListView优化篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/IT_ZJYANG/article/details/51596292
在我的上一篇博客《Android ListView基础篇》中陈列了ListView和adapter的多种结合方式的基本使用,在本篇文章中将具体讲述如何通过多种方式处理好ListView的优化问题。


在上篇文章的例子中,我们使用了一张图片和一个文本作为每一行的数据,发现效果已经完全达到了,而且没出现什么问题。但如果我们将Item的数量调大,比如调到1000、10000、100000条数据,这个时候当你打开ListView的时候,肯定会不禁感慨“什么鬼,卡机了?!”等了好几秒钟,ListView才显示出来,用户体验非常不好,特别是如果是要上市的项目,后果很严重!所以针对ListView的优化至关重要。


ListView内存调用机制的原理

ListView消耗内存的主要地方就在于每一个ListItem的绘制,之前说过了,ListView的每一项的绘制的地方就在于Adapter的getView()方法中,getView()方法的返回值是一个View,这个View就是每一行的视图,然后ListView再将其展示出来,那如果我们像之前那种写法,不做任何修改,结果会是怎样?

我们通过上一篇的例子做个测试,将行数调到100,在getView中打印一句Log看看:



Log打印结果:



可以看到,初始化ListView时getView运行了9次,而界面上刚好也仅显示到第9条数据,也就是只有屏幕范围内显示的才会调用getView(),另外,可以看到它们的convertView都会null,然后我们再将界面稍微往下拖动,如图:



再看Logcat:



注意到,第九项数据从底部开始进入界面,它的getView也调用了一遍,convertView依然为null,这是因为顶部的第一项数据还未完全脱离屏幕范围外,也就是第一项的视图还未进入Android的Recycler中,还不能被重用,我们再继续往下滑:



Logcat:



发现第10项的convertView不为空了!这是因为顶部的第一项数据已经完全离开了屏幕,所以Android会将它的convertView“推”进RecycleView中,然后第10行出现的时候,getView方法的convertView参数正是第一项存放在Recycler中的视图。如下图:




ConvertView的重用

了解了ListView的getView原理,我们就可以开始对它进行优化,上面提到了已经离开屏幕的convertView会被压入Recycler中,那我们可以在每次getView的一开始先判断convertView是否为空,不是为空的话就直接用那个已经存在的convertView来直接进行操作,代码如下:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	// TODO Auto-generated method stub
	Log.d("getView--->", convertView+"--position:"+position);
				
	if(convertView==null){
		convertView = inflater.inflate(R.layout.list_item, null);
	}

	TextView text = (TextView)convertView.findViewById(R.id.list_item_text);
	ImageView image = (ImageView)convertView.findViewById(R.id.list_item_image);
	text.setText(data.get(position).get("text").toString());
	image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
		
	return convertView;
}



运行滑动到如下图:



打印结果:



注意我圈起来的两个地方,两个地址一模一样!所以我们成功重用了Recycle中缓存的视图,这样可以有效优化ListView的内存消耗(试想一下,100000个视图我来来回回只用那10个convertView,能不减少内存开销吗?)



ViewHolder的使用

以上只是利用convertView的重用来做到优化效果,但是注意到还是有存在问题,每个视图里面有一个text和一个image,每次都要通过findViewByID来找到它们,这也是一件庞大的工程...那既然我们可以重用convertView,那可不可以将这两个子控件也缓存起来呢?
我们可以通过自定义一个ViewHolder来,来进行子控件视图的缓存,以达到更佳的优化效果:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	// TODO Auto-generated method stub
	Log.d("getView--->", convertView+"--position:"+position);
				
	ViewHolder holder = null;
	if(convertView==null){
		convertView = inflater.inflate(R.layout.list_item, null);
		holder = new ViewHolder();
		holder.text = (TextView)convertView.findViewById(R.id.list_item_text);
		holder.image = (ImageView)convertView.findViewById(R.id.list_item_image);
		convertView.setTag(holder);
	}
	else{
		holder = (ViewHolder)convertView.getTag();
	}
		
	holder.text.setText(data.get(position).get("text").toString());
	holder.image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
		
	return convertView;
}
	
public static class ViewHolder{
	public TextView text;
	public ImageView image;
}


代码分析:首先自定义了一个ViewHolder类,这里设置为static这样ViewHolder无论new多少次都是指向同一个内存空间,在ViewHolder类中添加了两个成员变量,分别对应我们的子控件。每次getView的时候,同样先判断ViewHolder对象是否为空,如果为空,就实例化一个ViewHolder对象,并将convertView通过findViewById找到的子控件赋给holder,再将holder通过setTag()方法设置在convertView上,之后重用的时候可以通过convertView的getTag()来获得。其实ViewHolder相当于我们子控件的一个封装类而已,通过这样实现不用每次都去findViewById查找子控件,每次做的事情只是重用之前的视图和控件设置一下数据,达到优化的目的。



ListView多种子布局的重用方式

上面的操作虽然已经对ListView进行了一些优化,但是依然存在问题,如果所有的ListItem的布局并不是都一样(例如类似微信朋友圈,一些是图片,一些是文字,一些是小视频等等),就不能全部都用一样的ViewHolder或者convertView来处理了,因为重用的布局不一定适合新出现的ListItem,ListView中提供了另外两个方法: 
getItemViewType(int position)  【根据下标返回当前视图的类型】
getViewTypeCount()                    【返回类型的种类数】


代码如下:

public class ListViewAdapter extends SimpleAdapter{
	
	private Context context;
	
	private List<Map<String,Object>> data;
	
	private LayoutInflater inflater;
	
	//注意,这里定义的这些整型数要小于getViewTypeCount()所返回的那个数字,否则会报错越界
	private final int TYPE_1 = 0;
	private final int TYPE_2 = 1;
	
	public ListViewAdapter(Context context,
			List<Map<String, Object>> data, int resource, String[] from,
			int[] to) {
		super(context, data, resource, from, to);
		// TODO Auto-generated constructor stub
		this.context = context;
		this.data = data;
		inflater = LayoutInflater.from(context);
	}
	//返回数据的大小,即listview的行数
	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return data.size();
	}
	//根据下标获得某一行的数据
	@Override
	public Object getItem(int position) {
		// TODO Auto-generated method stub
		return data.get(position);
	}
	//获得指定的Item的下标
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}
	
	@Override
	public int getItemViewType(int position) {
		// TODO Auto-generated method stub
		//如果当前行是偶数行,返回类型1
		if(position%2==0){
			return TYPE_1;
		}
		//如果当前行是奇数行,返回类型2
		else{
			return TYPE_2;
		}
	}
	
	@Override
	public int getViewTypeCount() {
		// TODO Auto-generated method stub
		return 2;
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		Log.d("getView--->", convertView+"--position:"+position);
				
		ViewHolder1 holder1 = null;
		ViewHolder2 holder2 = null;
		int type = getItemViewType(position);
		if(convertView==null){
			switch (type) {
			case TYPE_1:
				convertView = inflater.inflate(R.layout.list_item, null);
				holder1 = new ViewHolder1();
				holder1.text = (TextView)convertView.findViewById(R.id.list_item_text);
				holder1.image = (ImageView)convertView.findViewById(R.id.list_item_image);
				convertView.setTag(holder1);
				break;

			case TYPE_2:
				convertView = inflater.inflate(R.layout.list_item2, null);
				holder2 = new ViewHolder2();
				holder2.text = (TextView)convertView.findViewById(R.id.list_item_text2);
				holder2.detail = (TextView)convertView.findViewById(R.id.list_item_detail2);
				convertView.setTag(holder2);
				break;
			}
			
		}
		else{
			switch (type) {
			case TYPE_1:
				holder1 = (ViewHolder1)convertView.getTag();
				break;

			case TYPE_2:
				holder2 = (ViewHolder2)convertView.getTag();
				break;
			}
			
		}
		
		switch (type) {
			case TYPE_1:
				holder1.text.setText(data.get(position).get("text").toString());
				holder1.image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
				break;
	
			case TYPE_2:
				holder2.text.setText(data.get(position).get("text").toString());
				holder2.detail.setText(data.get(position).get("text").toString());
				break;
		}
		
		
		
		return convertView;
	}
	
	public static class ViewHolder1{
		public TextView text;
		public ImageView image;
	}
	
	public static class ViewHolder2{
		public TextView text;
		public TextView detail;
	}

}


代码分析:创建另外一个ViewHolder,用于加载和重用另外一种布局,其实就是在原来的基础上,为每个操作都套上一层switch判断,然后根据type的类型来分别设置两种布局。



ListView异步加载乱序问题

出现乱序的原因

上面的操作都是属于同步加载每一行,所以不会出现什么问题。但如果当我们是网络异步加载每一行的图片时,就会出现数据紊乱,前文已经说了,Android为ListView进行的Reycler的处理,减少内存开销,那么,每当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。但是还没完,新进入屏幕的元素它也会发起一条网络请求来获取当前位置的图片,等到图片下载完的时候会设置到同样的ImageView上面,因此就会出现先显示一张图片,然后又变成了另外一张图片的情况。


如何解决乱序问题?

由于篇幅问题这里附上郭神的一篇博文http://blog.csdn.net/guolin_blog/article/details/45586553很详细地讲解了三种方式来解决异步加载乱序。


总之,以上讲述了ListView的多种优化方式,但是并不是万能,也仅仅只是起到了一部分效果,真实开发中还要视情况而定,比如如果是多图片,首先需要将图片压缩,并且不要再getView中做过多的耗时操作!希望本文对大家理解ListView的优化有所帮助。

猜你喜欢

转载自blog.csdn.net/IT_ZJYANG/article/details/51596292