Android中的延遲加載系列4(ImageView)


在Android應用程序的開發中,從網絡或者服務器上取得圖片,往往需要花費一定的時間,佔用一定的用戶帶寬,當頁面有大量的圖片時,如果不採取延遲加載的方法,則客戶端需要等到所有的圖片都獲取之後,纔可以呈現完整界面,這就可能導致界面反應不流暢,影響用戶體驗。


圖片延遲加載的原理其實非常簡單,有兩種思路:

第一種思路是後臺啓動Thread下載圖片,下載完成後,通過Message Handle的方式把控制權轉讓給UI Thread並繪製圖片。此方法的優點是比較簡單,缺點是不容易封裝。

第二種思路是啓動異步任務AsyncTask,通過doInBackground()方法獲得圖片資源之後,再通過onPostExecute()方法在UI Thread中繪製取好的圖片。


以上兩種方式都可以很好地處理圖片的延遲加載。本文通過第一種方式來處理,對圖片的延遲加載進行封裝,並對性能進行如下優化:

1) 圖片加載的線程優先級比UI低一級,這樣可以優先處理UI任務。

2) 在內存、磁盤緩存下載的應用程序圖片。讀取圖片的順序爲 內存 -> 磁盤 -> 網絡,這樣可以避免下載重複的圖片,節省網絡流量,並且提高響應速度和性能。

3) 本地緩存的圖片可能由於某種原因過期,而與服務端不一致,比如服務端更新了圖片資源,這個時候本地並不知道,從而導致了圖片的不一致性,這也是採取緩存技術提高性能而導致的副作用。爲了解決這個問題,可以引入時間戳的概念,當時間戳發生變化之後,重新從網絡上獲取圖片並緩存。


以下將做簡要的說明。


首先建立LazyImage對象,此對象包括了圖片image,圖片的網絡資源鏈接url,以及圖片所對應的時間戳(從服務端獲得,如果沒有時間戳的固定圖片,也可以不用設置)。

package com.whyonly.core.bean;

import android.graphics.Bitmap;

public abstract class LazyImage {
	
	private Bitmap image;
	private String image_url;
	private long timestamp;
	
	
	public boolean hasLoadPhoto(){
		return image==null ? false : true;
	}
	public long getTimestamp() {
		return timestamp;
	}
	public void setTimestamp(long timestamp) {
		this.timestamp = timestamp;
		this.hashCode();
	}
	
	
	
	public Bitmap getImage() {
		return image;
	}
	public void setImage(Bitmap image) {
		this.image = image;
	}
	public String getImage_url() {
		return image_url;
	}
	public void setImage_url(String image_url) {
		this.image_url = image_url;
	}

	
	public String toFileName(){ //convert the url + timestamp to file name to instore to local disk
		String fileName = "";
		if(image_url!=null && image_url.indexOf("/")!=-1 ){
			fileName=image_url.substring(image_url.lastIndexOf("/")+1,image_url.length());
		}
		return  fileName+"_"+timestamp;
	}
	
	

}

建立一個下載任務

//Task for the queue
    private class PhotoToLoad
    {
        public LazyImage lazyImage;
        public ImageView imageView;
        public boolean saveDisk;
        public PhotoToLoad(LazyImage l, ImageView i, boolean s){
        	lazyImage=l; 
            imageView=i;
            saveDisk = s;
        }
    }

以及下載隊列

    //stores list of photos to download
    class PhotosQueue
    {
        private Stack<PhotoToLoad> photosToLoadStack=new Stack<PhotoToLoad>();
        
        //removes all instances of this ImageView
        public void clean(ImageView image)
        {
            for(int j=0 ;j<photosToLoadStack.size();){
                if(photosToLoadStack.get(j).imageView==image)
                    photosToLoadStack.remove(j);
                else
                    ++j;
            }
        }
    }




接着建立內存緩衝類:

class MemoryCache {
    private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
    
    public Bitmap get(String id){
        if(!cache.containsKey(id))
            return null;
        SoftReference<Bitmap> ref=cache.get(id);
        return ref.get();
    }
    
    public void put(String id, Bitmap bitmap){
        cache.put(id, new SoftReference<Bitmap>(bitmap));
    }

    public void clear() {
        cache.clear();
    }
}


和本地磁盤緩衝類

class FileCache {
  
	private File cacheDir;
    
    public FileCache(Context context){
        //Find the dir to save cached images
//        if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
            cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"whyonly/cache");
//        else
//            cacheDir=context.getCacheDir();
        Log.d("ImageLoader","cacheDir:"+cacheDir);
        if(!cacheDir.exists())
            cacheDir.mkdirs();
    }
    
	public File getFile(LazyImage lazyImage){   
        String filename= lazyImage.toFileName();
        File f = new File(cacheDir, filename);
        return f;
        
    }
    
    public void clear(){
        File[] files=cacheDir.listFiles();
        for(File f:files)
            f.delete();
    }

}

把圖片資源顯示到ImageView

//Used to display bitmap in the UI thread
    class BitmapDisplayer implements Runnable
    {
        Bitmap bitmap;
        ImageView imageView;
        public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
        public void run()
        {
            if(bitmap!=null)
                imageView.setImageBitmap(bitmap);
            else
                imageView.setImageResource(defaultImageResId);
        }
    }

最後是通過線程來控制圖片的下載和顯示過程

    class PhotosLoaderThread extends Thread {
        public void run() {
            try {
                while(true)
                {
                    //thread waits until there are any images to load in the queue
                    if(photosQueue.photosToLoadStack.size()==0)
                        synchronized(photosQueue.photosToLoadStack){
                            photosQueue.photosToLoadStack.wait();
                        }
                    if(photosQueue.photosToLoadStack.size()!=0)
                    {
                        PhotoToLoad photoToLoad;
                        synchronized(photosQueue.photosToLoadStack){
                            photoToLoad=photosQueue.photosToLoadStack.pop();
                        }
                        Bitmap bmp=getBitmap(photoToLoad.lazyImage,photoToLoad.saveDisk);
                        memoryCache.put(photoToLoad.lazyImage.toFileName(), bmp);
                        String tag=imageViews.get(photoToLoad.imageView);
                        if(tag!=null && tag.equals(photoToLoad.lazyImage.toFileName())){
                            BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                            Activity a=(Activity)photoToLoad.imageView.getContext();
                            a.runOnUiThread(bd);
                        }
                    }
                    if(Thread.interrupted())
                        break;
                }
            } catch (InterruptedException e) {
                //allow thread to exit
            }
        }
    }

完整的圖片加載類如下:

package com.whyonly.core.wrapper;

import java.io.File;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.WeakHashMap;

import com.whyonly.core.bean.LazyImage;
import com.whyonly.core.util.ImageUtil;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.widget.ImageView;

public class ImageLoader {
    
    private static final String TAG = "ImageLoader";
	private MemoryCache memoryCache;
    private FileCache fileCache;
    private Map<ImageView, String> imageViews;
    
    private PhotosLoaderThread photoLoaderThread;
    private PhotosQueue photosQueue;
    private int defaultImageResId;
    //private Context context;
    
    public ImageLoader(Context context,int defaultImageResId){
        //Make the background thead low priority. So it will not affect the UI performance
    	photoLoaderThread=new PhotosLoaderThread();
    	photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);  
    	memoryCache=new MemoryCache();
        fileCache=new FileCache(context);
        imageViews=Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
        photosQueue=new PhotosQueue();
        //this.context = context;
        this.defaultImageResId = defaultImageResId;
    }
    
    public void displayImage(LazyImage lazyImage, ImageView imageView){
    	displayImage(lazyImage,imageView,true);
    }
    
    public void displayImage(LazyImage lazyImage, ImageView imageView,boolean saveDisk)
    {
        imageViews.put(imageView, lazyImage.toFileName());  
        if(lazyImage.getImage()!=null){
        	imageView.setImageBitmap(lazyImage.getImage());//get from lazy image
            //Log.d(TAG,"----LazyImage cache:"+lazyImage.toFileName());
        }else{
        	Bitmap bitmap=memoryCache.get(lazyImage.toFileName());//get from memory cache
            if(bitmap!=null){
            	lazyImage.setImage(bitmap);
                imageView.setImageBitmap(bitmap);
                //Log.d(TAG,"----MEMORY cache:"+lazyImage.toFileName());
            }else
            {
            	if(defaultImageResId>0)
                	imageView.setImageResource(defaultImageResId);
                else
                	imageView.setImageBitmap(null);
            	if(lazyImage.getImage_url() != null)
            		queuePhoto(lazyImage, imageView,saveDisk);//get from SD card or web 
            }    
        }
    }
        
    private void queuePhoto(LazyImage lazyImage, ImageView imageView,boolean saveDisk)
    {
        //This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them. 
        photosQueue.clean(imageView);
        PhotoToLoad photosToLoad=new PhotoToLoad(lazyImage, imageView, saveDisk);
        synchronized(photosQueue.photosToLoadStack){
            photosQueue.photosToLoadStack.push(photosToLoad);
            photosQueue.photosToLoadStack.notifyAll();
        }
        
        //start thread if it's not started yet
        if(photoLoaderThread.getState()==Thread.State.NEW)
            photoLoaderThread.start();
    }
    
    private Bitmap getBitmap(LazyImage lazyImage,boolean saveDisk) 
    {
      	if(!saveDisk){
      		return ImageUtil.returnBitMap(lazyImage.getImage_url());
      	}
    	
    	File f=fileCache.getFile(lazyImage);
        //from SD cache
        Bitmap b = ImageUtil.file2Bitmap(f);
        if(b!=null){
        	lazyImage.setImage(b);
        	//Log.d(TAG,"----FILE cache:"+lazyImage.toFileName());
            return b;
        } 
        
        //from web
        try {
            URL imageUrl = new URL(lazyImage.getImage_url());
            HttpURLConnection conn = (HttpURLConnection)imageUrl.openConnection();
            conn.setConnectTimeout(30000);
            conn.setReadTimeout(30000);
            InputStream is=conn.getInputStream();
            ImageUtil.inputStream2File(is, f);
            lazyImage.setImage(ImageUtil.file2Bitmap(f));
            //Log.d(TAG,"----WEB URL:"+lazyImage.toFileName());
            return lazyImage.getImage();
        } catch (Exception ex){
           //ex.printStackTrace();
           return null;
        }
    }

    
    //Task for the queue
    private class PhotoToLoad
    {
        public LazyImage lazyImage;
        public ImageView imageView;
        public boolean saveDisk;
        public PhotoToLoad(LazyImage l, ImageView i, boolean s){
        	lazyImage=l; 
            imageView=i;
            saveDisk = s;
        }
    }
    
    
    public void stopThread()
    {
        photoLoaderThread.interrupt();
    }
    
    //stores list of photos to download
    class PhotosQueue
    {
        private Stack<PhotoToLoad> photosToLoadStack=new Stack<PhotoToLoad>();
        
        //removes all instances of this ImageView
        public void clean(ImageView image)
        {
            for(int j=0 ;j<photosToLoadStack.size();){
                if(photosToLoadStack.get(j).imageView==image)
                    photosToLoadStack.remove(j);
                else
                    ++j;
            }
        }
    }
    
    class PhotosLoaderThread extends Thread {
        public void run() {
            try {
                while(true)
                {
                    //thread waits until there are any images to load in the queue
                    if(photosQueue.photosToLoadStack.size()==0)
                        synchronized(photosQueue.photosToLoadStack){
                            photosQueue.photosToLoadStack.wait();
                        }
                    if(photosQueue.photosToLoadStack.size()!=0)
                    {
                        PhotoToLoad photoToLoad;
                        synchronized(photosQueue.photosToLoadStack){
                            photoToLoad=photosQueue.photosToLoadStack.pop();
                        }
                        Bitmap bmp=getBitmap(photoToLoad.lazyImage,photoToLoad.saveDisk);
                        memoryCache.put(photoToLoad.lazyImage.toFileName(), bmp);
                        String tag=imageViews.get(photoToLoad.imageView);
                        if(tag!=null && tag.equals(photoToLoad.lazyImage.toFileName())){
                            BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                            Activity a=(Activity)photoToLoad.imageView.getContext();
                            a.runOnUiThread(bd);
                        }
                    }
                    if(Thread.interrupted())
                        break;
                }
            } catch (InterruptedException e) {
                //allow thread to exit
            }
        }
    }
    
    
    
    //Used to display bitmap in the UI thread
    class BitmapDisplayer implements Runnable
    {
        Bitmap bitmap;
        ImageView imageView;
        public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
        public void run()
        {
            if(bitmap!=null)
                imageView.setImageBitmap(bitmap);
            else
                imageView.setImageResource(defaultImageResId);
        }
    }

    public void clearCache() {
        memoryCache.clear();
        fileCache.clear();
    }
    
    public void clearMemoryCache() {
        memoryCache.clear();
    }

}

class MemoryCache {
    private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
    
    public Bitmap get(String id){
        if(!cache.containsKey(id))
            return null;
        SoftReference<Bitmap> ref=cache.get(id);
        return ref.get();
    }
    
    public void put(String id, Bitmap bitmap){
        cache.put(id, new SoftReference<Bitmap>(bitmap));
    }

    public void clear() {
        cache.clear();
    }
}

class FileCache {
  
	private File cacheDir;
    
    public FileCache(Context context){
        //Find the dir to save cached images
//        if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
            cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"whyonly/cache");
//        else
//            cacheDir=context.getCacheDir();
        Log.d("ImageLoader","cacheDir:"+cacheDir);
        if(!cacheDir.exists())
            cacheDir.mkdirs();
    }
    
	public File getFile(LazyImage lazyImage){   
        String filename= lazyImage.toFileName();
        File f = new File(cacheDir, filename);
        return f;
        
    }
    
    public void clear(){
        File[] files=cacheDir.listFiles();
        for(File f:files)
            f.delete();
    }

}


通過 ImageLoader的包裝,應用起來應該非常簡單,示例代碼如下:

private ImageLoader imageLoader = new ImageLoader(this,R.drawable.nohead);

imageLoader.displayImage(bean, imageView);


以上給出來圖片延遲加載的基本概念,以及通過一個示例,說明了如何封裝,以便應用程序可以簡單地調用。下一篇將通過一個完整的工程例子,對圖片的延遲加載,以及ListView的延遲加載加載進行綜述。(待續)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章