1. 程式人生 > >Android中圖片的三級緩存策略

Android中圖片的三級緩存策略

getitem 圖片顯示 current sco clas 實例 ase activit fileinput

一、簡單介紹

如今的Android應用程序中。不可避免的都會使用到圖片。假設每次載入圖片的時候都要從網絡又一次拉取,這樣不但非常耗費用戶的流量。並且圖片載入的也會非常慢,用戶體驗非常不好。

所以一個應用的圖片緩存策略是非常重要的。

通常情況下。Android應用程序中圖片的緩存策略採用“內存-本地-網絡”三級緩存策略,首先應用程序訪問網絡拉取圖片,分別將載入的圖片保存在本地SD卡中和內存中。當程序再一次須要載入圖片的時候,先推斷內存中是否有緩存。有則直接從內存中拉取,否則查看本地SD卡中是否有緩存。SD卡中假設存在緩存,則圖片從SD卡中拉取,否則從網絡載入圖片。依據這三級緩存機制。能夠讓我們的應用程序在載入圖片的時候做到遊刃有余,有效的避免內存溢出。

PS:當然如今處理網絡圖片的時候,一般人都會選擇XUtils中的BitmapUtil,它已經將網絡緩存處理的相當好了,使用起來非常方便--本人就一直在用。

仿照BitMapUtil的實現思路,定制一個自己的圖片載入工具,來理解一下三級緩存的策略,希望對自己會有一個提升。

二、網絡緩存

網絡拉取圖片嚴格來講不能稱之為緩存,實質上就是下載url相應的圖片。我們這裏姑且把它看作是緩存的一種。仿照BitmapUtil中的display方法,我自己定制的CustomBitmapUtils也定義這種方法,依據傳入的url,將圖片設置到ivPic控件上。

public void display(ImageView ivPic, String url) {

}
定義網絡緩存的工具類,在訪問網絡的時候,我使用了AsyncTask來實現。在AsyncTask的doInBackGround方法裏下載圖片。然後將 圖片設置給ivPic控件,AsyncTask有三個泛型,其中第一個泛型是運行異步任務的時候,通過execute傳過來的參數。第二個泛型是更新的進度。第三個泛型是異步任務運行完畢之後,返回來的結果,我們這裏返回一個Bitmap。詳細的下載實現代碼例如以下:

<pre name="code" class="java">/**
 * 網絡緩存的工具類
 * 
 * @author ZHY
 * 
 */
public class NetCacheUtils {

	private LocalCacheUtils localCacheUtils;
	private MemoryCacheUtils memoryCacheUtils;

	public NetCacheUtils() {
		localCacheUtils = new LocalCacheUtils();
		memoryCacheUtils = new MemoryCacheUtils();
	}

	/**
	 * 從網絡下載圖片
	 * 
	 * @param ivPic
	 * @param url
	 */
	public void getBitmapFromNet(ImageView ivPic, String url) {
		// 訪問網絡的操作一定要在子線程中進行,採用異步任務實現
		MyAsyncTask task = new MyAsyncTask();
		task.execute(ivPic, url);

	}

	/**
	 * 第一個泛型--異步任務運行的時候。通過execute傳過來的參數; 第二個泛型--更新進度。 第三個泛型--異步任務運行以後返回的結果
	 * 
	 * @author ZHY
	 * 
	 */
	private class MyAsyncTask extends AsyncTask<Object, Void, Bitmap> {

		private ImageView ivPic;
		private String url;

		// 耗時任務運行之前 --主線程
		@Override
		protected void onPreExecute() {
			super.onPreExecute();
		}

		// 後臺運行的任務
		@Override
		protected Bitmap doInBackground(Object... params) {
			// 運行異步任務的時候,將URL傳過來
			ivPic = (ImageView) params[0];
			url = (String) params[1];
			Bitmap bitmap = downloadBitmap(url);
			// 為了保證ImageView控件和URL一一相應。給ImageView設定一個標記
			ivPic.setTag(url);// 關聯ivPic和URL

			return bitmap;
		}

		// 更新進度 --主線程
		@Override
		protected void onProgressUpdate(Void... values) {
			super.onProgressUpdate(values);
		}

		// 耗時任務運行之後--主線程
		@Override
		protected void onPostExecute(Bitmap result) {
			String mCurrentUrl = (String) ivPic.getTag();
			if (url.equals(mCurrentUrl)) {
				ivPic.setImageBitmap(result);
				System.out.println("從網絡獲取圖片");
				// 從網絡載入完之後。將圖片保存到本地SD卡一份,保存到內存中一份
				localCacheUtils.setBitmap2Local(url, result);
				// 從網絡載入完之後。將圖片保存到本地SD卡一份,保存到內存中一份
				memoryCacheUtils.setBitmap2Memory(url, result);

			}
		}
	}

	/**
	 * 下載網絡圖片
	 * 
	 * @param url
	 * @return
	 */
	private Bitmap downloadBitmap(String url) {
		HttpURLConnection conn = null;
		try {
			URL mURL = new URL(url);
			// 打開HttpURLConnection連接
			conn = (HttpURLConnection) mURL.openConnection();
			// 設置參數
			conn.setConnectTimeout(5000);
			conn.setReadTimeout(5000);
			conn.setRequestMethod("GET");
			// 開啟連接
			conn.connect();

			// 獲得響應碼
			int code = conn.getResponseCode();
			if (code == 200) {
				// 相應成功,獲得網絡返回來的輸入流
				InputStream is = conn.getInputStream();

				// 圖片的輸入流獲取成功之後,設置圖片的壓縮參數,將圖片進行壓縮
				BitmapFactory.Options options = new BitmapFactory.Options();
				options.inSampleSize = 2;// 將圖片的寬高都壓縮為原來的一半,在開發中此參數須要依據圖片展示的大小來確定,否則可能展示的不正常
				options.inPreferredConfig = Bitmap.Config.RGB_565;// 這個壓縮的最小

				// Bitmap bitmap = BitmapFactory.decodeStream(is);
				Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);// 經過壓縮的圖片

				return bitmap;
			}

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 斷開連接
			conn.disconnect();
		}

		return null;
	}
}
</pre><p></p><pre>

三、本地緩存

從網絡載入完圖片之後,將圖片保存到本地SD卡中。在載入圖片的時候。推斷一下SD卡中是否有圖片緩存,假設有。就直接從SD卡載入圖片。本地緩存的工具類中有兩個公共的方法。各自是向本地SD卡設置網絡圖片,獲取SD卡中的圖片。

設置圖片的時候採用鍵值對的形式進行存儲,將圖片的url作為鍵,作為文件的名字。圖片的Bitmap作位值來保存。

由於url含有特殊字符,不能直接作為圖片的名字來存儲。故採用url的MD5值作為文件的名字。

/**
 * 本地緩存
 * 
 * @author ZHY
 * 
 */
public class LocalCacheUtils {
	/**
	 * 文件保存的路徑
	 */
	public static final String FILE_PATH = Environment
			.getExternalStorageDirectory().getAbsolutePath() + "/cache/pics";

	/**
	 * 從本地SD卡獲取網絡圖片,key是url的MD5值
	 * 
	 * @param url
	 * @return
	 */
	public Bitmap getBitmapFromLocal(String url) {
		try {
			String fileName = MD5Encoder.encode(url);
			File file = new File(FILE_PATH, fileName);
			if (file.exists()) {
				Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(
						file));
				return bitmap;
			}

		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;

	}

	/**
	 * 向本地SD卡寫網絡圖片
	 * 
	 * @param url
	 * @param bitmap
	 */
	public void setBitmap2Local(String url, Bitmap bitmap) {
		try {
			// 文件的名字
			String fileName = MD5Encoder.encode(url);
			// 創建文件流,指向該路徑。文件名稱叫做fileName
			File file = new File(FILE_PATH, fileName);
			// file事實上是圖片,它的父級File是目錄,推斷一下目錄是否存在,假設不存在,創建目錄
			File fileParent = file.getParentFile();
			if (!fileParent.exists()) {
				// 目錄不存在
				fileParent.mkdirs();// 創建目錄
			}
			// 將圖片保存到本地
			bitmap.compress(CompressFormat.JPEG, 100,
					new FileOutputStream(file));

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

四、內存緩存

內存緩存說白了就是在內存中保存一份圖片集合,首先會想到HashMap這樣的鍵值對的形式來進行保存。以url作為key,bitmap作為value。可是在Java中這樣的默認的new對象的方式是強引用。JVM在進行垃圾回收的時候是不會回收強引用的,所以假設載入的圖片過多的話,map會越來越大,非常easy出現OOM異常。在Android2.3之前,還能夠通過軟引用或者弱引用來解決。可是Android2.3之後,Google官方便不再推薦軟引用了,Google推薦我們使用LruCache。

在過去,我們常常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。

可是如今已經不再推薦使用這樣的方式了。由於從 Android 2.3 (API Level 9)開始。垃圾回收器會更傾向於回收持有軟引用或弱引用的對象。這讓軟引用和弱引用變得不再可靠。

另外,Android 3.0 (API Level 11)中。圖片的數據會存儲在本地的內存其中,因而無法用一種可預見的方式將其釋放。這就有潛在的風險造成應用程序的內存溢出並崩潰。

為了能夠選擇一個合適的緩存大小給LruCache, 有下面多個因素應該放入考慮範圍內。比如:

  • 你的設備能夠為每一個應用程序分配多大的內存?Android默認是16M。

  • 設備屏幕上一次最多能顯示多少張圖片?有多少圖片須要進行預載入,由於有可能非常快也會顯示在屏幕上?
  • 你的設備的屏幕大小和分辨率各自是多少?一個超高分辨率的設備(比如 Galaxy Nexus) 比起一個較低分辨率的設備(比如 Nexus S)。在持有同樣數量圖片的時候,須要更大的緩存空間。
  • 圖片的尺寸和大小。還有每張圖片會占領多少內存空間。
  • 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其他圖片要高?假設有的話,你或許應該讓一些圖片常駐在內存其中。或者使用多個LruCache 對象來區分不同組的圖片。

  • 你能維持好數量和質量之間的平衡嗎?有些時候。存儲多個低像素的圖片。而在後臺去開線程載入高像素的圖片會更加的有效。
以上是Google對LruCache的描寫敘述,事實上LruCache的使用非常簡單,跟Map非常相近,僅僅是在創建LruCache對象的時候須要指定它的最大同意內存,一般設置為當前應用程序的最大運行內存的八分之中的一個就可以。

/**
 * 內存緩存
 * 
 * @author ZHY
 * 
 */
public class MemoryCacheUtils {
	/*
	 * 由於map默認是強引用,全部在JVM進行垃圾回收的時候不會回收map的引用
	 */
	// private HashMap<String, Bitmap> map = new HashMap<String, Bitmap>();
	// 軟引用的實例,在內存不夠時。垃圾回收器會優先考慮回收
	// private HashMap<String, SoftReference<Bitmap>> mSoftReferenceMap = new
	// HashMap<String, SoftReference<Bitmap>>();
	// LruCache
	private LruCache<String, Bitmap> lruCache;

	public MemoryCacheUtils() {
		// lruCache最大同意內存一般為Android系統分給每一個應用程序內存大小(默認Android系統給每一個應用程序分配16兆內存)的八分之中的一個(推薦)
		// 獲得當前應用程序運行的內存大小
		long mCurrentMemory = Runtime.getRuntime().maxMemory();
		int maxSize = (int) (mCurrentMemory / 8);
		// 給LruCache設置最大的內存
		lruCache = new LruCache<String, Bitmap>(maxSize) {
			@Override
			protected int sizeOf(String key, Bitmap value) {
				// 獲取每張圖片所占內存的大小
				// 計算方法是:圖片顯示的寬度的像素點乘以高度的像素點
				int byteCount = value.getRowBytes() * value.getHeight();// 獲取圖片占用內存大小
				return byteCount;
			}
		};
	}

	/**
	 * 從內存中讀取Bitmap
	 * 
	 * @param url
	 * @return
	 */
	public Bitmap getBitmapFromMemory(String url) {

		// Bitmap bitmap = map.get(url);
		// SoftReference<Bitmap> softReference = mSoftReferenceMap.get(url);
		// Bitmap bitmap = softReference.get();
		// 軟引用在Android2.3以後就不推薦使用了,Google推薦使用lruCache
		// LRU--least recently use
		// 近期最少使用,將內存控制在一定的大小內。超過這個內存大小,就會優先釋放近期最少使用的那些東東
		Bitmap bitmap = lruCache.get(url);
		return bitmap;

	}

	/**
	 * 將圖片保存到內存中
	 * 
	 * @param url
	 * @param bitmap
	 */
	public void setBitmap2Memory(String url, Bitmap bitmap) {
		// 向內存中設置,key,value的形式,首先想到HashMap
		// map.put(url, bitmap);
		// 保存軟引用到map中
		// SoftReference<Bitmap> mSoftReference = new
		// SoftReference<Bitmap>(bitmap);
		// mSoftReferenceMap.put(url, mSoftReference);
		lruCache.put(url, bitmap);
	}

}
好了。如今三級緩存策略封裝完畢。接下來定制我們自己的BitmapUtils

/**
 * 自己定義的載入圖片的工具類,相似於Xutils中的BitmapUtil,在實際使用中,一般使用BitmapUtil。為了理解三級緩存。
 * 這裏模擬BitmapUtil自己定義了CustomBitmapUtil
 * 
 * @author ZHY
 * 
 */
public class CustomBitmapUtils {

	private Bitmap bitmap;

	private NetCacheUtils netCacheUtils;
	private LocalCacheUtils localCacheUtils;
	private MemoryCacheUtils memoryCacheUtils;

	public CustomBitmapUtils() {
		netCacheUtils = new NetCacheUtils();
		localCacheUtils = new LocalCacheUtils();
		memoryCacheUtils = new MemoryCacheUtils();
	}

	/**
	 * 載入圖片,將當前URL相應的圖片顯示到ivPic的控件上
	 * 
	 * @param ivPic
	 *            ImageView控件
	 * @param url
	 *            圖片的地址
	 */
	public void display(ImageView ivPic, String url) {
		// 設置默認顯示的圖片
		ivPic.setImageResource(R.drawable.ic_launcher);

		// 1、內存緩存
		bitmap = memoryCacheUtils.getBitmapFromMemory(url);
		if (bitmap != null) {
			ivPic.setImageBitmap(bitmap);
			System.out.println("從內存緩存中載入圖片");
			return;
		}
		// 2、本地磁盤緩存
		bitmap = localCacheUtils.getBitmapFromLocal(url);
		if (bitmap != null) {
			ivPic.setImageBitmap(bitmap);
			System.out.println("從本地SD卡載入的圖片");
			memoryCacheUtils.setBitmap2Memory(url, bitmap);// 將圖片保存到內存
			return;
		}
		// 3、網絡緩存
		netCacheUtils.getBitmapFromNet(ivPic, url);
		/*
		 * 從網絡獲取圖片之後。將圖片保存到手機SD卡中,在進行圖片展示的時候,優先從SD卡中讀取緩存,key是圖片的URL的MD5值。
		 * value是保存的圖片bitmap
		 */
	}

}
在mainActivity中使用ListView載入網絡圖片

/**
 * Android中三級緩存--網絡緩存-本地緩存-內存緩存
 * 
 * @author ZHY
 * 
 */
public class MainActivity extends Activity {

	private ListView list;
	private Button btn;
	private CustomBitmapUtils utils;

	private static final String BASE_URL = "http://192.168.0.148:8080/pics";
	// 初始化一些網絡圖片
	String[] urls = { BASE_URL + "/1.jpg", BASE_URL + "/2.jpg",
			BASE_URL + "/3.jpg", BASE_URL + "/4.jpg", BASE_URL + "/5.jpg",
			BASE_URL + "/6.jpg", BASE_URL + "/7.jpg", BASE_URL + "/8.jpg",
			BASE_URL + "/9.jpg", BASE_URL + "/10.jpg", BASE_URL + "/11.jpg",
			BASE_URL + "/12.jpg", BASE_URL + "/13.jpg", BASE_URL + "/14.jpg",
			BASE_URL + "/15.jpg", BASE_URL + "/16.jpg", BASE_URL + "/17.jpg",
			BASE_URL + "/18.jpg", BASE_URL + "/19.jpg", BASE_URL + "/20.jpg",
			BASE_URL + "/21.jpg", BASE_URL + "/22.jpg", BASE_URL + "/23.jpg",
			BASE_URL + "/24.jpg", BASE_URL + "/25.jpg", BASE_URL + "/26.jpg",
			BASE_URL + "/27.jpg", BASE_URL + "/28.jpg", BASE_URL + "/29.jpg",
			BASE_URL + "/30.jpg" };

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		list = (ListView) findViewById(R.id.list);
		btn = (Button) findViewById(R.id.btn_load);
		utils = new CustomBitmapUtils();

		// 載入網絡圖片
		btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				MyAdapter adapter = new MyAdapter();
				list.setAdapter(adapter);
			}
		});
	}

	class MyAdapter extends BaseAdapter {

		@Override
		public int getCount() {
			return urls.length;
		}

		@Override
		public String getItem(int position) {
			return urls[position];
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder;
			if (convertView == null) {
				convertView = View.inflate(MainActivity.this,
						R.layout.item_list, null);
				holder = new ViewHolder();
				holder.ivPic = (ImageView) convertView.findViewById(R.id.iv);
				convertView.setTag(holder);
			} else {
				holder = (ViewHolder) convertView.getTag();
			}
			utils.display(holder.ivPic, urls[position]);
			return convertView;
		}

		class ViewHolder {
			ImageView ivPic;
		}
	}
}
運行的結果例如以下:

程序第一次運行。日誌打印例如以下

技術分享圖片

之後將圖片緩存在SD卡中,從本地載入圖片

技術分享圖片

然後將圖片緩存到內存,從內存載入圖片

技術分享圖片

OK,到眼下為止。Android中圖片的三級緩存原理就都介紹完了,我自己本人受益匪淺,希望能夠幫助到須要的朋友。須要源代碼的請點擊例如以下鏈接進行下載。

源代碼下載


Android中圖片的三級緩存策略