Android中圖片的三級緩存策略
一、簡單介紹
如今的Android應用程序中。不可避免的都會使用到圖片。假設每次載入圖片的時候都要從網絡又一次拉取,這樣不但非常耗費用戶的流量。並且圖片載入的也會非常慢,用戶體驗非常不好。
所以一個應用的圖片緩存策略是非常重要的。
通常情況下。Android應用程序中圖片的緩存策略採用“內存-本地-網絡”三級緩存策略,首先應用程序訪問網絡拉取圖片,分別將載入的圖片保存在本地SD卡中和內存中。當程序再一次須要載入圖片的時候,先推斷內存中是否有緩存。有則直接從內存中拉取,否則查看本地SD卡中是否有緩存。SD卡中假設存在緩存,則圖片從SD卡中拉取,否則從網絡載入圖片。依據這三級緩存機制。能夠讓我們的應用程序在載入圖片的時候做到遊刃有余,有效的避免內存溢出。
PS:當然如今處理網絡圖片的時候,一般人都會選擇XUtils中的BitmapUtil,它已經將網絡緩存處理的相當好了,使用起來非常方便--本人就一直在用。
仿照BitMapUtil的實現思路,定制一個自己的圖片載入工具,來理解一下三級緩存的策略,希望對自己會有一個提升。
二、網絡緩存
網絡拉取圖片嚴格來講不能稱之為緩存,實質上就是下載url相應的圖片。我們這裏姑且把它看作是緩存的一種。仿照BitmapUtil中的display方法,我自己定制的CustomBitmapUtils也定義這種方法,依據傳入的url,將圖片設置到ivPic控件上。
public void display(ImageView ivPic, String url) { }
<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 對象來區分不同組的圖片。
- 你能維持好數量和質量之間的平衡嗎?有些時候。存儲多個低像素的圖片。而在後臺去開線程載入高像素的圖片會更加的有效。
/** * 內存緩存 * * @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中圖片的三級緩存策略