1. 程式人生 > >Android快取機制詳解之硬碟快取DiskLruCache

Android快取機制詳解之硬碟快取DiskLruCache

簡介

防止多圖OOM的核心解決思路就是使用LruCache技術。但LruCache只是管理了記憶體中圖片的儲存與釋放,如果圖片從記憶體中被移除的話,那麼又需要從網路上重新載入一次圖片,這顯然非常耗時。對此,Google又提供了一套硬碟快取的解決方案:DiskLruCache(非Google官方編寫,但獲得官方認證),我們先來看一下有哪些應用程式已經使用了DiskLruCache技術,在我所接觸的應用範圍裡,Dropbox、Twitter、網易新聞等都是使用DiskLruCache來進行硬碟快取的。如果你手機上安裝了網頁新聞這個APP,當你開啟它的Cache目錄時,你會發現一個名叫journal的檔案,這個檔案通常是使DiskLruCache技術的標誌。

下載

可以點選這裡下載DiskLruCache的原始碼。下載好了原始碼之後,只需要在專案中新建一個libcore.io包,然後將DiskLruCache.java檔案複製到這個包中即可。

常用的快取位置

1.有SDCard:/sdcard/Android/data/<application package>/cache

cachePath = context.getExternalCacheDir().getPath();

2.沒有SDCard:/data/data/<applicationpackage>/cache

cachePath = context.getCacheDir().getPath();

常用方法

1. 開啟快取

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 

2.寫入快取

public Editor edit(String key) throws IOException

3.讀取快取:

public synchronized Snapshot get(String key) throws IOException  
InputStream is = snapShot.getInputStream(0);

4. 其它API:

1. size(): 這個方法會返回當前快取路徑下所有快取資料的總位元組數,以byte為單位
2. flush(): 這個方法用於將記憶體中的操作記錄同步到日誌檔案(也就是journal檔案)當中,標準的做法就是在Activity的onPause()方法中去呼叫一次flush()方法
3. close(): 這個方法用於將DiskLruCache關閉掉,通常只應該在Activity的onDestroy()方法中去呼叫close()方法
4. delete():這個方法用於將所有的快取資料全部刪除

解讀journal

1. journal檔案頭  格式
2. dirty -->呼叫一次DiskLruCache的edit()方法,都會向journal檔案中寫入一條DIRTY記錄
3. clean -->呼叫一次DiskLruCache的commit()方法,表示寫入快取成功。後面會跟檔案的大小
4. remove-->呼叫abort()方法表示寫入快取失敗
5. read  -->呼叫get()方法去讀取一條快取資料時會呼叫

/**
 * GridView的介面卡,負責非同步從網路上下載圖片展示在照片牆上。
 */
public class PhotoWallAdapter extends ArrayAdapter<String> {
	private Set<BitmapWorkerTask> taskCollection;
	/**
	 * 圖片快取技術的核心類,用於快取所有下載好的圖片,在程式記憶體達到設定值時會將最少最近使用的圖片移除掉。
	 */
	private LruCache<String, Bitmap> mMemoryCache;
	private DiskLruCache mDiskLruCache;
	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>();
		/**
		 * LruCache的使用
		 */
		int maxMemory = (int) Runtime.getRuntime().maxMemory();
		int cacheSize = maxMemory / 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();
	}

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

		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;
		}
	}
}

詳細步驟:

1.在Adapter的getView方法中獲取圖片的url
2.建立一個HashSet把所有的非同步任務放入集合中
3.開啟非同步任務,傳入url,執行非同步任務。
4.使用MD5加密演算法生成URL對應的key,呼叫DiskLruCache.get(key)方法查詢對應的快取,返回Snapshot物件
5.如果Snapshot物件為null,呼叫mDiskLruCache.edit(key).editor.newOutputStream(0)建立容器,下載圖片放入容器,成功呼叫commit(),失敗呼叫abort()
6.如果Snapshot物件不為null,將快取的資料解析成Bitmap,然後將Bitmap新增到快取中,LruCache<String, Bitmap> mMemoryCache.put(key,value)
7.根據tag獲取imageview,呼叫imageView.setImageBitmap(bitmap),ok

下載Demo請猛戳