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