列表的显示需要三个元素:
-
ListVeiw: 用来展示列表的View。
-
适配器 : 用来把数据映射到ListView上
-
数据: 具体的将被映射的字符串,图片,或者基本组件。
ListView的工作原理如下:
下面简单说下上图的原理:
- 如果你有几千几万甚至更多的选项(item)时,其中只有可见的项目存在内存(内存内存哦,说的优化就是说在内存中的优化!!!)中,其他的在Recycler中
- ListView先请求一个type1视图(getView)然后请求其他可见的项目。convertView在getView中是空(null)的
- 当item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1。你只需设定新的数据然后返回convertView,不必重新创建一个视图
- public class MultipleItemsList extends ListActivity {
- private MyCustomAdapter mAdapter;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mAdapter = new MyCustomAdapter();
- for (int i = 0; i < 50; i++) {
- mAdapter.addItem("item " + i);
- }
- setListAdapter(mAdapter);
- }
- private class MyCustomAdapter extends BaseAdapter {
- private ArrayList mData = new ArrayList();
- private LayoutInflater mInflater;
- public MyCustomAdapter() {
- mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- }
- public void addItem(final String item) {
- mData.add(item);
- notifyDataSetChanged();
- }
- @Override
- public int getCount() {
- return mData.size();
- }
- @Override
- public String getItem(int position) {
- return mData.get(position);
- }
- @Override
- public long getItemId(int position) {
- return position;
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- System.out.println("getView " + position + " " + convertView);
- ViewHolder holder = null;
- if (convertView == null) {
- convertView = mInflater.inflate(R.layout.item1, null);
- holder = new ViewHolder();
- holder.textView = (TextView)convertView.findViewById(R.id.text);
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder)convertView.getTag();
- }
- holder.textView.setText(mData.get(position));
- return convertView;
- }
- }
- public static class ViewHolder {
- public TextView textView;
- }
- }
- if(!bmp.isRecycle() ){
- bmp.recycle() //回收图片所占的内存
- system.gc() //提醒系统及时回收
- }
下面来列举下真正意义上的优化吧:
- ViewHolder Tag 必不可少,这个不多说!
- 如果自定义Item中有涉及到图片等等的,一定要狠狠的处理图片,图片占的内存是ListView项中最恶心的,处理图片的方法大致有以下几种:
2.1:不要直接拿个路径就去循环decodeFile();这是找死….用Option保存图片大小、不要加载图片到内存去;
2.2: 拿到的图片一定要经过边界压缩
2.3:在ListView中取图片时也不要直接拿个路径去取图片,而是以WeakReference(使用WeakReference代替强引用。比如可以使 用WeakReference<Context> mContextRef)、SoftReference、WeakHashMap等的来存储图片信息,是图片信息不是图片哦!
2.4:在getView中做图片转换时,产生的中间变量一定及时释放,用以下形式: - 尽量避免在BaseAdapter中使用static 来定义全局静态变量,我以为这个没影响 ,这个影响很大,static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了..
- 如果为了满足需求下必须使用Context的话:Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题
- 尽量避免在ListView适配器中使用线程,因为线程产生内存泄露的主要原因在于线程生命周期的不可控制
- 记下小马自己的错误:
之前使用的自定义ListView中适配数据时使用AsyncTask自行开启线程的,这个比用Thread更危险,因为Thread只有在run函数不 结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了线程执行池(ThreadPoolExcutor,要想了解这个类的话大家加下我们的Android开发群五号,因为其它群的存储空间快满了,所以只上传到五群里了,看下小马上传的Gallery源码,你会对线程执行池、软、弱、强引用有个更深入的认识),这个类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。这个问题的解决办法小马当时网上查到了记在txt里了,如下:
6.1:将线程的内部类,改为静态内部类。
6.2:在线程内部采用弱引用保存Context引用
示例代码如下:
- public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends
- AsyncTask<Params, Progress, Result> {
- protected WeakReference<WeakTarget> mTarget;
- public WeakAsyncTask(WeakTarget target) {
- mTarget = new WeakReference<WeakTarget>(target);
- }
- /** {@inheritDoc} */
- @Override
- protected final void onPreExecute() {
- final WeakTarget target = mTarget.get();
- if (target != null) {
- this.onPreExecute(target);
- }
- }
- /** {@inheritDoc} */
- @Override
- protected final Result doInBackground=\'#\'" /span>
- final WeakTarget target = mTarget.get();
- if (target != null) {
- return this.doInBackground=\'#\'" /span>
- } else {
- return null;
- }
- }
- /** {@inheritDoc} */
- @Override
- protected final void onPostExecute(Result result) {
- final WeakTarget target = mTarget.get();
- if (target != null) {
- this.onPostExecute(target, result);
- }
- }
- protected void onPreExecute(WeakTarget target) {
- // No default action
- }
- protected abstract Result doInBackground(WeakTarget target, Params... params);
- protected void onPostExecute(WeakTarget target, Result result) {
- // No default action
- }
- }
不知不觉,都已经正式工作三个月了,这几天将工作中学习的知识进行一个小小的总结,ListView是android中一个非常常用的空间,下面就对ListView控件的性能优化做一点儿分享,由于内容相对有点儿多,我会分为几篇文章进行分享,希望对同学们有帮助,个人愚见,请保持怀疑的眼光阅读。
下集预告: 这一小节从表面上讲了性能提高的方法,下一小节将从listview的源码中探寻convertView是如何被回收利用的
问题
- 当列表中有成千上万条记录的时候,如果每一条记录都去new 一个view的话,就会发生oom
解决问题: 通过view的复用来解决这一问题,用户之需要关心在屏幕之内看得见的部分就可以了,所以不在屏幕中的部分可以回收利用。
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- Log.d(TAG, "position=" + position + ",convertView=" + convertView);
- ViewHolder viewHolder = null;// 保存控件的引用,不用每次都调用findviewById(...)
- if (convertView == null) {// 只有当convertView为空的时候,才去创建
- convertView = LayoutInflater.from(mContext).inflate(
- R.layout.list_item, null);// 这个过程相当耗时间
- viewHolder = new ViewHolder();
- viewHolder.mTextView = (TextView) convertView
- .findViewById(R.id.tv_tips);
- viewHolder.mImageView = (ImageView) convertView
- .findViewById(R.id.iv_image);
- convertView.setTag(viewHolder);
- } else {// 利用回收回来的view
- viewHolder = (ViewHolder) convertView.getTag();
- }
- viewHolder.mTextView.setText("-----" + position);
- return convertView;
- }
- class ViewHolder {
- TextView mTextView;
- ImageView mImageView;
- }
综上:
1、因为不用每次调用getView的时候都inflate一个view出来,所以从速度上更快,并且内存也得到了优化。
2、由于通过ViewHolder保存了对控件的引用,不要每次都去调用findviewbyId(),也更加的方便快捷
日志分析
1、首先运行程序,观察日志输出
![](http://dl.iteye.com/upload/attachment/0074/6104/89bdf3f6-e3c7-3957-8d8e-62835a9da495.png)
2、拖动listview,直到第一项完全消失在屏幕可见区域,神奇的事情发生了
![点击查看原始大小图片](http://dl.iteye.com/upload/attachment/0074/6106/06188d5f-5053-3abc-b42b-006811ad7bb8.png)
3、继续拖动listView我想你应该明白了。。。
上一节从应用层面给出了ListView性能优化的方法,这一节将从ListView源码中探寻真谛。
下集预告: 这一小节讲垃圾回收站,下一个小节讲这个回收站具体是怎么用的,以及adapter中的getView方法是如何拿到回收站的内容的。
下面是ListView的继承关系
![](http://dl.iteye.com/upload/attachment/0074/6119/fe7179bd-c0b7-3a32-98a1-0aa49c8826e1.jpg)
ListView继承自AbsListView,而在AbsListView中的RecycleBin就是上一节中View复用的关键。下面就对RecycleBin进行分析
RecycleBin
1、RecycleBin中的数据结构 ,这些数据结构就是在垃圾回收站堆放的一个个垃圾桶。
- private RecyclerListener mRecyclerListener;
- // 第一个存放在mActiveViews中的view的位置.
- private int mFirstActivePosition;
- // 用来存放屏幕可见区域的view
- private View[] mActiveViews = new View[0];
- /**
- * 这个ArrayList就是adapter中getView方法中的参数convertView的来源
- * 注意:这里是一个数组,因为如果adapter中数据有多种类型
- * 那么就会有多个垃圾桶(对比垃圾分类)来放每一种垃圾
- */
- private ArrayList<View>[] mScrapViews;
- /*
- * view的类型数,列表中可能有多种数据类型(根据adapter提供的数据)
- *比如用户真正关心的数据,或者是分割符
- */
- private int mViewTypeCount;
- /*
- * 其实就是指向mScrapViews[0]的一个引用,也就是第一种view对应的垃圾桶
- */
- private ArrayList<View> mCurrentScrap;
2、RecycleBin中的重要主要方法分析
- //从回收站中获取view
- View getScrapView(int position) {
- ArrayList<View> scrapViews;
- if (mViewTypeCount == 1) {//只有一种视图类型
- scrapViews = mCurrentScrap;
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);//数据返回的同时,回收站中的内容被删除了
- } else {
- return null;
- }
- } else {//多种数据类型
- int whichScrap = mAdapter.getItemViewType(position);
- if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
- scrapViews = mScrapViews[whichScrap];
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);
- }
- }
- }
- return null;
- }
- /**
- * 将视图放入回收站
- * @param scrap
- */
- void addScrapView(View scrap) {
- AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap
- .getLayoutParams();
- if (lp == null) {
- return;
- }
- int viewType = lp.viewType;
- if (!shouldRecycleViewType(viewType)) {//过滤掉不需要回收的view
- if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- removeDetachedView(scrap, false);//分发托管事件以及通知
- }
- return;
- }
- if (mViewTypeCount == 1) {//只有一种视图类型
- scrap.dispatchStartTemporaryDetach();
- mCurrentScrap.add(scrap);
- } else {//有多种视图类型
- scrap.dispatchStartTemporaryDetach();
- mScrapViews[viewType].add(scrap);
- }
- if (mRecyclerListener != null) {//监听器回调
- mRecyclerListener.onMovedToScrapHeap(scrap);
- }
- }
总结: 贴了一大堆的代码,写了一大堆的注释,其实RecycleBin就是一个大的垃圾回收站,回收 所有可以被回收的视图 (源码过滤掉了不需要回收的视图,包括header和footer)。RecycleBin提供了一系列的方法对回收站进行管理。这里只是简单介绍了两个方法,其他方法有兴趣的同学可以自己看看。
[Android]ListView性能优化之视图缓存
前言
ListView是Android中最常用的控件,通过适配器来进行数据适配然后显示出来,而其性能是个很值得研究的话题。本文与你一起探讨Google I/O提供的优化Adapter方案,欢迎大家交流。
正文
一、准备
1.1 了解关于Google IO大会关于Adapter的优化,参考以下文章:
Android开发之ListView 适配器(Adapter)优化
Android开发——09Google I/O之让Android UI性能更高效(1)
PDF下载:Google IO.pdf
1.2 准备测试代码:
Activity
private String[] mArrData;
private TextView mTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.main);
mTV = (TextView) findViewById(R.id.tvShow);
mArrData = new String[ 1000 ];
for ( int i = 0 ; i < 1000 ; i ++ ) {
mArrData[i] = " Google IO Adapter " + i ;
}
mAdapter = new TestAdapter( this , mArrData);
((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);
}
代码说明:模拟一千条数据,TestAdapter继承自BaseAdapter,main.xml见文章末尾下载。
二、测试
测试方法:手动滑动ListView至position至50然后往回滑动,充分利用convertView不等于null的代码段。
2.1 方案一
按照Google I/O介绍的第二种方案,把item子元素分别改为4个和10个,这样效果更佳明显。
2.1.1 测试代码
private long sum = 0L ;
@Override
public View getView( int position, View convertView, ViewGroup parent) {
// 开始计时
long startTime = System.nanoTime();
if (convertView == null ) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null );
}
((ImageView) convertView.findViewById(R.id.icon1)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text1)).setText(mData[position]);
((ImageView) convertView.findViewById(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text2)).setText(mData[position]);
// 停止计时
long endTime = System.nanoTime();
// 计算耗时
long val = (endTime - startTime) / 1000L ;
Log.e( " Test " , " Position: " + position + " : " + val);
if (count < 100 ) {
if (val < 1000L ) {
sum += val;
count ++ ;
}
} else
mTV.setText(String.valueOf(sum / 100L )); // 显示统计结果
return convertView;
}
2.1.2 测试结果(微秒除以1000,见代码)
次数 | 4个子元素 | 10个子元素 |
第一次 | 366 | 723 |
第二次 | 356 | 689 |
第三次 | 371 | 692 |
第四次 | 356 | 696 |
第五次 | 371 | 662 |
按照Google I/O介绍的第三种方案,是把item子元素分别改为4个和10个。
2.2.1 测试代码
private long sum = 0L ;
@Override
public View getView( int position, View convertView, ViewGroup parent) {
// 开始计时
long startTime = System.nanoTime();
ViewHolder holder;
if (convertView == null ) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null );
holder = new ViewHolder();
holder.icon1 = (ImageView) convertView.findViewById(R.id.icon1);
holder.text1 = (TextView) convertView.findViewById(R.id.text1);
holder.icon2 = (ImageView) convertView.findViewById(R.id.icon2);
holder.text2 = (TextView) convertView.findViewById(R.id.text2);
convertView.setTag(holder);
}
else {
holder = (ViewHolder)convertView.getTag();
}
holder.icon1.setImageResource(R.drawable.icon);
holder.text1.setText(mData[position]);
holder.icon2 .setImageResource(R.drawable.icon);
holder.text2.setText(mData[position]);
// 停止计时
long endTime = System.nanoTime();
// 计算耗时
long val = (endTime - startTime) / 1000L ;
Log.e( " Test " , " Position: " + position + " : " + val);
if (count < 100 ) {
if (val < 1000L ) {
sum += val;
count ++ ;
}
} else
mTV.setText(String.valueOf(sum / 100L )); // 显示统计结果
return convertView;
}
}
static class ViewHolder {
TextView text1;
ImageView icon1;
TextView text2;
ImageView icon2;
}
2.2.2 测试结果(微秒除以1000,见代码)
次数 | 4个子元素 | 10个子元素 |
第一次 | 311 | 417 |
第二次 | 291 | 441 |
第三次 | 302 | 462 |
第四次 | 286 | 444 |
第五次 | 299 | 436 |
此方案为“Henry Hu”提示,API Level 4以上提供,这里顺带测试了一下不使用静态内部类情况下性能。
public View getView( int position, View convertView, ViewGroup parent) {
// 开始计时
long startTime = System.nanoTime();
if (convertView == null ) {
convertView = mInflater.inflate(R.layout.list_item_icon_text, null );
convertView.setTag(R.id.icon1, convertView.findViewById(R.id.icon1));
convertView.setTag(R.id.text1, convertView.findViewById(R.id.text1));
convertView.setTag(R.id.icon2, convertView.findViewById(R.id.icon2));
convertView.setTag(R.id.text2, convertView.findViewById(R.id.text2));
}
((ImageView) convertView.getTag(R.id.icon1)).setImageResource(R.drawable.icon);
((ImageView) convertView.getTag(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.getTag(R.id.text1)).setText(mData[position]);
((TextView) convertView.getTag(R.id.text2)).setText(mData[position]);
// 停止计时
long endTime = System.nanoTime();
// 计算耗时
long val = (endTime - startTime) / 1000L ;
Log.e( " Test " , " Position: " + position + " : " + val);
if (count < 100 ) {
if (val < 1000L ) {
sum += val;
count ++ ;
}
} else
mTV.setText(String.valueOf(sum / 100L ) + " : " + nullcount); // 显示统计结果
return convertView;
}
2.3.2 测试结果(微秒除以1000,见代码)
第一次:450
第二次:467
第三次:472
第四次:451
4.1 首先有一个认识是错误的,我们先来看截图:
可以发现,只有第一屏(可视范围)调用getView所消耗的时间远远多于后面的,通过对
此外了解这个原理了,那么以下代码不运行你可能猜到结果了:
convertView = mInflater.inflate(R.layout.list_item_icon_text, null );
((ImageView) convertView.findViewById(R.id.icon1)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text1)).setText(mData[position]);
((ImageView) convertView.findViewById(R.id.icon2)).setImageResource(R.drawable.icon);
((TextView) convertView.findViewById(R.id.text2)).setText(mData[position]);
}
else
return convertView;
没错,你会发现滚动时会重复显示第一屏的数据!
子控件里的事件因为是同一个控件,也可以直接放到convertView == null 代码块内部,如果需要交互数据比如position,可以通过tag方式来设置并获取当前数据。
4.2 本文方案一与方案二对比
这里推荐如果只是一般的应用(一般指子控件不多),无需都是用静态内部类来优化,使用第二种方案即可;反之,对性能要求较高时可采用。此外需要提醒的是这里也是用空间换时间的做法,View本身因为setTag而会占用更多的内存,还会增加代码量;而findViewById会临时消耗更多的内存,所以不可盲目使用,依实际情况而定。
4.3 方案三
此方案为“Henry Hu”提示,API Level 4以上支持,原理和方案三一致,减少findViewById次数,但是从测试结果来看效果并不理想,这里不再做进一步的测试。