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

评价:

一个研究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的优化,还有许多这样那样的技巧,欢迎大家交流与分享 :)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值