Listview的异步加载性能优化
Android中ListView是使用平率最高的控件之一(GridView跟ListView是兄弟,都是继承AbsListView),ListView优化最有效的无非就是采用ViewHolder来减少频繁的对view查询和更新,缓存图片加快解码,减小图片尺寸。
关于listview的异步加载,网上其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化,下面就让在下阐述其原理以探索个中奥秘在APP应用中,listview的异步加载图片方式能够带来很好的用户体验,同时也是考量程序性能的一个重要指标。关于listview的异步加载,网上其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化。有鉴于此,本人在网上找了个相对理想的版本并在此基础上进行改造,下面就让在下阐述其原理以探索个中奥秘,与诸君共赏…
异步加载图片基本思想:
1.先从内存缓存中获取图片显示(内存缓冲)
2.获取不到的话从SD卡里获取(SD卡缓冲)
3.都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)
OK,先上adapter的代码:
publicclassLoaderAdapterextendsBaseAdapter{ privatestaticfinalStringTAG="LoaderAdapter"; privatebooleanmBusy=false; publicvoidsetFlagBusy(booleanbusy){ this.mBusy=busy; } privateImageLoadermImageLoader; privateintmCount; privateContextmContext; privateString[]urlArrays; publicLoaderAdapter(intcount,Contextcontext,String[]url){ this.mCount=count; this.mContext=context; urlArrays=url; mImageLoader=newImageLoader(context); } publicImageLoadergetImageLoader(){ returnmImageLoader; } @Override publicintgetCount(){ returnmCount; } @Override publicObjectgetItem(intposition){ returnposition; } @Override publiclonggetItemId(intposition){ returnposition; } @Override publicViewgetView(intposition,ViewconvertView,ViewGroupparent){ ViewHolderviewHolder=null; if(convertView==null){ convertView=LayoutInflater.from(mContext).inflate( R.layout.list_item,null); viewHolder=newViewHolder(); viewHolder.mTextView=(TextView)convertView .findViewById(R.id.tv_tips); viewHolder.mImageView=(ImageView)convertView .findViewById(R.id.iv_image); convertView.setTag(viewHolder); }else{ viewHolder=(ViewHolder)convertView.getTag(); } Stringurl=""; url=urlArrays[position%urlArrays.length]; viewHolder.mImageView.setImageResource(R.drawable.ic_launcher); if(!mBusy){ mImageLoader.DisplayImage(url,viewHolder.mImageView,false); viewHolder.mTextView.setText("--"+position +"--IDLE||TOUCH_SCROLL"); }else{ mImageLoader.DisplayImage(url,viewHolder.mImageView,true); viewHolder.mTextView.setText("--"+position+"--FLING"); } returnconvertView; } staticclassViewHolder{ TextViewmTextView; ImageViewmImageView; } }
关键代码是ImageLoader的DisplayImage方法,再看ImageLoader的实现
publicclassImageLoader{ privateMemoryCachememoryCache=newMemoryCache(); privateAbstractFileCachefileCache; privateMap<ImageView,String>imageViews=Collections .synchronizedMap(newWeakHashMap<ImageView,String>()); //线程池 privateExecutorServiceexecutorService; publicImageLoader(Contextcontext){ fileCache=newFileCache(context); executorService=Executors.newFixedThreadPool(5); } //最主要的方法 publicvoidDisplayImage(Stringurl,ImageViewimageView,booleanisLoadOnlyFromCache){ imageViews.put(imageView,url); //先从内存缓存中查找 Bitmapbitmap=memoryCache.get(url); if(bitmap!=null) imageView.setImageBitmap(bitmap); elseif(!isLoadOnlyFromCache){ //若没有的话则开启新线程加载图片 queuePhoto(url,imageView); } } privatevoidqueuePhoto(Stringurl,ImageViewimageView){ PhotoToLoadp=newPhotoToLoad(url,imageView); executorService.submit(newPhotosLoader(p)); } privateBitmapgetBitmap(Stringurl){ Filef=fileCache.getFile(url); //先从文件缓存中查找是否有 Bitmapb=null; if(f!=null&&f.exists()){ b=decodeFile(f); } if(b!=null){ returnb; } //最后从指定的url中下载图片 try{ Bitmapbitmap=null; URLimageUrl=newURL(url); HttpURLConnectionconn=(HttpURLConnection)imageUrl .openConnection(); conn.setConnectTimeout(30000); conn.setReadTimeout(30000); conn.setInstanceFollowRedirects(true); InputStreamis=conn.getInputStream(); OutputStreamos=newFileOutputStream(f); CopyStream(is,os); os.close(); bitmap=decodeFile(f); returnbitmap; }catch(Exceptionex){ Log.e("","getBitmapcatchException...\nmessage="+ex.getMessage()); returnnull; } } //decode这个图片并且按比例缩放以减少内存消耗,虚拟机对每张图片的缓存大小也是有限制的 privateBitmapdecodeFile(Filef){ try{ //decodeimagesize BitmapFactory.Optionso=newBitmapFactory.Options(); o.inJustDecodeBounds=true; BitmapFactory.decodeStream(newFileInputStream(f),null,o); //Findthecorrectscalevalue.Itshouldbethepowerof2. finalintREQUIRED_SIZE=100; intwidth_tmp=o.outWidth,height_tmp=o.outHeight; intscale=1; while(true){ if(width_tmp/2<REQUIRED_SIZE ||height_tmp/2<REQUIRED_SIZE) break; width_tmp/=2; height_tmp/=2; scale*=2; } //decodewithinSampleSize BitmapFactory.Optionso2=newBitmapFactory.Options(); o2.inSampleSize=scale; returnBitmapFactory.decodeStream(newFileInputStream(f),null,o2); }catch(FileNotFoundExceptione){ } returnnull; } //Taskforthequeue privateclassPhotoToLoad{ publicStringurl; publicImageViewimageView; publicPhotoToLoad(Stringu,ImageViewi){ url=u; imageView=i; } } classPhotosLoaderimplementsRunnable{ PhotoToLoadphotoToLoad; PhotosLoader(PhotoToLoadphotoToLoad){ this.photoToLoad=photoToLoad; } @Override publicvoidrun(){ if(imageViewReused(photoToLoad)) return; Bitmapbmp=getBitmap(photoToLoad.url); memoryCache.put(photoToLoad.url,bmp); if(imageViewReused(photoToLoad)) return; BitmapDisplayerbd=newBitmapDisplayer(bmp,photoToLoad); //更新的操作放在UI线程中 Activitya=(Activity)photoToLoad.imageView.getContext(); a.runOnUiThread(bd); } } /** *防止图片错位 * *@paramphotoToLoad *@return */ booleanimageViewReused(PhotoToLoadphotoToLoad){ Stringtag=imageViews.get(photoToLoad.imageView); if(tag==null||!tag.equals(photoToLoad.url)) returntrue; returnfalse; } //用于在UI线程中更新界面 classBitmapDisplayerimplementsRunnable{ Bitmapbitmap; PhotoToLoadphotoToLoad; publicBitmapDisplayer(Bitmapb,PhotoToLoadp){ bitmap=b; photoToLoad=p; } publicvoidrun(){ if(imageViewReused(photoToLoad)) return; if(bitmap!=null) photoToLoad.imageView.setImageBitmap(bitmap); } } publicvoidclearCache(){ memoryCache.clear(); fileCache.clear(); } publicstaticvoidCopyStream(InputStreamis,OutputStreamos){ finalintbuffer_size=1024; try{ byte[]bytes=newbyte[buffer_size]; for(;;){ intcount=is.read(bytes,0,buffer_size); if(count==-1) break; os.write(bytes,0,count); } }catch(Exceptionex){ Log.e("","CopyStreamcatchException..."); } } }
先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅,这是优化一。于此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片,这是优化二。ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁,有的童鞋每次总是new一个线程去执行这是非常不可取的,好一点的用的AsyncTask类,其实内部也是用到了线程池。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存,这是优化三。
而图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item的时候,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候其实该item已经不在当前显示区域内了,此时显示的后果将是在可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageViews的map对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。
下面再说下内存缓冲机制,本例采用的是LRU算法,先看看MemoryCache的实现
publicclassMemoryCache{ privatestaticfinalStringTAG="MemoryCache"; //放入缓存时是个同步操作 //LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU //这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率 privateMap<String,Bitmap>cache=Collections .synchronizedMap(newLinkedHashMap<String,Bitmap>(10,1.5f,true)); //缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存 privatelongsize=0;//currentallocatedsize //缓存只能占用的最大堆内存 privatelonglimit=1000000;//maxmemoryinbytes publicMemoryCache(){ //use25%ofavailableheapsize setLimit(Runtime.getRuntime().maxMemory()/10); } publicvoidsetLimit(longnew_limit){ limit=new_limit; Log.i(TAG,"MemoryCachewilluseupto"+limit/1024./1024.+"MB"); } publicBitmapget(Stringid){ try{ if(!cache.containsKey(id)) returnnull; returncache.get(id); }catch(NullPointerExceptionex){ returnnull; } } publicvoidput(Stringid,Bitmapbitmap){ try{ if(cache.containsKey(id)) size-=getSizeInBytes(cache.get(id)); cache.put(id,bitmap); size+=getSizeInBytes(bitmap); checkSize(); }catch(Throwableth){ th.printStackTrace(); } } /** *严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存 * */ privatevoidcheckSize(){ Log.i(TAG,"cachesize="+size+"length="+cache.size()); if(size>limit){ //先遍历最近最少使用的元素 Iterator<Entry<String,Bitmap>>iter=cache.entrySet().iterator(); while(iter.hasNext()){ Entry<String,Bitmap>entry=iter.next(); size-=getSizeInBytes(entry.getValue()); iter.remove(); if(size<=limit) break; } Log.i(TAG,"Cleancache.Newsize"+cache.size()); } } publicvoidclear(){ cache.clear(); } /** *图片占用的内存 * *<Ahref='\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"'target='\"_blank\"'>@Param</A>bitmap * *@return */ longgetSizeInBytes(Bitmapbitmap){ if(bitmap==null) return0; returnbitmap.getRowBytes()*bitmap.getHeight(); } }
首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里加时判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除,当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担,OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成的。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。
以上所述是针对listview的异步加载性能优化的全部介绍,希望对大家有所帮助。