Android圖片三級緩存策略(網(wǎng)絡(luò)、本地、內(nèi)存緩存)
一、簡介
現(xiàn)在的Android應(yīng)用程序中,不可避免的都會(huì)使用到圖片,如果每次加載圖片的時(shí)候都要從網(wǎng)絡(luò)重新拉取,這樣不但很耗費(fèi)用戶的流量,而且圖片加載的也會(huì)很慢,用戶體驗(yàn)很不好。所以一個(gè)應(yīng)用的圖片緩存策略是很重要的。通常情況下,Android應(yīng)用程序中圖片的緩存策略采用“內(nèi)存-本地-網(wǎng)絡(luò)”三級緩存策略,首先應(yīng)用程序訪問網(wǎng)絡(luò)拉取圖片,分別將加載的圖片保存在本地SD卡中和內(nèi)存中,當(dāng)程序再一次需要加載圖片的時(shí)候,先判斷內(nèi)存中是否有緩存,有則直接從內(nèi)存中拉取,否則查看本地SD卡中是否有緩存,SD卡中如果存在緩存,則圖片從SD卡中拉取,否則從網(wǎng)絡(luò)加載圖片。依據(jù)這三級緩存機(jī)制,可以讓我們的應(yīng)用程序在加載圖片的時(shí)候做到游刃有余,有效的避免內(nèi)存溢出。
PS:當(dāng)然現(xiàn)在處理網(wǎng)絡(luò)圖片的時(shí)候,一般人都會(huì)選擇XUtils中的BitmapUtil,它已經(jīng)將網(wǎng)絡(luò)緩存處理的相當(dāng)好了,使用起來非常方便--本人就一直在用。仿照BitMapUtil的實(shí)現(xiàn)思路,定制一個(gè)自己的圖片加載工具,來理解一下三級緩存的策略,希望對自己會(huì)有一個(gè)提升。
二、網(wǎng)絡(luò)緩存
網(wǎng)絡(luò)拉取圖片嚴(yán)格來講不能稱之為緩存,實(shí)質(zhì)上就是下載url對應(yīng)的圖片,我們這里姑且把它看作是緩存的一種。仿照BitmapUtil中的display方法,我自己定制的CustomBitmapUtils也定義這個(gè)方法,根據(jù)傳入的url,將圖片設(shè)置到ivPic控件上。
public void display(ImageView ivPic, String url) { }
定義網(wǎng)絡(luò)緩存的工具類,在訪問網(wǎng)絡(luò)的時(shí)候,我使用了AsyncTask來實(shí)現(xiàn),在AsyncTask的doInBackGround方法里下載圖片,然后將 圖片設(shè)置給ivPic控件,AsyncTask有三個(gè)泛型,其中第一個(gè)泛型是執(zhí)行異步任務(wù)的時(shí)候,通過execute傳過來的參數(shù),第二個(gè)泛型是更新的進(jìn)度,第三個(gè)泛型是異步任務(wù)執(zhí)行完成之后,返回來的結(jié)果,我們這里返回一個(gè)Bitmap。具體的下載實(shí)現(xiàn)代碼如下:
/** * 網(wǎng)絡(luò)緩存的工具類 * * @author ZHY * */ public class NetCacheUtils { private LocalCacheUtils localCacheUtils; private MemoryCacheUtils memoryCacheUtils; public NetCacheUtils() { localCacheUtils = new LocalCacheUtils(); memoryCacheUtils = new MemoryCacheUtils(); } /** * 從網(wǎng)絡(luò)下載圖片 * * @param ivPic * @param url */ public void getBitmapFromNet(ImageView ivPic, String url) { // 訪問網(wǎng)絡(luò)的操作一定要在子線程中進(jìn)行,采用異步任務(wù)實(shí)現(xiàn) MyAsyncTask task = new MyAsyncTask(); task.execute(ivPic, url); } /** * 第一個(gè)泛型--異步任務(wù)執(zhí)行的時(shí)候,通過execute傳過來的參數(shù); 第二個(gè)泛型--更新進(jìn)度; 第三個(gè)泛型--異步任務(wù)執(zhí)行以后返回的結(jié)果 * * @author ZHY * */ private class MyAsyncTask extends AsyncTask { private ImageView ivPic; private String url; // 耗時(shí)任務(wù)執(zhí)行之前 --主線程 @Override protected void onPreExecute() { super.onPreExecute(); } // 后臺(tái)執(zhí)行的任務(wù) @Override protected Bitmap doInBackground(Object... params) { // 執(zhí)行異步任務(wù)的時(shí)候,將URL傳過來 ivPic = (ImageView) params[0]; url = (String) params[1]; Bitmap bitmap = downloadBitmap(url); // 為了保證ImageView控件和URL一一對應(yīng),給ImageView設(shè)定一個(gè)標(biāo)記 ivPic.setTag(url); // 關(guān)聯(lián)ivPic和URL return bitmap; } // 更新進(jìn)度 --主線程 @Override protected void onProgressUpdate(Void... values) { super.onProgressUpdate(values); } // 耗時(shí)任務(wù)執(zhí)行之后--主線程 @Override protected void onPostExecute(Bitmap result) { String mCurrentUrl = (String) ivPic.getTag(); if (url.equals(mCurrentUrl)) { ivPic.setImageBitmap(result); System.out.println("從網(wǎng)絡(luò)獲取圖片"); // 從網(wǎng)絡(luò)加載完之后,將圖片保存到本地SD卡一份,保存到內(nèi)存中一份 localCacheUtils.setBitmap2Local(url, result); // 從網(wǎng)絡(luò)加載完之后,將圖片保存到本地SD卡一份,保存到內(nèi)存中一份 memoryCacheUtils.setBitmap2Memory(url, result); } } } /** * 下載網(wǎng)絡(luò)圖片 * * @param url * @return */ private Bitmap downloadBitmap(String url) { HttpURLConnection conn = null; try { URL mURL = new URL(url); // 打開HttpURLConnection連接 conn = (HttpURLConnection) mURL.openConnection(); // 設(shè)置參數(shù) conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestMethod("GET"); // 開啟連接 conn.connect(); // 獲得響應(yīng)碼 int code = conn.getResponseCode(); if (code == 200) { // 相應(yīng)成功,獲得網(wǎng)絡(luò)返回來的輸入流 InputStream is = conn.getInputStream(); // 圖片的輸入流獲取成功之后,設(shè)置圖片的壓縮參數(shù),將圖片進(jìn)行壓縮 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; // 將圖片的寬高都壓縮為原來的一半,在開發(fā)中此參數(shù)需要根據(jù)圖片展示的大小來確定,否則可能展示的不正常 options.inPreferredConfig = Bitmap.Config.RGB_565; // 這個(gè)壓縮的最小 // Bitmap bitmap = BitmapFactory.decodeStream(is); Bitmap bitmap = BitmapFactory.decodeStream(is, null, options) ;// 經(jīng)過壓縮的圖片 return bitmap; } } catch (Exception e) { e.printStackTrace(); } finally { // 斷開連接 conn.disconnect(); } return null; } }
三、本地緩存
從網(wǎng)絡(luò)加載完圖片之后,將圖片保存到本地SD卡中。在加載圖片的時(shí)候,判斷一下SD卡中是否有圖片緩存,如果有,就直接從SD卡加載圖片。本地緩存的工具類中有兩個(gè)公共的方法,分別是向本地SD卡設(shè)置網(wǎng)絡(luò)圖片,獲取SD卡中的圖片。設(shè)置圖片的時(shí)候采用鍵值對的形式進(jìn)行存儲(chǔ),將圖片的url作為鍵,作為文件的名字,圖片的Bitmap作位值來保存。由于url含有特殊字符,不能直接作為圖片的名字來存儲(chǔ),故采用url的MD5值作為文件的名字。
/** * 本地緩存 * * @author ZHY * */ public class LocalCacheUtils { /** * 文件保存的路徑 */ public static final String FILE_PATH = Environment .getExternalStorageDirectory().getAbsolutePath() + "/cache/pics"; /** * 從本地SD卡獲取網(wǎng)絡(luò)圖片,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卡寫網(wǎng)絡(luò)圖片 * * @param url * @param bitmap */ public void setBitmap2Local(String url, Bitmap bitmap) { try { // 文件的名字 String fileName = MD5Encoder.encode(url); // 創(chuàng)建文件流,指向該路徑,文件名叫做fileName File file = new File(FILE_PATH, fileName); // file其實(shí)是圖片,它的父級File是文件夾,判斷一下文件夾是否存在,如果不存在,創(chuàng)建文件夾 File fileParent = file.getParentFile(); if (!fileParent.exists()) { // 文件夾不存在 fileParent.mkdirs();// 創(chuàng)建文件夾 } // 將圖片保存到本地 bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(file)); } catch (Exception e) { e.printStackTrace(); } } }
四、內(nèi)存緩存
內(nèi)存緩存說白了就是在內(nèi)存中保存一份圖片集合,首先會(huì)想到HashMap這種鍵值對的形式來進(jìn)行保存,以url作為key,bitmap作為value。但是在Java中這種默認(rèn)的new對象的方式是強(qiáng)引用,JVM在進(jìn)行垃圾回收的時(shí)候是不會(huì)回收強(qiáng)引用的,所以如果加載的圖片過多的話,map會(huì)越來越大,很容易出現(xiàn)OOM異常。在Android2.3之前,還可以通過軟引用或者弱引用來解決,但是Android2.3之后,Google官方便不再推薦軟引用了,Google推薦我們使用LruCache。
在過去,我們經(jīng)常會(huì)使用一種非常流行的內(nèi)存緩存技術(shù)的實(shí)現(xiàn),即軟引用或弱引用 (SoftReference or WeakReference)。但是現(xiàn)在已經(jīng)不再推薦使用這種方式了,因?yàn)閺?Android 2.3 (API Level 9)開始,垃圾回收器會(huì)更傾向于回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數(shù)據(jù)會(huì)存儲(chǔ)在本地的內(nèi)存當(dāng)中,因而無法用一種可預(yù)見的方式將其釋放,這就有潛在的風(fēng)險(xiǎn)造成應(yīng)用程序的內(nèi)存溢出并崩潰。
為了能夠選擇一個(gè)合適的緩存大小給LruCache, 有以下多個(gè)因素應(yīng)該放入考慮范圍內(nèi),例如:
你的設(shè)備可以為每個(gè)應(yīng)用程序分配多大的內(nèi)存?Android默認(rèn)是16M。 設(shè)備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進(jìn)行預(yù)加載,因?yàn)橛锌赡芎芸煲矔?huì)顯示在屏幕上? 你的設(shè)備的屏幕大小和分辨率分別是多少?一個(gè)超高分辨率的設(shè)備(例如 Galaxy Nexus) 比起一個(gè)較低分辨率的設(shè)備(例如 Nexus S),在持有相同數(shù)量圖片的時(shí)候,需要更大的緩存空間。 圖片的尺寸和大小,還有每張圖片會(huì)占據(jù)多少內(nèi)存空間。 圖片被訪問的頻率有多高?會(huì)不會(huì)有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應(yīng)該讓一些圖片常駐在內(nèi)存當(dāng)中,或者使用多個(gè)LruCache 對象來區(qū)分不同組的圖片。 你能維持好數(shù)量和質(zhì)量之間的平衡嗎?有些時(shí)候,存儲(chǔ)多個(gè)低像素的圖片,而在后臺(tái)去開線程加載高像素的圖片會(huì)更加的有效。 以上是Google對LruCache的描述,其實(shí)LruCache的使用非常簡單,跟Map非常相近,只是在創(chuàng)建LruCache對象的時(shí)候需要指定它的最大允許內(nèi)存,一般設(shè)置為當(dāng)前應(yīng)用程序的最大運(yùn)行內(nèi)存的八分之一即可。
/** * 內(nèi)存緩存 * * @author ZHY * */ public class MemoryCacheUtils { /* * 由于map默認(rèn)是強(qiáng)引用,所有在JVM進(jìn)行垃圾回收的時(shí)候不會(huì)回收map的引用 */ // private HashMap<string, bitmap=""> map = new HashMap<string, bitmap="">(); // 軟引用的實(shí)例,在內(nèi)存不夠時(shí),垃圾回收器會(huì)優(yōu)先考慮回收 // private HashMap<string, bitmap="">> mSoftReferenceMap = new // HashMap<string, bitmap="">>(); // LruCache private LruCache<string, bitmap=""> lruCache; public MemoryCacheUtils() { // lruCache最大允許內(nèi)存一般為Android系統(tǒng)分給每個(gè)應(yīng)用程序內(nèi)存大?。J(rèn)Android系統(tǒng)給每個(gè)應(yīng)用程序分配16兆內(nèi)存)的八分之一(推薦) // 獲得當(dāng)前應(yīng)用程序運(yùn)行的內(nèi)存大小 long mCurrentMemory = Runtime.getRuntime().maxMemory(); int maxSize = (int) (mCurrentMemory / 8); // 給LruCache設(shè)置最大的內(nèi)存 lruCache = new LruCache<string, bitmap="">(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { // 獲取每張圖片所占內(nèi)存的大小 // 計(jì)算方法是:圖片顯示的寬度的像素點(diǎn)乘以高度的像素點(diǎn) int byteCount = value.getRowBytes() * value.getHeight();// 獲取圖片占用內(nèi)存大小 return byteCount; } }; } /** * 從內(nèi)存中讀取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 // 最近最少使用,將內(nèi)存控制在一定的大小內(nèi),超過這個(gè)內(nèi)存大小,就會(huì)優(yōu)先釋放最近最少使用的那些東東 Bitmap bitmap = lruCache.get(url); return bitmap; } /** * 將圖片保存到內(nèi)存中 * * @param url * @param bitmap */ public void setBitmap2Memory(String url, Bitmap bitmap) { // 向內(nèi)存中設(shè)置,key,value的形式,首先想到HashMap // map.put(url, bitmap); // 保存軟引用到map中 // SoftReference<bitmap> mSoftReference = new // SoftReference<bitmap>(bitmap); // mSoftReferenceMap.put(url, mSoftReference); lruCache.put(url, bitmap); } }</bitmap></bitmap></bitmap></string,></string,></string,></string,></string,></string,>
好了?,F(xiàn)在三級緩存策略封裝完畢,接下來定制我們自己的BitmapUtils
/** * 自定義的加載圖片的工具類,類似于Xutils中的BitmapUtil,在實(shí)際使用中,一般使用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(); } /** * 加載圖片,將當(dāng)前URL對應(yīng)的圖片顯示到ivPic的控件上 * * @param ivPic * ImageView控件 * @param url * 圖片的地址 */ public void display(ImageView ivPic, String url) { // 設(shè)置默認(rèn)顯示的圖片 ivPic.setImageResource(R.drawable.ic_launcher); // 1、內(nèi)存緩存 bitmap = memoryCacheUtils.getBitmapFromMemory(url); if (bitmap != null) { ivPic.setImageBitmap(bitmap); System.out.println("從內(nèi)存緩存中加載圖片"); return; } // 2、本地磁盤緩存 bitmap = localCacheUtils.getBitmapFromLocal(url); if (bitmap != null) { ivPic.setImageBitmap(bitmap); System.out.println("從本地SD卡加載的圖片"); memoryCacheUtils.setBitmap2Memory(url, bitmap);// 將圖片保存到內(nèi)存 return; } // 3、網(wǎng)絡(luò)緩存 netCacheUtils.getBitmapFromNet(ivPic, url); /* * 從網(wǎng)絡(luò)獲取圖片之后,將圖片保存到手機(jī)SD卡中,在進(jìn)行圖片展示的時(shí)候,優(yōu)先從SD卡中讀取緩存,key是圖片的URL的MD5值, * value是保存的圖片bitmap */ } }
在mainActivity中使用ListView加載網(wǎng)絡(luò)圖片
/** * Android中三級緩存--網(wǎng)絡(luò)緩存-本地緩存-內(nèi)存緩存 * * @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"; // 初始化一些網(wǎng)絡(luò)圖片 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(); // 加載網(wǎng)絡(luò)圖片 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; } } }
運(yùn)行的結(jié)果如下:
程序第一次運(yùn)行,日志打印如下
之后將圖片緩存在SD卡中,從本地加載圖片
然后將圖片緩存到內(nèi)存,從內(nèi)存加載圖片
OK,到目前為止,Android中圖片的三級緩存原理就都介紹完了,我自己本人受益匪淺,希望能夠幫助到需要的朋友。需要源碼的請點(diǎn)擊如下鏈接進(jìn)行下載。
相關(guān)文章
Android Studio實(shí)現(xiàn)簡易計(jì)算器
這篇文章主要為大家詳細(xì)介紹了Android Studio實(shí)現(xiàn)簡易計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07詳解Android開啟OTG功能/USB?Host?API功能
這篇文章主要介紹了Android開啟OTG功能/USB?Host?API功能,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Android 監(jiān)聽Notification 被清除實(shí)例代碼
本文主要介紹Android 監(jiān)聽Notification 事件,這里給大家提供實(shí)例代碼進(jìn)行參考,有需要的小伙伴可以參考下2016-07-07Android onMeasure與onDraw及自定義屬性使用示例
這篇文章主要介紹了Android onMeasure與onDraw及自定義屬性使用示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02android使用Rxjava實(shí)現(xiàn)倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了android使用Rxjava實(shí)現(xiàn)倒計(jì)時(shí)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06Android編程實(shí)現(xiàn)的重力感應(yīng)示例代碼
這篇文章主要介紹了Android編程實(shí)現(xiàn)的重力感應(yīng)效果,以完整示例代碼形式分析了重力感應(yīng)的原理與實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android自定義View實(shí)現(xiàn)漸變色進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)漸變色進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05