Listview优化总结
2016-07-28 17:23
309 查看
Listview是andorid中最常用的控件之一,但要用好这个控件并不是那么容易。不注意优化的使用经常出现页面卡顿,OOM等问题的出现。在此本人将自己的拙见整理汇总,归纳listview的优化措施。
1.复用convertView
2.viewHolder保存控件
3.分页加载
4.UI卡顿优化
5.OOM
相信大家都做过手扶电梯,台阶是由一块块板组成的,运行的过程中,人只需要站在一块板上,就会自动把人送到上方,然后人离开,该块板会在电梯内部重新运行到下方,再次携带一个人上来。
类比到listview,“台阶板”可以看做是子条目item的view,而“人”就是子条目item要显示的数据,屏幕中可见的item就相当于在电梯上的“人”和“板”,当listview往下滑直到某一个item不可见时,就相当于电梯的某块“板”把“人”送到上方直到离开电梯。在不进行优化的情况下,我们在getView方法中每次都会给子条目new一个view出来,当该view不可见后销毁它,把这个过程放到手扶电梯中,就是不断地在下方造“板”,把“人”送到上方后,销毁这块“板”。看到这里相信你肯定在想,这得多浪费资源啊!所以电梯采用的是将“板”在电梯内部重新运行到下方,这样同一块“板”就可以不断地运送“人”。
google工程师在设计listview的时候已经考虑到这种情况,因此在getView的参数中有一个convertView,它是用来保存滑出屏幕范围的item的view,即运送完“人”转入电梯内部的“板”。我们要做的就是获取到这个convertView,重新赋予数据,作为新item来展示,即这块“板”运行到下方,再次运送一个“人”到上方的过程。
电梯将“板”运送到下方需要时间,因此通常电梯“板”的数量是你能看到的2倍。而在内存中一切都是非常快速的,当一个item的view不可见,被保存到convertView后,马上可以作为新的数据的载体,用作下一个item的view来使用,因此,listview仅需要一个屏幕可见item的个数 + 1(convertView)就可以显示所有的数据,无需创建更多的view再销毁,从而达到优化目的。
通过复用后,测试手机的屏幕可以看到13个item,无论我如何滑动,也只会创建14次view。
想象一下让你在一本书中找到某一句话,每次你都要从头翻到尾,直到找到那句话,这是非常耗时的。而如果你在第一次找到后,把对应的页数记录下来,下次再让你找的时候,直接翻到对应页面就可以快速地找到那句话。在listview中我们也可以采用同样的方法,将找到的控件记录在内存中,这样下次再找控件的时候直接从内存中取出即可。
通常的做法是定义一个静态内部类ViewHolder,里面仅有成员变量来保存控件:
是不是会有疑问:为什么要定义成static的那?为什么要叫viewHolder?
非静态内部类拥有外部类的强引用,因此为了避免对外部类(可能是 Activity)对象的引用造成内存泄露,那么最好将内部类声明为 static 的;
其实ViewHolder这个名字可以随意的取,但你写的代码不是只有你自己看,如果你定义为“abc”,那只有你知道这是干嘛的,而其他人不知道,而定义成ViewHolder可以达到见名知意。
分页加载
当listview需要显示的数据较多,数据加载的时间过长,避免用户过长时间的等待数据刷新,可以采用的分页加载的方式对listview进行优化。分页加载的思路就是将大量地数据分成多个小份,每次只加载其中的一份数据,加载新数据的时机即用户滑动到listview底部时。为提升用户体验,在加载数据时在listview底部显示一个progressBar来告诉用户正在加载新数据,并在完成加载后取消这个显示。下面是实现的步骤:
1.inflate一个listview的footer,用于显示progressBar
2.之前在网上看到的资料都会在setAdapter之前添加footer,setAdater之后移除
此处肯定有疑惑,为啥要如此操作?先来看下setAdapter源码中的一段:
mFooterViewInfos这个变量一看便知是footer的信息类,当有footer时,传入的adapter会进行包装,因此当时看到的资料才会有如此操作,目的是让adater进行包装。但实际上是多此一举的。继续看下addFooterView方法的源码:
可以看到内部有判断,如果已经setAdpter,即mAdapter != null条件满足,就会将该adapter进行包装。因此无需做上述操作,仅在需要的时候直接setFooterView就可以了。
3.设置滑动监听,用于判断用户滑动到底部:
当滑动到底部时添加footer显示progressBar,setSelection调整listview的显示位置,同时开启子线程加载数据。
4.加载数据:
5.更新UI
NO_MORE_DATA的情况是模拟数据加载完成,更换footer显示内容“无更多数据加载。。。”
2.UI视图避免过于复杂,过多的层级嵌套,重复的布局可以使用include标签代替,不常用的视图(例如点击才显示)可以使用viewStub标签。可以使用SDK自带的hierarchyviewer工具(androidSDK\tools目录下)查看视图层级。
1.加载图片时对分辨率进行缩放,在android手机中一个像素点会占用4byte的内存,分辨路过大的图片会占用更多的内存。
2.使用弱引用来保存bitmap,并及时调用recycle()方法释放内存。
3.使用已有框架,如ImageLoader、Glide 、Picasso、Fresco 等,这些框架都做了相应处理。
1.复用convertView
2.viewHolder保存控件
3.分页加载
4.UI卡顿优化
5.OOM
复用convertView
这一条和下面一条是最常见的优化,相信你也会在各种网络资料或者参考书中见到此类优化。如果还不是很理解为何要这样处理的话,我举一个例子来类比一下这个过程。相信大家都做过手扶电梯,台阶是由一块块板组成的,运行的过程中,人只需要站在一块板上,就会自动把人送到上方,然后人离开,该块板会在电梯内部重新运行到下方,再次携带一个人上来。
类比到listview,“台阶板”可以看做是子条目item的view,而“人”就是子条目item要显示的数据,屏幕中可见的item就相当于在电梯上的“人”和“板”,当listview往下滑直到某一个item不可见时,就相当于电梯的某块“板”把“人”送到上方直到离开电梯。在不进行优化的情况下,我们在getView方法中每次都会给子条目new一个view出来,当该view不可见后销毁它,把这个过程放到手扶电梯中,就是不断地在下方造“板”,把“人”送到上方后,销毁这块“板”。看到这里相信你肯定在想,这得多浪费资源啊!所以电梯采用的是将“板”在电梯内部重新运行到下方,这样同一块“板”就可以不断地运送“人”。
google工程师在设计listview的时候已经考虑到这种情况,因此在getView的参数中有一个convertView,它是用来保存滑出屏幕范围的item的view,即运送完“人”转入电梯内部的“板”。我们要做的就是获取到这个convertView,重新赋予数据,作为新item来展示,即这块“板”运行到下方,再次运送一个“人”到上方的过程。
电梯将“板”运送到下方需要时间,因此通常电梯“板”的数量是你能看到的2倍。而在内存中一切都是非常快速的,当一个item的view不可见,被保存到convertView后,马上可以作为新的数据的载体,用作下一个item的view来使用,因此,listview仅需要一个屏幕可见item的个数 + 1(convertView)就可以显示所有的数据,无需创建更多的view再销毁,从而达到优化目的。
@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = View.inflate(mContext, android.R.layout.simple_list_item_1, null); Log.i("test", "inflate a View"); } TextView textView = (TextView) convertView.findViewById(android.R.id.text1); textView.setText(data.get(position)); return convertView; }
通过复用后,测试手机的屏幕可以看到13个item,无论我如何滑动,也只会创建14次view。
viewHolder保存控件
通过上面的代码可以看到,view是不再过多的创建了,但是findViewById这个方法还是在每次getView的时候都会执行,该方法需要遍历整个布局文件来找到对应的ID,是很耗费资源的,有没有什么办法减少这个过程那?想象一下让你在一本书中找到某一句话,每次你都要从头翻到尾,直到找到那句话,这是非常耗时的。而如果你在第一次找到后,把对应的页数记录下来,下次再让你找的时候,直接翻到对应页面就可以快速地找到那句话。在listview中我们也可以采用同样的方法,将找到的控件记录在内存中,这样下次再找控件的时候直接从内存中取出即可。
通常的做法是定义一个静态内部类ViewHolder,里面仅有成员变量来保存控件:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = View.inflate(mContext, android.R.layout.simple_list_item_1, null); holder.textView = (TextView) convertView.findViewById(android.R.id.text1); convertView.setTag(holder); Log.i("test", "inflate a View"); }else { holder = (ViewHolder) convertView.getTag(); } holder.textView.setText(data.get(position)); return convertView; } } private static class ViewHolder{ TextView textView; }
是不是会有疑问:为什么要定义成static的那?为什么要叫viewHolder?
非静态内部类拥有外部类的强引用,因此为了避免对外部类(可能是 Activity)对象的引用造成内存泄露,那么最好将内部类声明为 static 的;
其实ViewHolder这个名字可以随意的取,但你写的代码不是只有你自己看,如果你定义为“abc”,那只有你知道这是干嘛的,而其他人不知道,而定义成ViewHolder可以达到见名知意。
分页加载
当listview需要显示的数据较多,数据加载的时间过长,避免用户过长时间的等待数据刷新,可以采用的分页加载的方式对listview进行优化。分页加载的思路就是将大量地数据分成多个小份,每次只加载其中的一份数据,加载新数据的时机即用户滑动到listview底部时。为提升用户体验,在加载数据时在listview底部显示一个progressBar来告诉用户正在加载新数据,并在完成加载后取消这个显示。下面是实现的步骤:
1.inflate一个listview的footer,用于显示progressBar
footer = View.inflate(this, R.layout.footer, null);
2.之前在网上看到的资料都会在setAdapter之前添加footer,setAdater之后移除
//listView.addFooterView(footer); adapter = new MyAdapter(this, data); listView.setAdapter(adapter); //listView.removeFooterView(footer);
此处肯定有疑惑,为啥要如此操作?先来看下setAdapter源码中的一段:
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; }
mFooterViewInfos这个变量一看便知是footer的信息类,当有footer时,传入的adapter会进行包装,因此当时看到的资料才会有如此操作,目的是让adater进行包装。但实际上是多此一举的。继续看下addFooterView方法的源码:
public void addFooterView(View v, Object data, boolean isSelectable) { final FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; info.isSelectable = isSelectable; mFooterViewInfos.add(info); mAreAllItemsSelectable &= isSelectable; // Wrap the adapter if it wasn't already wrapped. if (mAdapter != null) { if (!(mAdapter instanceof HeaderViewListAdapter)) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); } // In the case of re-adding a footer view, or adding one later on, // we need to notify the observer. if (mDataSetObserver != null) { mDataSetObserver.onChanged(); } } }
可以看到内部有判断,如果已经setAdpter,即mAdapter != null条件满足,就会将该adapter进行包装。因此无需做上述操作,仅在需要的时候直接setFooterView就可以了。
3.设置滑动监听,用于判断用户滑动到底部:
listView.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if(scrollState==SCROLL_STATE_IDLE){ int lastVisiblePosition = listView.getLastVisiblePosition(); if(lastVisiblePosition==adapter.getCount()-1){ //避免重复加载数据 if(isBottom){ return; } isBottom=true; listView.addFooterView(footer); listView.setSelection(lastVisiblePosition); new Thread(new MyTask()).start(); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} });
当滑动到底部时添加footer显示progressBar,setSelection调整listview的显示位置,同时开启子线程加载数据。
4.加载数据:
private static class MyTask implements Runnable{ @Override public void run() { List<String> newData = new ArrayList<String>(); //模拟数据 count+=20; //模拟无更多数据加载情况 if(count>150){ handler.sendEmptyMessage(NO_MORE_DATA); }else{ for (int i = count-20; i < count; i++) { newData.add("item" + i); } //模拟耗时操作 SystemClock.sleep(2000); //通知主线程更新UI handler.obtainMessage(RESULT_OK, newData).sendToTarget(); } } }
5.更新UI
private static Handler handler = new Handler(){ public void handleMessage(android.os.Message msg) { switch (msg.what) { case RESULT_OK: List<String> newData = (List<String>) msg.obj; data.addAll(newData); //移除footer listView.removeFooterView(footer); //更新数据显示 adapter.notifyDataSetChanged(); isBottom = false; break; case NO_MORE_DATA: View pb = footer.findViewById(R.id.footer_pb); pb.setVisibility(View.GONE); View tView = footer.findViewById(R.id.footer_tv); tView.setVisibility(View.VISIBLE); isBottom = false; break; default: break; } }
NO_MORE_DATA的情况是模拟数据加载完成,更换footer显示内容“无更多数据加载。。。”
UI卡顿优化
1.避免耗时操作在主线程完成,运行所需时间超过16毫秒的任务都会引起页面的卡顿,因此超过16毫秒的任务都应放在子线程完成。2.UI视图避免过于复杂,过多的层级嵌套,重复的布局可以使用include标签代替,不常用的视图(例如点击才显示)可以使用viewStub标签。可以使用SDK自带的hierarchyviewer工具(androidSDK\tools目录下)查看视图层级。
OOM
如果在listview中显示了大量图片,不进行处理的话,很容易出现OOM的情况。常见的处理方式有:1.加载图片时对分辨率进行缩放,在android手机中一个像素点会占用4byte的内存,分辨路过大的图片会占用更多的内存。
2.使用弱引用来保存bitmap,并及时调用recycle()方法释放内存。
3.使用已有框架,如ImageLoader、Glide 、Picasso、Fresco 等,这些框架都做了相应处理。
相关文章推荐
- 完美实现Android ListView中的TextView的跑马灯效果
- android上改变listView的选中颜色
- MySQL 优化
- more、less 和 most 的区别
- Google排名优化的几个影响因素
- DB2优化(简易版)
- Mysql limit 优化,百万至千万级快速分页 复合索引的引用并应用于轻量级框架
- C#中尾递归的使用、优化及编译器优化
- 对优化Ruby on Rails性能的一些办法的探究
- AJAX实现瀑布流触发分页与分页触发瀑布流的方法
- 优化Ruby脚本效率实例分享
- Delphi7中Listview的常用功能汇总
- Delphi控件ListView的属性及使用方法详解
- 十万条Access数据表分页的两个解决方法
- Asp编码优化技巧
- 如何监测和优化OLAP数据库
- sqlserver关于分页存储过程的优化【让数据库按我们的意思执行查询计划】
- mysql -参数thread_cache_size优化方法 小结
- 高效的mysql分页方法及原理
- MySQL数据库优化技术之配置技巧总结