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的异步加载性能优化的全部介绍,希望对大家有所帮助。