Android ListView优化篇

在我的上一篇博客《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的优化有所帮助。

时间: 2024-11-05 11:30:21

Android ListView优化篇的相关文章

【朝花夕拾】Android性能优化篇之(一)序言及JVM篇

序言        笔者从事Anroid开发有些年头了,深知掌握Anroid性能优化方面的知识的必要性,这是一个程序员必须修炼的内功.在面试中,它是面试官的挚爱,在工作中,它是代码质量的拦路虎,其重要性可见一斑.在团队中,性能优化的工作又往往由经验丰富的老师傅来完成,可见要做好性能优化,绝不是一件容易的事情. 性能优化方面涉及的知识点比较广,有理论基础知识,也有实际操作技能,笔者将通过一系列的文章来进行整理,将主要包括Java虚拟机.内存分配.垃圾回收,android虚拟机.进程管理.内存优化.

【朝花夕拾】Android性能优化篇之(四)Apk打包

前言 APK,即Android Package,是将android程序和资源整合在一起,形成的一个.apk文件.相信所有的Android程序员是在IDE的帮助下,完成打包轻而易举,但对打包流程真正清楚的可能并不多.本章的内容比较简单,也是非常基础的内容,但是对理解android应用的结构却有很大的帮助.笔者写这篇文章的目的,一方面是为了弥补这方面的盲点,回顾和梳理apk打包方面的理论知识点:第二方面,是为了给后续写Android虚拟机知识做铺垫,进而去研究android的性能优化,这也是把这篇文

【朝花夕拾】Android性能优化篇之(五)Android虚拟机简介

前言 Android虚拟机的使用,使得android应用和Linux内核分离,这样做使得android系统更稳定可靠,比如程序中即使包含恶意代码,也不会直接影响系统文件:也提高了跨平台兼容性.在Android4.4以前的系统中,Android系统均采用Dalvik作为运行andorid程序的虚拟机,在android发展中具有举足轻重的地位,而Android 5.0及以后的系统使用ART虚拟机取代Dalvik,在性能上做了很大的优化.本文将对这两款虚拟机做一些介绍,主要内容如下: 一.什么是Dal

Android调试优化篇

为了开发出商业级的应用程序,大规模的測试是不可避免的,同一时候为了提高应用程序的执行速度,须要进行必要的优化.在Android中.提供了丰富的调试与优化工具供开发者应用,主要包含模拟器和目标端等两种场景下使用的工具. 1.Android调试 软件调试是一个伴随软件开发的必定过程.好的调试环境和工具能够提高开发的效率.在Android中,除了提供GDB调试外.还提供了DNSS.Logcat.Dmtracedump.DevTools.Procrank.Dumpsys等开发工具供开发人员使用,当中DM

Android性能优化篇

很多App都会遇到以下几个常见的性能问题: 启动速度慢:界面跳转慢:事件响应慢:滑动和动画卡顿. 一.启动速度优化. 优化初始化任务: 1. 把一些初始化任务懒加载初始化 2. 把初始化任务并行化(异步化) 3. 使初始化任务可以插拔(一个任务出问题不会影响到其他的任务) 其他: 1. 控制线程数量,注意线程的使用,用自己的线程池替换三方或者二方SDK的线程池,线程太多占用cpu资源, 2. 使用缓存来减少I/O操作(读数据库,读文件,SharedPreference)减少网络请求(判断无网络直

Android内存优化篇

在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能,而且每台设备配置不一,分配内存大小也不一样 首先看Android中的ActivityManager,这个类可以得到"设备配置的属性","进程信息","任务信息","服务","正在运行的程序" ActivityManager的

Android ListView 优化最佳实践

原文地址链接:http://stackvoid.com/list-view-optimization-best-practice-android/ 我有篇博客教大家如何利用 convertView 以及 viewHolder(static) 改善 ListView 卡顿情况:但是在 ListView 加载大量复杂布局和图片的时候,即使使用了 convertView 和 viewHolder,ListView还是卡顿,本文主要讨论了如何在加载复杂 list_item 同时保证 ListView 流

Android性能优化篇 [ 谷歌官方 ]

https://www.kancloud.cn/kancloud/android-performance#/catalog https://www.kancloud.cn/kancloud/android-performance/53237

Android ListView优化 如何省略ViewHolder方法

代码: public class ViewHolder { /** * @param view converView * @param id 控件的id * @return 返回<T extends View> */ public static <T extends View> T get(View view, int id) { SparseArray<View> viewHolder = (SparseArray<View>) view.getTag()