標籤:
一、簡介
現在的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;// 耗時任務執行之前 --主線程@Overrideprotected void onPreExecute() {super.onPreExecute();}// 後台執行的任務@Overrideprotected Bitmap doInBackground(Object... params) {// 執行非同步任務的時候,將URL傳過來ivPic = (ImageView) params[0];url = (String) params[1];Bitmap bitmap = downloadBitmap(url);// 為了保證ImageView控制項和URL一一對應,給ImageView設定一個標記ivPic.setTag(url);// 關聯ivPic和URLreturn bitmap;}// 更新進度 --主線程@Overrideprotected void onProgressUpdate(Void... values) {super.onProgressUpdate(values);}// 耗時任務執行之後--主線程@Overrideprotected 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);// 建立檔案流,指向該路徑,檔案名稱叫做fileNameFile 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會越來越大,很容易出現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>>();// LruCacheprivate 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) {@Overrideprotected 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" };@Overrideprotected 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() {@Overridepublic void onClick(View v) {MyAdapter adapter = new MyAdapter();list.setAdapter(adapter);}});}class MyAdapter extends BaseAdapter {@Overridepublic int getCount() {return urls.length;}@Overridepublic String getItem(int position) {return urls[position];}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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中圖片的三級緩衝策略