1. 程式人生 > >【LruCache和DiskLruCache結合】圖片快取機制

【LruCache和DiskLruCache結合】圖片快取機制

本文是對網路上幾篇文章的綜合,

 

第一部分,使用LruCache和DiskLruCache:

新建一個Android專案,起名叫PhotoWallDemo,這裡我使用的是Android 4.0的API。然後新建一個libcore.io包,並將DiskLruCache.java檔案拷貝到這個包下,這樣就把準備工作完成了。

接下來首先需要考慮的仍然是圖片源的問題,簡單起見,我仍然是吧所有圖片都上傳到了我的CSDN相簿當中,然後新建一個Images類,將所有相簿中圖片的網址都配置進去,程式碼如下所示:

public class Images {

	public final static String[] imageThumbUrls = new String[] {
		"http://img.my.csdn.net/uploads/201407/26/1406383299_1976.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383291_6518.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383291_8239.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383290_9329.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383290_1042.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383275_3977.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383265_8550.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383264_3954.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383264_4787.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383264_8243.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383248_3693.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383243_5120.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383242_3127.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383242_9576.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383242_1721.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383219_5806.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383214_7794.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383213_4418.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383213_3557.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383210_8779.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383172_4577.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383166_3407.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383166_2224.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383166_7301.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383165_7197.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383150_8410.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383131_3736.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383130_5094.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383130_7393.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383129_8813.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383100_3554.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383093_7894.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383092_2432.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383092_3071.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383091_3119.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383059_6589.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383059_8814.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383059_2237.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383058_4330.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406383038_3602.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382942_3079.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382942_8125.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382942_4881.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382941_4559.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382941_3845.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382924_8955.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382923_2141.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382923_8437.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382922_6166.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382922_4843.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382905_5804.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382904_3362.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382904_2312.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382904_4960.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382900_2418.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382881_4490.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382881_5935.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382880_3865.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382880_4662.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382879_2553.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382862_5375.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382862_1748.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382861_7618.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382861_8606.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382861_8949.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382841_9821.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382840_6603.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382840_2405.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382840_6354.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382839_5779.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382810_7578.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382810_2436.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382809_3883.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382809_6269.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382808_4179.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382790_8326.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382789_7174.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382789_5170.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382789_4118.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382788_9532.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382767_3184.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382767_4772.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382766_4924.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382766_5762.jpg",
		"http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg"
	};
}

設定好了圖片源之後,我們需要一個GridView來展示照片牆上的每一張圖片。開啟或修改activity_main.xml中的程式碼,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <GridView
        android:id="@+id/photo_wall"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:columnWidth="@dimen/image_thumbnail_size"
        android:gravity="center"
        android:horizontalSpacing="@dimen/image_thumbnail_spacing"
        android:numColumns="auto_fit"
        android:stretchMode="columnWidth"
        android:verticalSpacing="@dimen/image_thumbnail_spacing" >
    </GridView>

</LinearLayout>


很簡單,只是在LinearLayout中寫了一個GridView而已。接著我們要定義GridView中每一個子View的佈局,新建一個photo_layout.xml佈局,加入如下程式碼:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >

    <ImageView 
        android:id="@+id/photo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:scaleType="fitXY"
        />

</RelativeLayout>

仍然很簡單,photo_layout.xml佈局中只有一個ImageView控制元件,就是用它來顯示圖片的。這樣我們就把所有的佈局檔案都寫好了。

接下來新建PhotoWallAdapter做為GridView的介面卡,程式碼如下所示:

public class PhotoWallAdapter extends ArrayAdapter<String> {

	/**
	 * 記錄所有正在下載或等待下載的任務。
	 */
	private Set<BitmapWorkerTask> taskCollection;

	/**
	 * 圖片快取技術的核心類,用於快取所有下載好的圖片,在程式記憶體達到設定值時會將最少最近使用的圖片移除掉。
	 */
	private LruCache<String, Bitmap> mMemoryCache;

	/**
	 * 圖片硬碟快取核心類。
	 */
	private DiskLruCache mDiskLruCache;

	/**
	 * GridView的例項
	 */
	private GridView mPhotoWall;

	/**
	 * 記錄每個子項的高度。
	 */
	private int mItemHeight = 0;

	public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,
			GridView photoWall) {
		super(context, textViewResourceId, objects);
		mPhotoWall = photoWall;
		taskCollection = new HashSet<BitmapWorkerTask>();
		// 獲取應用程式最大可用記憶體
		int maxMemory = (int) Runtime.getRuntime().maxMemory();
		int cacheSize = maxMemory / 8;
		// 設定圖片快取大小為程式最大可用記憶體的1/8
		mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
			@Override
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getByteCount();
			}
		};
		try {
			// 獲取圖片快取路徑
			File cacheDir = getDiskCacheDir(context, "thumb");
			if (!cacheDir.exists()) {
				cacheDir.mkdirs();
			}
			// 建立DiskLruCache例項,初始化快取資料
			mDiskLruCache = DiskLruCache
					.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		final String url = getItem(position);
		View view;
		if (convertView == null) {
			view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);
		} else {
			view = convertView;
		}
		final ImageView imageView = (ImageView) view.findViewById(R.id.photo);
		if (imageView.getLayoutParams().height != mItemHeight) {
			imageView.getLayoutParams().height = mItemHeight;
		}
		// 給ImageView設定一個Tag,保證非同步載入圖片時不會亂序
		imageView.setTag(url);
		imageView.setImageResource(R.drawable.empty_photo);
		loadBitmaps(imageView, url);
		return view;
	}

	/**
	 * 將一張圖片儲存到LruCache中。
	 * 
	 * @param key
	 *            LruCache的鍵,這裡傳入圖片的URL地址。
	 * @param bitmap
	 *            LruCache的鍵,這裡傳入從網路上下載的Bitmap物件。
	 */
	public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
		if (getBitmapFromMemoryCache(key) == null) {
			mMemoryCache.put(key, bitmap);
		}
	}

	/**
	 * 從LruCache中獲取一張圖片,如果不存在就返回null。
	 * 
	 * @param key
	 *            LruCache的鍵,這裡傳入圖片的URL地址。
	 * @return 對應傳入鍵的Bitmap物件,或者null。
	 */
	public Bitmap getBitmapFromMemoryCache(String key) {
		return mMemoryCache.get(key);
	}

	/**
	 * 載入Bitmap物件。此方法會在LruCache中檢查所有螢幕中可見的ImageView的Bitmap物件,
	 * 如果發現任何一個ImageView的Bitmap物件不在快取中,就會開啟非同步執行緒去下載圖片。
	 */
	public void loadBitmaps(ImageView imageView, String imageUrl) {
		try {
			Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
			if (bitmap == null) {
				BitmapWorkerTask task = new BitmapWorkerTask();
				taskCollection.add(task);
				task.execute(imageUrl);
			} else {
				if (imageView != null && bitmap != null) {
					imageView.setImageBitmap(bitmap);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 取消所有正在下載或等待下載的任務。
	 */
	public void cancelAllTasks() {
		if (taskCollection != null) {
			for (BitmapWorkerTask task : taskCollection) {
				task.cancel(false);
			}
		}
	}

	/**
	 * 根據傳入的uniqueName獲取硬碟快取的路徑地址。
	 */
	public File getDiskCacheDir(Context context, String uniqueName) {
		String cachePath;
		if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
				|| !Environment.isExternalStorageRemovable()) {
			cachePath = context.getExternalCacheDir().getPath();
		} else {
			cachePath = context.getCacheDir().getPath();
		}
		return new File(cachePath + File.separator + uniqueName);
	}

	/**
	 * 獲取當前應用程式的版本號。
	 */
	public int getAppVersion(Context context) {
		try {
			PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),
					0);
			return info.versionCode;
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}
		return 1;
	}

	/**
	 * 設定item子項的高度。
	 */
	public void setItemHeight(int height) {
		if (height == mItemHeight) {
			return;
		}
		mItemHeight = height;
		notifyDataSetChanged();
	}

	/**
	 * 使用MD5演算法對傳入的key進行加密並返回。
	 */
	public String hashKeyForDisk(String key) {
		String cacheKey;
		try {
			final MessageDigest mDigest = MessageDigest.getInstance("MD5");
			mDigest.update(key.getBytes());
			cacheKey = bytesToHexString(mDigest.digest());
		} catch (NoSuchAlgorithmException e) {
			cacheKey = String.valueOf(key.hashCode());
		}
		return cacheKey;
	}
	
	/**
	 * 將快取記錄同步到journal檔案中。
	 */
	public void fluchCache() {
		if (mDiskLruCache != null) {
			try {
				mDiskLruCache.flush();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private String bytesToHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < bytes.length; i++) {
			String hex = Integer.toHexString(0xFF & bytes[i]);
			if (hex.length() == 1) {
				sb.append('0');
			}
			sb.append(hex);
		}
		return sb.toString();
	}

	/**
	 * 非同步下載圖片的任務。
	 * 
	 * @author guolin
	 */
	class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {

		/**
		 * 圖片的URL地址
		 */
		private String imageUrl;

		@Override
		protected Bitmap doInBackground(String... params) {
			imageUrl = params[0];
			FileDescriptor fileDescriptor = null;
			FileInputStream fileInputStream = null;
			Snapshot snapShot = null;
			try {
				// 生成圖片URL對應的key
				final String key = hashKeyForDisk(imageUrl);
				// 查詢key對應的快取
				snapShot = mDiskLruCache.get(key);
				if (snapShot == null) {
					// 如果沒有找到對應的快取,則準備從網路上請求資料,並寫入快取
					DiskLruCache.Editor editor = mDiskLruCache.edit(key);
					if (editor != null) {
						OutputStream outputStream = editor.newOutputStream(0);
						if (downloadUrlToStream(imageUrl, outputStream)) {
							editor.commit();
						} else {
							editor.abort();
						}
					}
					// 快取被寫入後,再次查詢key對應的快取
					snapShot = mDiskLruCache.get(key);
				}
				if (snapShot != null) {
					fileInputStream = (FileInputStream) snapShot.getInputStream(0);
					fileDescriptor = fileInputStream.getFD();
				}
				// 將快取資料解析成Bitmap物件
				Bitmap bitmap = null;
				if (fileDescriptor != null) {
					bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
				}
				if (bitmap != null) {
					// 將Bitmap物件新增到記憶體快取當中
					addBitmapToMemoryCache(params[0], bitmap);
				}
				return bitmap;
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				if (fileDescriptor == null && fileInputStream != null) {
					try {
						fileInputStream.close();
					} catch (IOException e) {
					}
				}
			}
			return null;
		}

		@Override
		protected void onPostExecute(Bitmap bitmap) {
			super.onPostExecute(bitmap);
			// 根據Tag找到相應的ImageView控制元件,將下載好的圖片顯示出來。
			ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
			if (imageView != null && bitmap != null) {
				imageView.setImageBitmap(bitmap);
			}
			taskCollection.remove(this);
		}

		/**
		 * 建立HTTP請求,並獲取Bitmap物件。
		 * 
		 * @param imageUrl
		 *            圖片的URL地址
		 * @return 解析後的Bitmap物件
		 */
		private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
			HttpURLConnection urlConnection = null;
			BufferedOutputStream out = null;
			BufferedInputStream in = null;
			try {
				final URL url = new URL(urlString);
				urlConnection = (HttpURLConnection) url.openConnection();
				in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
				out = new BufferedOutputStream(outputStream, 8 * 1024);
				int b;
				while ((b = in.read()) != -1) {
					out.write(b);
				}
				return true;
			} catch (final IOException e) {
				e.printStackTrace();
			} finally {
				if (urlConnection != null) {
					urlConnection.disconnect();
				}
				try {
					if (out != null) {
						out.close();
					}
					if (in != null) {
						in.close();
					}
				} catch (final IOException e) {
					e.printStackTrace();
				}
			}
			return false;
		}

	}

}

程式碼有點長,我們一點點進行分析。首先在PhotoWallAdapter的建構函式中,我們初始化了LruCache類,並設定了記憶體快取容量為程式最大可用記憶體的1/8,緊接著呼叫了DiskLruCache的open()方法來建立例項,並設定了硬碟快取容量為10M,這樣我們就把LruCache和DiskLruCache的初始化工作完成了。

接著在getView()方法中,我們為每個ImageView設定了一個唯一的Tag,這個Tag的作用是為了後面能夠準確地找回這個ImageView,不然非同步載入圖片會出現亂序的情況。然後在getView()方法的最後呼叫了loadBitmaps()方法,載入圖片的具體邏輯也就是在這裡執行的了。

進入到loadBitmaps()方法中可以看到,實現是呼叫了getBitmapFromMemoryCache()方法來從記憶體中獲取快取,如果獲取到了則直接呼叫ImageView的setImageBitmap()方法將圖片顯示到介面上。如果記憶體中沒有獲取到,則開啟一個BitmapWorkerTask任務來去非同步載入圖片。

那麼在BitmapWorkerTask的doInBackground()方法中,我們就靈活運用了上篇文章中學習的DiskLruCache的各種用法。首先根據圖片的URL生成對應的MD5 key,然後呼叫DiskLruCache的get()方法來獲取硬碟快取,如果沒有獲取到的話則從網路上請求圖片並寫入硬碟快取,接著將Bitmap物件解析出來並新增到記憶體快取當中,最後將這個Bitmap物件顯示到介面上,這樣一個完整的流程就執行完了。

那麼我們再來分析一下上述流程,每次載入圖片的時候都優先去記憶體快取當中讀取,當讀取不到的時候則回去硬碟快取中讀取,而如果硬碟快取仍然讀取不到的話,就從網路上請求原始資料。不管是從硬碟快取還是從網路獲取,讀取到了資料之後都應該新增到記憶體快取當中,這樣的話我們下次再去讀取圖片的時候就能迅速從記憶體當中讀取到,而如果該圖片從記憶體中被移除了的話,那就重複再執行一遍上述流程就可以了。

這樣我們就把LruCache和DiskLruCache完美結合到一起了。接下來還需要編寫MainActivity的程式碼,非常簡單,如下所示:

public class MainActivity extends Activity {

	/**
	 * 用於展示照片牆的GridView
	 */
	private GridView mPhotoWall;

	/**
	 * GridView的介面卡
	 */
	private PhotoWallAdapter mAdapter;

	private int mImageThumbSize;
	private int mImageThumbSpacing;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mImageThumbSize = getResources().getDimensionPixelSize(
				R.dimen.image_thumbnail_size);
		mImageThumbSpacing = getResources().getDimensionPixelSize(
				R.dimen.image_thumbnail_spacing);
		mPhotoWall = (GridView) findViewById(R.id.photo_wall);
		mAdapter = new PhotoWallAdapter(this, 0, Images.imageThumbUrls,
				mPhotoWall);
		mPhotoWall.setAdapter(mAdapter);
		mPhotoWall.getViewTreeObserver().addOnGlobalLayoutListener(
				new ViewTreeObserver.OnGlobalLayoutListener() {
					
					@Override
					public void onGlobalLayout() {
						final int numColumns = (int) Math.floor(mPhotoWall
								.getWidth()
								/ (mImageThumbSize + mImageThumbSpacing));
						if (numColumns > 0) {
							int columnWidth = (mPhotoWall.getWidth() / numColumns)
									- mImageThumbSpacing;
							mAdapter.setItemHeight(columnWidth);
							mPhotoWall.getViewTreeObserver()
									.removeGlobalOnLayoutListener(this);
						}
					}
				});
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		mAdapter.fluchCache();
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		// 退出程式時結束所有的下載任務
		mAdapter.cancelAllTasks();
	}

}

上述程式碼中,我們通過getViewTreeObserver()的方式監聽View的佈局事件,當佈局完成以後,我們重新修改一下GridView中子View的高度,以保證子View的寬度和高度可以保持一致。

到這裡還沒有結束,最後還需要配置一下AndroidManifest.xml檔案,並加入相應的許可權,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.photoswalldemo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.photoswalldemo.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


好了,全部程式碼都在這兒了,讓我們來執行一下吧,效果如下圖所示:

第一次從網路上請求圖片的時候有點慢,但之後載入圖片就會非常快了,滑動起來也很流暢。

那麼我們最後再檢查一下這些圖片是不是已經正確快取在指定地址了,進入 /sdcard/Android/data/<application package>/cache/thumb 這個路徑,如下圖所示:

可以看到,每張圖片的快取以及journal檔案都在這裡了,說明我們的硬碟快取已經成功了。

第二部分,LruCache和DiskLruCache的原理實際上是LinkedHashMap和File,

下面就來看看使用LinkedHashMap和File的實現:

網上關於這個方面的文章也不少,基本的思路是執行緒+快取來解決。下面提出一些優化:

1、採用執行緒池

2、記憶體快取+檔案快取

3、記憶體快取中網上很多是採用SoftReference來防止堆溢位,這兒嚴格限制只能使用最大JVM記憶體的1/4

4、對下載的圖片進行按比例縮放,以減少記憶體的消耗

具體的程式碼裡面說明。先放上記憶體快取類的程式碼MemoryCache.java:

public class MemoryCache {

	private static final String TAG = "MemoryCache";
	// 放入快取時是個同步操作
	// LinkedHashMap構造方法的最後一個引數true代表這個map裡的元素將按照最近使用次數由少到多排列,即LRU
	// 這樣的好處是如果要將快取中的元素替換,則先遍歷出最近最少使用的元素來替換以提高效率
	private Map<String, Bitmap> cache = Collections
			.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
	// 快取中圖片所佔用的位元組,初始0,將通過此變數嚴格控制快取所佔用的堆記憶體
	private long size = 0;// current allocated size
	// 快取只能佔用的最大堆記憶體
	private long limit = 1000000;// max memory in bytes

	public MemoryCache() {
		// use 25% of available heap size
		setLimit(Runtime.getRuntime().maxMemory() / 4);
	}

	public void setLimit(long new_limit) { 
		limit = new_limit;
		Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");
	}

	public Bitmap get(String id) {
		try {
			if (!cache.containsKey(id))
				return null;
			return cache.get(id);
		} catch (NullPointerException ex) {
			return null;
		}
	}

	public void put(String id, Bitmap bitmap) {
		try {
			if (cache.containsKey(id))
				size -= getSizeInBytes(cache.get(id));
			cache.put(id, bitmap);
			size += getSizeInBytes(bitmap);
			checkSize();
		} catch (Throwable th) {
			th.printStackTrace();
		}
	}

	/**
	 * 嚴格控制堆記憶體,如果超過將首先替換最近最少使用的那個圖片快取
	 * 
	 */
	private void checkSize() {
		Log.i(TAG, "cache size=" + 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, "Clean cache. New size " + cache.size());
		}
	}

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

	/**
	 * 圖片佔用的記憶體
	 * 
	 * @param bitmap
	 * @return
	 */
	long getSizeInBytes(Bitmap bitmap) {
		if (bitmap == null)
			return 0;
		return bitmap.getRowBytes() * bitmap.getHeight();
	}
}

也可以使用SoftReference,程式碼會簡單很多,但是推薦上面的方法。因為上面的方法更有效率,而SoftReference要依靠系統回收,並且對檔案大小沒有節制。

public class MemoryCache {
	
	private Map<String, SoftReference<Bitmap>> cache = Collections
			.synchronizedMap(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();
	}

}


下面是檔案快取類的程式碼FileCache.java:下面是檔案快取類的程式碼FileCache.java:

public class FileCache {

	private File cacheDir;

	public FileCache(Context context) {
		// 如果有SD卡則在SD卡中建一個LazyList的目錄存放快取的圖片
		// 沒有SD卡就放在系統的快取目錄中
		if (android.os.Environment.getExternalStorageState().equals(
				android.os.Environment.MEDIA_MOUNTED))
			cacheDir = new File(
					android.os.Environment.getExternalStorageDirectory(),
					"LazyList");
		else
			cacheDir = context.getCacheDir();
		if (!cacheDir.exists())
			cacheDir.mkdirs();
	}

	public File getFile(String url) {
		// 將url的hashCode作為快取的檔名
		String filename = String.valueOf(url.hashCode());
		// Another possible solution
		// String filename = URLEncoder.encode(url);
		File f = new File(cacheDir, filename);
		return f;

	}

	public void clear() {
		File[] files = cacheDir.listFiles();
		if (files == null)
			return;
		for (File f : files)
			f.delete();
	}

}
最後最重要的載入圖片的類,ImageLoader.java:
public class ImageLoader {

	MemoryCache memoryCache = new MemoryCache();
	FileCache fileCache;
	private Map<ImageView, String> imageViews = Collections
			.synchronizedMap(new WeakHashMap<ImageView, String>());
	// 執行緒池
	ExecutorService executorService;

	public ImageLoader(Context context) {
		fileCache = new FileCache(context);
		executorService = Executors.newFixedThreadPool(5);
	}

	// 當進入listview時預設的圖片,可換成你自己的預設圖片
	final int stub_id = R.drawable.stub;

	// 最主要的方法
	public void DisplayImage(String url, ImageView imageView) {
		imageViews.put(imageView, url);
		// 先從記憶體快取中查詢

		Bitmap bitmap = memoryCache.get(url);
		if (bitmap != null)
			imageView.setImageBitmap(bitmap);
		else {
			// 若沒有的話則開啟新執行緒載入圖片
			queuePhoto(url, imageView);
			imageView.setImageResource(stub_id);
		}
	}

	private void queuePhoto(String url, ImageView imageView) {
		PhotoToLoad p = new PhotoToLoad(url, imageView);
		executorService.submit(new PhotosLoader(p));
	}

	private Bitmap getBitmap(String url) {
		File f = fileCache.getFile(url);

		// 先從檔案快取中查詢是否有
		Bitmap b = decodeFile(f);
		if (b != null)
			return b;

		// 最後從指定的url中下載圖片
		try {
			Bitmap bitmap = null;
			URL imageUrl = new URL(url);
			HttpURLConnection conn = (HttpURLConnection) imageUrl
					.openConnection();
			conn.setConnectTimeout(30000);
			conn.setReadTimeout(30000);
			conn.setInstanceFollowRedirects(true);
			InputStream is = conn.getInputStream();
			OutputStream os = new FileOutputStream(f);
			CopyStream(is, os);
			os.close();
			bitmap = decodeFile(f);
			return bitmap;
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
	}

	// decode這個圖片並且按比例縮放以減少記憶體消耗,虛擬機器對每張圖片的快取大小也是有限制的
	private Bitmap decodeFile(File f) {
		try {
			// decode image size
			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inJustDecodeBounds = true;
			BitmapFactory.decodeStream(new FileInputStream(f), null, o);

			// Find the correct scale value. It should be the power of 2.
			final int REQUIRED_SIZE = 70;
			int width_tmp = o.outWidth, height_tmp = o.outHeight;
			int scale = 1;
			while (true) {
				if (width_tmp / 2 < REQUIRED_SIZE
						|| height_tmp / 2 < REQUIRED_SIZE)
					break;
				width_tmp /= 2;
				height_tmp /= 2;
				scale *= 2;
			}

			// decode with inSampleSize
			BitmapFactory.Options o2 = new BitmapFactory.Options();
			o2.inSampleSize = scale;
			return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
		} catch (FileNotFoundException e) {
		}
		return null;
	}

	// Task for the queue
	private class PhotoToLoad {
		public String url;
		public ImageView imageView;

		public PhotoToLoad(String u, ImageView i) {
			url = u;
			imageView = i;
		}
	}

	class PhotosLoader implements Runnable {
		PhotoToLoad photoToLoad;

		PhotosLoader(PhotoToLoad photoToLoad) {
			this.photoToLoad = photoToLoad;
		}

		@Override
		public void run() {
			if (imageViewReused(photoToLoad))
				return;
			Bitmap bmp = getBitmap(photoToLoad.url);
			memoryCache.put(photoToLoad.url, bmp);
			if (imageViewReused(photoToLoad))
				return;
			BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
			// 更新的操作放在UI執行緒中
			Activity a = (Activity) photoToLoad.imageView.getContext();
			a.runOnUiThread(bd);
		}
	}

	/**
	 * 防止圖片錯位
	 * 
	 * @param photoToLoad
	 * @return
	 */
	boolean imageViewReused(PhotoToLoad photoToLoad) {
		String tag = imageViews.get(photoToLoad.imageView);
		if (tag == null || !tag.equals(photoToLoad.url))
			return true;
		return false;
	}

	// 用於在UI執行緒中更新介面
	class BitmapDisplayer implements Runnable {
		Bitmap bitmap;
		PhotoToLoad photoToLoad;

		public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
			bitmap = b;
			photoToLoad = p;
		}

		public void run() {
			if (imageViewReused(photoToLoad))
				return;
			if (bitmap != null)
				photoToLoad.imageView.setImageBitmap(bitmap);
			else
				photoToLoad.imageView.setImageResource(stub_id);
		}
	}

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

	public static void CopyStream(InputStream is, OutputStream os) {
		final int buffer_size = 1024;
		try {
			byte[] bytes = new byte[buffer_size];
			for (;;) {
				int count = is.read(bytes, 0, buffer_size);
				if (count == -1)
					break;
				os.write(bytes, 0, count);
			}
		} catch (Exception ex) {
		}
	}
}

主要流程是先從記憶體快取中查詢,若沒有再開執行緒,從檔案快取中查詢都沒有則從指定的url中查詢,並對bitmap進行處理,最後通過下面方法對UI進行更新操作:

a.runOnUiThread(...);


在你的程式中的基本用法:

ImageLoader imageLoader=new ImageLoader(context);
...
imageLoader.DisplayImage(url, imageView);
比如你的放在你的ListView的adapter的getView()方法中,當然也適用於GridView。