正文  软件开发 > 编程综合 >

Android 适配器教程(五)

在之前四讲中,我们已经由浅入深的认识了适配器,从最简单的ListView写起,最后完成了自定义适配器的简单例子,这一次咱们就上次留下来的问题 再进行更加深入的学习,主要是getView方法的原...

在之前四讲中,我们已经由浅入深的认识了适配器,从最简单的ListView写起,最后完成了自定义适配器的简单例子,这一次咱们就上次留下来的问题 再进行更加深入的学习,主要是getView方法的原理,还有Holder的使用,还有关于ListView性能方面的优化问题。自定义适配器这一部分需要好好的进行理解,只有真正理解的比较透彻,写起来才会比较顺手。这一次就没有例子了,我会配上几张图片,希望能让大家看得明白。

先从ListView的原理来进行讲解吧!


大家先看一张图:\


这就是ListView的工作原理:

ListView 针对List中每个item,要求 adapter “给我一个视图” (getView)。

一个新的视图被返回并显示

如果我们有上亿个项目要显示怎么办?为每个项目创建一个新视图?这不可能!内存怎么办?

实际上Android为你缓存了视图。

Android中有个叫做Recycler的构件,


如果你有10亿个项目(item),其中只有可见的项目存在内存中,其他的在Recycler中。


ListView先请求一个type1视图(getView)然后请求其他可见的项目。convertView在getView中是空(null)的。


当item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1。你只需设定新的数据然后返回convertView,不必重新创建一个视图。这样直接使用convertView从而减少了很不不必要view的创建。


更快的方式是定义一个ViewHolder,将convertView的tag设置为ViewHolder,不为空是重新使用 现在我们来深入了解一下ViewHolder吧!
要注意:很多人都认为功劳都是ViewHolder的,其实不然,Tag也是功臣 ViewHolder只是将需要缓存的那些view封装好,convertView的setTag才是将这些缓存起来供下次调用 当你的listview里布局多样化的时候 viewholder的作用就有比较明显的体现了。 假如你2种模式的布局 当发生回收的时候 你会用setTag分别记录是哪两种 这两种模式会被封装到viewholder中进行保存方便你下次使用。ViewHolder就是个静态类 与缓存无关

复习一下源码先: 注释已经写的很明白了,大家要仔细看哦!

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定义一个ViewHolder对象
		if (convertView == null) {			//当初始化的时候 convertView是空的
			
			holder=new ViewHolder();		//创建一个ViewHolder
			
			//通过LayoutInflater用来找res/layout/下的xml布局文件,并且实例化,这样convertView的界面就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法来获得其中的界面元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//将holder对象作为标签添加到View上
			convertView.setTag(holder);
			
		}else {
			//不为空的时候直接重新使用convertView从而减少了很多不必要的View的创建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然后加载数据
		holder.img.setBackgroundResource((Integer)data.get(position).get("imgview2"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			
		
		
		return convertView;
	}

让我们再回忆一下getView的过程:

ListView有很多Item,getView就是根据Item的序号(position)来获取这个Item的控件。

convertView =mInflater.inflate(R.layout.itemrow, null);

这句话是将layout下面的 itemrow 作为ListView每个Item的样式。下面就是对Item的控件赋值。

上面的都很好理解,接下来是重点:


说白了也就是ListView的缓冲机制,不仅是ListView,GridView也可以这样。

对这个ViewHolder举个比较极端例子说明它的作用的话就是:

假设整个ListView有1000行,你往下翻页,翻到最后,如果不用ViewHolder并且代码完全没有考虑到效率问题的话他会到布局文件中拿1000次View,所以,ViewHolder就是为了解决这个。打个比方,比如ListView每页空间只能显示5个,那么你往下翻页,翻到第6个条目,那么第一个条目已经不在显示范围内了,这样调用getView的第二个参数convertView 不再是null了,那么ViewHolder作用就是将这个一开始的第一条反复利用,而不是再inflate一个控件出来,


if (convertView == null)这句很关键 你翻动的时候下面的item如果是刚出现的时候下面的是没有的 if下面的语句就是创建如果你已经翻动过则不会创建了这个类有节省内存的作用。

这样大家就发现一些ViewHolder的优势了,实际上构建这个ViewHolder是为了把查找的view缓存起来方便多次重用不用重新构建VIEW,利用系统中缓存的VIEW,可以提高效率。当然上面实际上我把ViewHolder有些过于夸大了,只有在代码完全没有考虑到效率的时候才会出现这样的对比。

别忽视了Tag,他其实就相当于一个持有者的类,他里面一般没有方法,只有属性,作用就是一个临时的储存器,把你getView方法中每次返回的View存起来,可以下次再用。这样做的好处就是不必每次都到布局文件中去拿到你的View,提高了效率。可以理解成ViewHolder,它的作用就在于减少不必要的调用findViewById,不必要每次都重新加载控件布局。所以ViewHolder和settag/gettag的妙用是成就了效率提升的关键。不管是调用findViewById还是直接new 一个View组件出来,他们的目的都是为了在内存中创建一个View对象。当有n项数据需要显示时,显然必然需要调用findViewById n次或new n次View对象。如果使用ViewHolder以及settag,那么就可以做到一次创建n次复用。

下面是摘自google 2010 I/O大会三个例子代码



在android开发中Listview是一个很重要的组件,用户可以自由的定义listview每一列的布局,但当listview有大量的数据需要加载的时候,会占据大量内存,影响性能,这时候就需要按需填充并重新使用view来减少对象的创建。


ListView加载数据都是在public View getView(int position, View convertView, ViewGroup parent) {}方法中进行的(要自定义listview都需要重写listadapter:如BaseAdapter,SimpleAdapter,CursorAdapter的等的getvView方法),优化listview的加载速度就要让convertView匹配列表类型,并最大程度上的重新使用convertView。


getview的加载方法一般有以下三种种方式:


最慢的加载方式是每一次都重新定义一个View载入布局,再加载数据


public View getView(int position, View convertView, ViewGroup parent) {
 View item = mInflater.inflate(R.layout.list_item_icon_text, null);
 ((TextView) item.findViewById(R.id.text)).setText(DATA[position]);
 ((ImageView) item.findViewById(R.id.icon)).setImageBitmap(
 (position & 1) == 1 ? mIcon1 : mIcon2);
 return item;
}


正确的加载方式是当convertView不为空的时候直接重新使用convertView从而减少了很多不必要的View的创建,然后加载数据


public View getView(int position, View convertView, ViewGroup parent) {
 if (convertView == null) {
 convertView = mInflater.inflate(R.layout.item, parent, false);
 }
 ((TextView) convertView.findViewById(R.id.text)).setText(DATA[position]);
 ((ImageView) convertView.findViewById(R.id.icon)).setImageBitmap(
 (position & 1) == 1 ? mIcon1 : mIcon2);
 return convertView;
 }


最快的方式是定义一个ViewHolder,将convetView的tag设置为ViewHolder,不为空时重新使用即可


static class ViewHolder {
TextView text;
ImageView icon;
}
 
public View getView(int position, View convertView, ViewGroup parent) {
 ViewHolder holder;
 
 if (convertView == null) {
 convertView = mInflater.inflate(R.layout.list_item_icon_text,
 parent, false);
 holder = new ViewHolder();
 holder.text = (TextView) convertView.findViewById(R.id.text);
 holder.icon = (ImageView) convertView.findViewById(R.id.icon);
 convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.text.setText(DATA[position]);
holder.icon.setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return convertView;
}


三种方式加载效率对比如下图所示:

\



当处理一些耗时的资源加载的时候需要做到以下几点,以使你的加载更快更平滑:

1. 适配器在界面主线程中进行修改

2. 可以在任何地方获取数据但应该在另外一个地方请求数据

3. 在主界面的线程中提交适配器的变化并调用notifyDataSetChanged()方法


好了,到此为止大家应该明白看似神秘的ViewHolder和set/get tag 的优点了吧,有优点才有使用的动力,同时也有去深入理解的动力,希望大家结合上一篇的内容,能够学到一些东西。


关于这个问题的理解,我看网上众说纷纭,以上的都是我个人观点。


下一讲我会用一个比较复杂的自定义适配器结束我们的学习,同时也算是深入学习的大练兵,最近略微有点忙,最后一篇有可能会过一段时间才能写完,请大家不要错过哦!


源码我会在最后一篇的最后附上链接,我还没有写完,之前都是边写博客边码代码的,我觉得这样思路会更加清晰。

我也是学生,水平十分有限,还请大家多多指教~