我们设置或者优化ListView的性能很多时候都是在getView中完成的,反过来说就是很多性能问题都是由于没有正确使用getView造成的。
public View getView(int position, View convertView, ViewGroup parent)
那么问题就来了。
在一次显示ListView的界面时,getView会被执行几次?
在绘制ListView前往往要计算它的高度,所以一个ListView界面上可以看到6个ItemView,但是getView的执行次数却有可能是12次,多出的次数用来计算高度(这个可以通过设置ListView的height为0来避免)。所以要避免在getView中进行逻辑运算,两次计算同一逻辑完全是浪费。
每次getView执行时间有多久?
1秒之内屏幕大概可以完成30帧的绘制,人才能看到它比较流畅,每帧可使用的时间:1000ms/30 = 33.33 ms,每个ListView一般要显示6个ListItem,加上1个重用convertView:33.33ms/7 = 4.76ms,即是说,每个getView要在4.76ms内完成工作才会较流畅,但是事实上,每个getView间的调用也会有一定的间隔(有可能是由于handler在处理别的消息),UI的handler处理不好的话,这个间隔也可难会很大(0ms-200ms)。结论就是,留给getView使用的时间应该在4ms之内,如果不能控制在这之内的话,ListView的滑动就会有卡顿的现象。
(转载:Android面试一天一题(11 Day) —— goeasyway)
有哪些常见的优化方案?
1. 使用ConvertView
也是最普通的优化,就在MyAdapter类中的getView方法中,我们注意到,上面的写法每次需要一个View对象时,都是去重新inflate一个View出来返回去,没有实现View对象的复用,而实际上对于ListView而言,只需要保留能够显示的最大个数的view即可,其他新的view可以通过复用的方式使用消失的条目的view,而getView方法里也提供了一个参数:convertView,这个就代表着可以复用的view对象,当然这个对象也可能为空,当它为空的时候,表示该条目view第一次创建,所以我们需要inflate一个view出来,所以在这里,我们使用下面这种方式来重写getView方法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
// 判断convertView的状态,来达到复用效果
if (null == convertView) {
//如果convertView为空,则表示第一次显示该条目,需要创建一个view
view = View.inflate(MainActivity.this, R.layout.listview_item,null);
} else {
//否则表示可以复用convertView
view = convertView;
}
// listview_item里只有一个textview
TextView tv_item = (TextView) view.findViewById(R.id.tv_item);
tv_item.setText(list.get(position));
return view;
}
2. 使用View Holder模式
经过上面的优化之后,我们不需要每一个view都重新生成了。下面我们来解决下一个每一次都需要做的工作,那就是view中组件的查找:
TextView tv_item = (TextView) view.findViewById(R.id.tv_item);
实际上,findViewById是到xml文件中去查找对应的id,可以想象如果组件多的话也是挺费事的,如果我们可以让view内的组件也随着view的复用而复用,就能对ListView进行很好的优化
private static class ViewHolder {
private TextView tvHolder;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder holder;
// 判断convertView的状态,来达到复用效果
if (null == convertView) {
// 如果convertView为空,则表示第一次显示该条目,需要创建一个view
view = View.inflate(MainActivity.this, R.layout.listview_item, null);
//新建一个viewholder对象
holder = new ViewHolder();
//将findviewbyID的结果赋值给holder对应的成员变量
holder.tvHolder = (TextView) view.findViewById(R.id.tv_item);
// 将holder与view进行绑定
view.setTag(holder);
} else {
// 否则表示可以复用convertView
view = convertView;
holder = (ViewHolder) view.getTag();
}
// 直接操作holder中的成员变量即可,不需要每次都findViewById
holder.tvHolder.setText(list.get(position));
return view;
}
ps:
这里的ViewHolder类需要不需要定义成static,根据实际情况而定,如果item不是很多的话,可以使用,这样在初始化的时候,只加载一次,可以稍微得到一些优化。
不过,如果item过多的话,建议不要使用。因为static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了。
3. 分批加载与分页加载相结合
我们需要进行分批加载,比如说1000条新闻的List集合,我们一次加载20条,等到用户翻页到底部的时候,我们再添加下面的20条到List中,再使用Adapter刷新ListView,这样用户一次只需要等待20条数据的传输时间,不需要一次等待好几分钟把数据都加载完再在ListView上显示。其次这样也可以缓解很多条新闻一次加载进行产生OOM应用崩溃的情况。
实际上,分批加载也不能完全解决问题,因为虽然我们在分批中一次只增加20条数据到List集合中,然后再刷新到ListView中去,假如有10万条数据,如果我们顺利读到最后这个List集合中还是会累积海量条数的数据,还是可能会造成OOM的情况,这时候我们就需要用到分页,比如说我们将这10万条数据分为1000页,每一页100条数据,每一页加载时都覆盖掉上一页中List集合中的内容,然后每一页内再使用分批加载,这样用户的体验就会相对好一些。
除此之前还有一些优化建议:
- 使用异步线程加载图片(一般都是直接使用图片库加载,如Glide, Picasso);
- 在adapter的getView方法中尽可能的减少逻辑判断,特别是耗时的判断;
- 避免GC(可以从LOGCAT查看有无GC的LOG);
- 在快速滑动时不要加载图片;
- 将ListView的scrollingCache和animateCache这两个属性设置为false(默认是true);
- 尽可能减少List Item的Layout层次(如可以使用RelativeLayout替换LinearLayout,或使用自定的View代替组合嵌套使用的Layout);