天天看點

GridView加載大量圖檔卡的問題

最近在練習中用GridView加入相冊中圖檔發現加入大量的相片之後,GirdView會變得很卡,想到或許可以用異步加載的方式來解決,但是能力有限,想得到卻無法實作。在讀了一些大牛的部落格和代碼之後,終于實作了。

1  在異步加載之前的代碼的和普通加載代碼一樣,隻需要在GirdView的Adapter的public View getView(int position, View convertView, ViewGroupparent)方法使用異步加載的方式傳回ImageView。

2  如果能把加載過的圖檔給緩存起來,而不用每次都從sd卡上讀取,這樣效率應該會提高不少。是以可以先建一個緩存類,MemoryCache,為了能盡可能緩存,又盡可能的不抛出OOM的異常,可以使用SoftReference<Bitmap>,軟引用來緩沖圖檔。MemoryCache類的代碼如下:

/**
 * 緩存類(圖檔的路徑和圖檔)
 */
public class MemoryCache {
	private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
	//擷取緩存圖檔
	public Bitmap get(String path) {
		if(cache.get(path)==null){
			return null;
		}else{
			return cache.get(path).get();
		}
	}
	
	//新增緩存
	public void put(String url,Bitmap bitmap) {
		cache.put(url, new SoftReference<Bitmap>(bitmap));
	}
	
	//清空緩存
	public void clear() {
		cache.clear();
	}
}
           

3  弄完了緩存類,接下來就可以弄異步加載的類,這個類主要在維護一個Stack,每次向這個類請求一個ImageView時,就要傳入對應ImageView和圖檔的路徑。異步類首先會根據圖檔的路徑先去緩存中查找是否有緩存對應的BItMap,如果有就把他放到ImageView傳回,如果沒有就把這個ImageView和圖檔路徑放到Stack中,并喚醒加載圖檔的線程。而加載圖檔的線程(線程優先權低于UI線程),會無限循環檢視Stack大小,如果為0,就進入等待。如果不為0,就依次出棧Stack中的元素進行處理。(感覺像生産者-消費者模式)。

3.1 接下來,就寫這個異步類的變量和構造函數了:

private MemoryCache cache;
	private PhotosStack photosStack;
	private LoadImageThread loadImageThread;
	//用來儲存每個月ImageView和對應應加載的圖檔
	private Map<ImageView, String> viewPath=Collections.synchronizedMap(new HashMap<ImageView, String>());
	public LoadImage(Activity activity) {
		super();
		cache=new MemoryCache();
		photosStack=new PhotosStack();
		loadImageThread=new LoadImageThread(activity);
		//設定異步加載線程優先權低于UI線程
		loadImageThread.setPriority(Thread.NORM_PRIORITY);
	}
           

其中MemoryCache是緩存類,PhotosStack類中維護一個Stack,LoadImageThread類是負責異步加載圖檔的。而viewPath這個變量是為了要保證GridView對應Position放對應的ImageView,如果沒有這個變量,GridView中排列就會無序,在處理GridView的點選事件時候,就不好處理。而對viewPath的操作的異步,是以就需要線程安全咯。

PhotosStack代碼如下:

//維護需要被填充的Image和對應圖檔路徑的棧
	class PhotosStack{
		Stack<MyPhoto> stack=new Stack<LoadImage.MyPhoto>();
		//移除指定ImageView
		public void remove(ImageView iView) {
			for(int i=0;i<stack.size();i++){
				if(stack.get(i).imageView==iView){
					stack.remove(i);
					break;
				}
			}
		}
	}
           

其中MyPhoto是一個照片的實體類,有兩個屬性,圖檔的路徑和對應的ImageView。

/**
	 * 照片的實體類
	 */
	class MyPhoto{
		 public String path;
	        public ImageView imageView;
	        public MyPhoto(String path, ImageView imageView){
	        	this.path=path; 
	        	this.imageView=imageView;
	        }
	}
	
           

3.2  接下來就可以實作給Adapter的調用的方法loadImage(Stringpath,ImageView iv ),接收兩參數——圖檔路徑和對應的ImageView。這個方法,首先會去緩存中檢視是否有緩存對應的圖檔,如果有就把它設給ImageView。如果沒有,就先為ImageView設個預設圖檔,然後以同步塊(鎖為PhotosStack中的stack)的方式加入PhotosStack中的stack中,并喚醒加載圖檔的線程。最後還要判斷下加載圖檔的線程是否已經被啟動了,如果沒有,就啟動。

/**
	 * 填充ImageView
	 * @param activity 
	 * @param path 圖檔的路徑
	 * @param iv 被填充的ImageView
	 */
	public void loadImage(String path,ImageView iv ) {
		viewPath.put(iv, path);
		Bitmap momeryBitmap=cache.get(path);
		if(momeryBitmap!=null){//有緩存這張圖檔
			iv.setImageBitmap(momeryBitmap);
		}else{//沒有緩存
			iv.setImageResource(R.drawable.ic_launcher);
			//有可以這個ImageVIew還沒出棧,是以需要先出棧
			photosStack.remove(iv);
			//由于photosStack中的stack,出棧入棧操作時不會在異步的,是以需要在同步塊中完成出入棧,并以photosStack.stack作為鎖
			synchronized (photosStack.stack) {
				photosStack.stack.push(new MyPhoto(path, iv));
				//喚醒持有鎖的線程
				photosStack.stack.notifyAll();
			}
		}
		if(loadImageThread.getState()==Thread.State.NEW){
			loadImageThread.start();
		}
		
		
	}
           

3.3 最後可以實作異步加載圖檔的方法了,主要是循環判斷stack中元素的數量,如果為0 ,說明所有的圖檔已經被加載完畢了,可以進入等待狀态。如果不為0,說明還有圖檔等待加載,就依次出棧這些元素,依次加載圖檔,并放到緩存中。代碼如下:

//異步加載圖檔的線程
	class LoadImageThread extends Thread{
		Activity activity;
		
		public LoadImageThread(Activity activity) {
			this.activity = activity;
		}

		@Override
		public void run() {
			while(true){
				try {
					if(photosStack.stack.size()>0){
						final MyPhoto photo;
						//得到棧頂的MyPhoto
						synchronized (photosStack.stack) {
							 photo= photosStack.stack.pop();
						} 
						cache.put(photo.path,getSmallBitmap(photo.path) );
						String ivPathString=viewPath.get(photo.imageView);
						if(ivPathString!=null&& ivPathString.equalsIgnoreCase(photo.path)){
							Runnable runableRunnable=new Runnable() {
								@Override
								public void run() {
									photo.imageView.setImageBitmap(getSmallBitmap(photo.path));
								}
							};
							activity.runOnUiThread(runableRunnable);
						}
					}
					if(photosStack.stack.size()==0){
						synchronized (photosStack.stack) {
							photosStack.stack.wait();
						}
					}
				} catch (InterruptedException e) {
					// TODO 自動生成的 catch 塊
					e.printStackTrace();
				}
			}
		}
	}
	
           

其中縮小圖檔方法getSmallBitmap(String path),代碼如下:

//縮小圖檔
	public Bitmap getSmallBitmap(String path) {
		Options options=new Options();
		options.inJustDecodeBounds=true;
		BitmapFactory.decodeFile(path,options);
		int REQUIRE_SIZE=80;
		int scare=1;
		
		while(true){
			if(options.outWidth<=REQUIRE_SIZE|| options.outHeight<=REQUIRE_SIZE){
				break;
			}else{
				options.outWidth=options.outWidth/2;
				options.outHeight=options.outHeight/2;
				scare++;
			} 
		}
		Options newoptions=new Options();
		newoptions.inSampleSize=scare;
		return BitmapFactory.decodeFile(path, newoptions);
	}
           

其實這個也适合ListView加載大量圖檔,可以下載下傳代碼。