您的位置:首页 > 移动开发 > Android开发

[Android]ListView性能优化之视图缓存(续)

2012-04-11 11:28 387 查看

评价:

一个研究ListView优化比较全面的博客,可惜的是,没有研究map内存缓存这种策略。。

[Android]ListView性能优化之视图缓存

前言

  ListView是Android中最常用的控件,通过适配器来进行数据适配然后显示出来,而其性能是个很值得研究的话题。本文与你一起探讨Google I/O提供的优化Adapter方案,欢迎大家交流。

声明

  欢迎转载,但请保留文章原始出处:)

    博客园:http://www.cnblogs.com

    农民伯伯: http://over140.cnblogs.com
正文

  一、准备

    1.1  了解关于Google IO大会关于Adapter的优化,参考以下文章:

      Android开发之ListView 适配器(Adapter)优化

      Android开发——09Google I/O之让Android UI性能更高效(1)

      PDF下载:Google IO.pdf

    1.2  准备测试代码:

      Activity

private TestAdapter mAdapter;

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 int count = 0;

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

    2.2  方案二

      按照Google I/O介绍的第三种方案,是把item子元素分别改为4个和10个。

      2.2.1  测试代码

private int count = 0;

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
    2.3  方案三
      此方案为“Henry Hu”提示,API Level 4以上提供,这里顺带测试了一下不使用静态内部类情况下性能。

      2.3.1  测试代码

@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);

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

        第五次:441

  四、总结
    4.1  首先有一个认识是错误的,我们先来看截图:

      


      


      可以发现,只有第一屏(可视范围)调用getView所消耗的时间远远多于后面的,通过对

convertView == null内代码监控也是同样的结果。也就是说ListView仅仅缓存了可视范围内的View,随后的滚动都是对这些View进行数据更新。不管你有多少数据,他都只用ArrayList缓存可视范围内的View,这样保证了性能,也造成了我以为ListView只缓存View结构不缓存数据的假相(不会只有我一人这么认为吧- - #)。这也能解释为什么GOOGLE优化方案一比二高很多的原因。那么剩下的也就只有findViewById比较耗时了。据此大家可以看看AbsListView的源代码,看看
obtainView这个方法内的代码及RecycleBin这个类的实现,欢迎分享。
      此外了解这个原理了,那么以下代码不运行你可能猜到结果了:

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]);

}

else

return convertView;

      没错,你会发现滚动时会重复显示第一屏的数据!

      子控件里的事件因为是同一个控件,也可以直接放到convertView == null 代码块内部,如果需要交互数据比如position,可以通过tag方式来设置并获取当前数据。

    4.2  本文方案一与方案二对比

      这里推荐如果只是一般的应用(一般指子控件不多),无需都是用静态内部类来优化,使用第二种方案即可;反之,对性能要求较高时可采用。此外需要提醒的是这里也是用空间换时间的做法,View本身因为setTag而会占用更多的内存,还会增加代码量;而findViewById会临时消耗更多的内存,所以不可盲目使用,依实际情况而定。

    4.3  方案三

      此方案为“Henry Hu”提示,API Level 4以上支持,原理和方案三一致,减少findViewById次数,但是从测试结果来看效果并不理想,这里不再做进一步的测试。

  五、推荐文章

    Android,谁动了我的内存(1)

    Android 内存泄漏调试

  六、后期维护
2011-3-30 参见这里(http://www.javaeye.com/topic/971782)的讨论,据此将计划写续篇。

结束

  对于Google I/O大会这个优化方案一直抱迟疑态度,此番测试总算是有了更进一步的了解,欢迎大家先测试后交流,看看还有什么办法能够再优化一点。

[Android]ListView性能优化之视图缓存(续)

前言

  在上一篇ListView性能优化之视图缓存我们讨论了Google I/O中的优化方法,在各个论坛发帖后得到了不错的反馈,诸如:使用ViewHolder技术Tag的问题,利用HashMap自行存储的方案等。这里结合新浪微博中主界面的做法及测试数据与大家进一步探讨。

声明

  欢迎转载,但请保留文章原始出处:)

    博客园:http://www.cnblogs.com

    农民伯伯: http://over140.cnblogs.com
文章

  [Android]ListView性能优化之视图缓存 [本文的上篇]

  [Android]ListView性能优化之视图缓存 [JavaEye讨论帖]

正文

  一、新浪微博

    1.1  截图

      

(来自网络)

    1.2  反编译后相关代码

      HomeListActivity

public View getView(int paramInt, View paramView, ViewGroup paramViewGroup)

{

int i = --paramInt;

int j = -1;

if (i == j);

for (Object localObject1 = HomeListActivity.this.getReloadView(); ; localObject1 = HomeListActivity.this.getLoadMoreView())

{

label26: return localObject1;

int k = HomeListActivity.this.mList.size();

int l = paramInt;

int i1 = k;

if (l != i1)

break;

}

boolean bool1 = true;

boolean bool2 = null;

String str1;

label110: Object localObject2;

if (StaticInfo.mUser == null)

{

List localList1 = HomeListActivity.this.mList;

int i2 = paramInt;

str1 = ((MBlog)localList1.get(i2)).uid;

List localList2 = HomeListActivity.this.mList;

int i3 = paramInt;

String str2 = ((MBlog)localList2.get(i3)).uid;

String str3 = str1;

if (!str2.equals(str3))

break label271;

int i4 = 1;

label156: if (paramView != null)

break label277;

HomeListActivity localHomeListActivity1 = HomeListActivity.this;

ListView localListView1 = HomeListActivity.this.mLvHome;

List localList3 = HomeListActivity.this.mList;

int i5 = paramInt;

MBlog localMBlog1 = (MBlog)localList3.get(i5);

HomeListActivity localHomeListActivity2 = HomeListActivity.this;

int i6 = paramInt;

boolean bool4 = localHomeListActivity2.isNewCommer(i6);

int i7 = HomeListActivity.this.mReadMode;

localObject2 = new MBlogListItemView(localHomeListActivity1, localListView1, localMBlog1, bool1, bool2, i4, bool4, i7);

}

while (true)

{

localObject1 = localObject2;

break label26:

str1 = StaticInfo.mUser.uid;

break label110:

label271: boolean bool3 = null;

break label156:

label277: localObject2 = paramView;

try

{

MainListItemView localMainListItemView = (MainListItemView)localObject2;

List localList4 = HomeListActivity.this.mList;

int i8 = paramInt;

Object localObject3 = localList4.get(i8);

HomeListActivity localHomeListActivity3 = HomeListActivity.this;

int i9 = paramInt;

boolean bool5 = localHomeListActivity3.isNewCommer(i9);

int i10 = HomeListActivity.this.mReadMode;

boolean bool6 = bool1;

boolean bool7 = bool2;

localMainListItemView.update(localObject3, bool6, bool7, bool5, i10);

}

catch (Exception localException)

{

HomeListActivity localHomeListActivity4 = HomeListActivity.this;

ListView localListView2 = HomeListActivity.this.mLvHome;

List localList5 = HomeListActivity.this.mList;

int i11 = paramInt;

MBlog localMBlog2 = (MBlog)localList5.get(i11);

HomeListActivity localHomeListActivity5 = HomeListActivity.this;

int i12 = paramInt;

boolean bool8 = localHomeListActivity5.isNewCommer(i12);

int i13 = HomeListActivity.this.mReadMode;

localObject2 = new MBlogListItemView(localHomeListActivity4, localListView2, localMBlog2, bool1, bool2, bool3, bool8, i13);

}

}

}

        代码说明:

          代码流程已经比较混乱,但是这里能看到并没有直接的inflate,而是自定义了继承自LinearLayout的MBlogListItemView。

      MBlogListItemView

public MBlogListItemView(Context paramContext, ListView paramListView, MBlog paramMBlog, boolean paramBoolean1, boolean paramBoolean2, boolean paramBoolean3, boolean paramBoolean4, int paramInt)

{

super(paramContext);

this.context = paramContext;

this.parent = paramListView;

this.mBlog = paramMBlog;

String str1 = paramContext.getCacheDir().getAbsolutePath();

this.mCacheDir = str1;

String str2 = paramContext.getFilesDir().getAbsolutePath();

this.mFileDir = str2;

((LayoutInflater)paramContext.getSystemService("layout_inflater")).inflate(2130903061, this);

TextView localTextView1 = (TextView)findViewById(2131624016);

this.mName = localTextView1;

TextView localTextView2 = (TextView)findViewById(2131624041);

this.mDate = localTextView2;

TextView localTextView3 = (TextView)findViewById(2131624018);

this.mContent = localTextView3;

TextView localTextView4 = (TextView)findViewById(2131624046);

this.mSubContent = localTextView4;

ImageView localImageView1 = (ImageView)findViewById(2131624040);

this.mIconV = localImageView1;

ImageView localImageView2 = (ImageView)findViewById(2131624042);

this.mIconPic = localImageView2;

ImageView localImageView3 = (ImageView)findViewById(2131624044);

this.mUploadPic1 = localImageView3;

ImageView localImageView4 = (ImageView)findViewById(2131623979);

this.mUploadPic2 = localImageView4;

TextView localTextView5 = (TextView)findViewById(2131624047);

this.tvForm = localTextView5;

TextView localTextView6 = (TextView)findViewById(2131623989);

this.tvComment = localTextView6;

this.tvComment.setOnClickListener(this);

TextView localTextView7 = (TextView)findViewById(2131623988);

this.tvRedirect = localTextView7;

this.tvRedirect.setOnClickListener(this);

ImageView localImageView5 = (ImageView)findViewById(2131624049);

this.imComment = localImageView5;

this.imComment.setOnClickListener(this);

ImageView localImageView6 = (ImageView)findViewById(2131624048);

this.imRedirect = localImageView6;

this.imRedirect.setOnClickListener(this);

ImageView localImageView7 = (ImageView)findViewById(2131624043);

this.imGpsIcon = localImageView7;

ImageView localImageView8 = (ImageView)findViewById(2131624013);

this.mPortrait = localImageView8;

LinearLayout localLinearLayout = (LinearLayout)findViewById(2131624045);

this.mSubLayout = localLinearLayout;

this.mReadMode = paramInt;

MBlogListItemView localMBlogListItemView = this;

MBlog localMBlog = paramMBlog;

boolean bool1 = paramBoolean1;

boolean bool2 = paramBoolean2;

boolean bool3 = paramBoolean4;

int i = paramInt;

localMBlogListItemView.update(localMBlog, bool1, bool2, bool3, i);

this.mUploadPic1.setOnClickListener(this);

this.mUploadPic2.setOnClickListener(this);

}

    代码说明:

      a).  MBlogListItemView extends LinearLayout
implements MainListItemView

      b).  inflate(2130903061,this)这个数字代表R.layout.itemview。

  二、测试方案(方案五)

    按照新浪微博类似的做法进行测试。

    2.1  测试代码

@Override

public View getView(int position, View convertView, ViewGroup parent) {

// 开始计时

long startTime = System.nanoTime();

TestItemLayout item;

if (convertView == null) {

item = new TestItemLayout(BaseAdapterActivity.this);

} else

item = (TestItemLayout) convertView;

item.icon1.setImageResource(R.drawable.icon);

item.text1.setText(mData[position]);

item.icon2.setImageResource(R.drawable.icon);

item.text2.setText(mData[position]);

// 停止计时

long endTime = System.nanoTime();

// 计算耗时

long val = (endTime - startTime) / 1000L;

Log.e("Test", "Position:" + position + ":" + val);

if (count < 100) {

if (val < 2000L) {

sum += val;

count++;

}

} else

mTV.setText(String.valueOf(sum / 100L) + ":" + nullcount);// 显示统计结果

return item;

}

      TestItemLayout

public class TestItemLayout extends LinearLayout {

public TextView text1;

public ImageView icon1;

public TextView text2;

public ImageView icon2;

public TestItemLayout(Context context) {

super(context);

((LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(

R.layout.list_item_icon_text, this);

icon1 = (ImageView) findViewById(R.id.icon1);

text1 = (TextView) findViewById(R.id.text1);

icon2 = (ImageView) findViewById(R.id.icon2);

text2 = (TextView) findViewById(R.id.text2);

}

}

    2.2  测试结果

次数
4个子元素
10个子元素
第一次
347
460

第二次
310

477

第三次
324
508

第四次
339

492

第五次
341
465
  三、总结

    从测试结果来看与ViewHolder性能非常接近,不会出现tag图片变小的问题(关于图片变小的问题,有朋友说是TAG中的元素对大小和位置有记忆),也能有效的减少findViewById的执行次数,这里建议完全可以取代ViewHolder。

    关于ListView内部Adapter的心得大家可以看一下上文的总结4.1。

  四、考虑

    关于静态内部类这里不是很理解,是否能应用方案五还有待验证。

  五、后期维护
2011-4-29 来自Stony Wang关于Tag的解释:

Stony Wang
tag的用途应该是仿照delphi的来的,设置一个关联的数据。

简单的说就是,你的UI控件有时候显示的内容带源于(绑定?)某个数据或者对象实例。

当你处理一些事件的时候,不推荐从UI上来重新获取,而是从Tag里面取出来。

举一个例子是,有一个按钮,一开始显示"0",然后每按一次计数增1。

每次click的时候,

1 从btn.getText()再转回Integer

2 从tag里面把之前设好的Integer拿出来,加一,再settag?

如果觉得1和2区别不大的话,那么如果显示的内容不是"0",而是"已经点了0次"呢?

更新UI的时候,可以将关联的对象放到tag里面,在处理相关触发事件的时候,可以方便的获取原始的数据。

ListView的Tag用法也不算很错,而是用的时候没有注意设置的时候要注意“对称”。

Tag本身可以理解成放ViewHolder,那么和ViewHolder的加速只不过是存放的位置不同,加速效果基本一致。

“对称”我所指的内容是:

不管你要显示的数据的逻辑是如何的,如果你设置了某个View的宽度,那么在任何一种数据的填充UI逻辑里面,不可以有不设置这个View宽度的代码路径。

简单的例子就是,我根据某个布尔值,如果是false的话,将ImageView设置为View.INVISIBLE

if ( item.isHidden()){

mImage.setVisibility(View.INVISIBLE);

}

这样是错误的,因为如果这里缓存的UI控件的状态会被复用到其它item上,而这个item恰巧可能是需要显示的。

必须补上else语句

else{

mImage.setVisibility(View.VISIBLE);

}

这个估计就是那位仁兄拖动图片变小的原因了。

最后说一下,ListView控件条目部分,一共产生的条目是屏幕能容纳的数目+2(还是+1?我记不清楚了),然后循环使用。

结束

  优化ListView不仅仅只有对convertView的优化,还有许多这样那样的技巧,欢迎大家交流与分享 :)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: