Android 學習筆記之圖片三級緩存

最近在學習的時候接觸到Android對圖片的獲取途徑,寫一篇筆記記錄一發。

什么是圖片三級緩存

三級緩存指的是內存緩存,本地緩存和網絡緩存,而App獲取圖片資源可以通過這三種途徑來獲取,也就是指圖片的三級緩存。

為什么要使用圖片三級緩存

如果一個App有很多圖片,而每次獲取圖片都要通過網絡來獲取,則這個應用一定會很消耗流量,所有這時候有必要將獲取的圖片做緩存保存在本地或者內存中。而他們獲取的優先級是首先從內存獲取,其次是從本地獲取,前面都獲取不到圖片才通過網絡從服務器中獲取圖片。從內存獲取圖片的速度是最快的,如果是加載很多圖片,有可能會導致內存溢出(OOM).

如何做圖片三級緩存

可以從最外層做起,首先從網絡獲取圖片

  • 網絡獲圖片存工具類(NetCacheUtils),核心是使用Android提供的異步任務獲取數據類AsyncTask來加載網絡圖片,他的底層其實就是對線程池和Handler的封裝,所有其中的方法才有運行在主線程和子線程之分。
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
/** 
* Created by 毛麒添 on 2017/1/14 0014.
 * 圖片三級緩存之網絡緩存 
* 從網絡中獲取圖片 
* 使用異步任務來獲取圖片 
*/
public class NetCacheUtils {   
 private ImageView imageView;    
private LocalCacheUtils localCacheUtils;   
 private MemoryCacheUtils memoryCacheUtils;    
private String url;    
public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) {
        this.localCacheUtils=localCacheUtils;        
        this.memoryCacheUtils=memoryCacheUtils;   
 }    

public void getBitmapFromNet(ImageView imageView, String url) {    
 //開啟異步任務      
 new BitmapTack().execute(imageView,url);
    
}    
class BitmapTack extends AsyncTask<Object,Integer,Bitmap>{       
 //預備加載,運行在主線程        
@Override       
 protected void onPreExecute() 
{           
 super.onPreExecute();       
 }        
@Override        
protected Bitmap doInBackground(Object... params) {            
//獲取外部需要設置的ImageView對象           
 imageView = (ImageView) params[0];            
url = (String) params[1];            //開始下載圖片          
  //imageView.setTag(url);
給圖片設置唯一標記,讓其在設置bitmap時候判斷url是否相同來進行設置,防止圖片不一致           
 Bitmap bitmap=downLoadBitmap(url);           
 return bitmap;        }        
   
//執行完成,運行在主線程        
@Override        
protected void onPostExecute(Bitmap bitmap) 
{            
if(bitmap!=null){               
 imageView.setImageBitmap(bitmap);                
//設置本地緩存               
 localCacheUtils.setLocalBitmapCache(bitmap,url);               
 //設置內存緩存                
memoryCacheUtils.setMemoryCache(url,bitmap);           
 }           
 super.onPostExecute(bitmap);       
 }   
 }    
/**     
* 下載圖片bitmap對象    
 * @param url 下載地址     
* @return 請求成功返回獲取圖片的bitmap對象,否則返回空     
*/    
private Bitmap downLoadBitmap(String url) {        
HttpsURLConnection conn=null;       
 try {                
conn= (HttpsURLConnection) new URL(url).openConnection();                
conn.setConnectTimeout(5000);//連接延時                
conn.setReadTimeout(5000);//讀取延時                
int code = conn.getResponseCode();               
 if(code==200){                    
InputStream inputStream = conn.getInputStream();                   
 //根據網絡獲取的輸入流生成Bitmap對象                   
 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);                   
 return bitmap;            }       
 } catch (IOException e) 
{            e.printStackTrace();       
 }finally {           
 if(conn!=null){               
 conn.disconnect();           
     }        
   }       
 return  null;  
  }
}
  • 從內存中或圖片緩存(LocalCacheUtils),要獲取圖片緩存,首先工具類中必定要有設置緩存的方法,然后獲取網絡數據到圖片的Bitmap對象后就將其保存起來,在使用的時候調用獲取的本地緩存的方法就可以的得到本地緩存的圖片,而將圖片保存一般是使用url地址作為圖片的文件名稱,所有為了服務器安全,有必要對url地址做MD5加密之后再作為緩存圖片文件名稱。
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import java.io.File;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/** 
* Created by 毛麒添 on 2017/1/14 0014. 
* 圖片三級緩存之本地緩存 
* 設置和獲取本地圖片緩存的方法(緩存放在Sdcard中) 
*/
public class LocalCacheUtils {     
//存放本地緩存的地址     
private static final  String CACHE_PATH=Environment.getExternalStorageDirectory().getAbsolutePath()+"bitmap_cache";     
//設置本地圖片緩存   
 public void setLocalBitmapCache(Bitmap bitmap, String url)
{        
File dir=new File(CACHE_PATH);       
 if(!dir.exists()||!dir.isDirectory()){//如果不存在或者不是一個文件夾           
//創建文件夾            
dir.mkdirs();        
}        
String filename = Md5Util.encoder(url);//給文件名稱使用MD5加密       
 //創建緩存文件       
 File bitmapcachefile=new File(dir,filename);        
try {            
//圖片壓縮格式,壓縮比例            
bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(bitmapcachefile));        
} catch (FileNotFoundException e) {            
e.printStackTrace();        
}    
}   
 //獲取本地圖片緩存    
public Bitmap getLocalBitmapCache(String url){       
 //根據圖片路徑和名稱獲取圖片       
 File bitmapcachefile=new File(CACHE_PATH,Md5Util.encoder(url));       
 if(bitmapcachefile.exists()){           
 try {               
 Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(bitmapcachefile));               
 return bitmap;            
} catch (FileNotFoundException e) { 
       e.printStackTrace();           
 }        
}        
return null;    
}}
  • MD5加密工具類
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {   
/**    
* 給指定字符串按照md5算法去加密   
 * @param psd 需要加密的字符串    
* @return  返回字符串     
*/  
 public static String encoder(String psd) {     
 try {        
 //1,指定加密算法類型         
MessageDigest digest = MessageDigest.getInstance("MD5");        
 //2,將需要加密的字符串中轉換成byte類型的數組,然后進行隨機哈希過程        
 byte[] bs = digest.digest(psd.getBytes());
//       System.out.println(bs.length);         
//3,循環遍歷bs,然后讓其生成32位字符串,固定寫法        
 //4,拼接字符串過程         
StringBuffer stringBuffer = new StringBuffer();        
 for (byte b : bs) {           
 int i = b & 0xff;            
//int類型的i需要轉換成16進制字符            
String hexString = Integer.toHexString(i);     i
f(hexString.length()<2){              
 hexString = "0"+hexString;           
 }            
stringBuffer.append(hexString);         
}                 
return stringBuffer.toString();    
  } catch (NoSuchAlgorithmException e) {  
       e.printStackTrace();     
 }      return "";  
 }
}
  • 內存緩存(MemoryCacheUtils)要在內存中存放東西,而要存放東西,能想到的就是ArrayList和HashMap,當程序跑起來,他們運行在內存當中。而ArrayList是可以存放數據,但是要從其中取數據還需只要數據的角標,不好取出;而HashMap中key和values的結果存放數據,則正好我們只要傳入圖片的URL地址和圖片的Bitmap對象,當從內存中獲取圖片緩存的時候只要用圖片的key獲取就可以了,好,開搞。
public class MemoryCacheUtils {
//使用hashmap 存儲圖片
private HashMap<String,Bitmap> myMemoryCache=new HashMap<String,Bitmap>();

//設置內存緩存
public void setMemoryCache(String url,Bitmap bitmap){

myMemoryCache.put(url,bitmap);
}

//獲取內存緩存public Bitmap getMemoryCache(String url){
 return myMemoryCache.get(url);
}
  • 這樣一搞,好像是沒有什么問題,而且代碼少,感覺內存緩存是三級緩存中最簡單的。其實不然,這樣的方式在內存中存放數據,當圖片加載很多的時候,程序就崩掉了,錯誤日志中顯示內存溢出(OOM)。為什么會出現這種情況的,在大多數情況下,Android 系統給每個應用分配的內存為16M,如果圖片加載太多超過系統分配的最大內存當然會造成內存溢出,而這時候java本身自帶的垃圾回收器不起作用,那該如何解決呢?我們可以從java中對對象的引用來分析,java中對象的引用是放在棧中,指向的引用對象是放在堆中,而引用在默認的情況下是強引用,而我們使用的HashMap來存放數據就是默認的強引用,所有本身的垃圾回收器當然不會回收釋放內存中圖片的緩存。這時候java除了有強引用,也還提供了其他的引用方式:
  • 軟引用(SoftReference):垃圾回收器會考慮回收
  • 弱引用(WeakReference):垃圾回收器優先考慮回收
  • 虛引用(PhantomReference):垃圾回收器最先考慮回收
    我們既要想在內存中緩存圖片又想垃圾回收器能夠起到回收作用,就可以選擇軟引用對Bitmap經常一次封裝
public class MemoryCacheUtils 
{
private HashMap<String,SoftReference<Bitmap>> myMemoryCache=new HashMap<String,SoftReference<Bitmap>>();
//設置內存緩存
public void setMemoryCache(String url,Bitmap bitmap)
{
  SoftReference<Bitmap> bitmapSoftReference=new SoftReference<Bitmap>(bitmap);
    myMemoryCache.put(url,bitmapSoftReference);
}
/獲取內存緩存
public Bitmap getMemoryCache(String url){ 
SoftReference<Bitmap> bitmapSoftReference=myMemoryCache.get(url);
if(bitmapSoftReference!=null){
Bitmap bitmap = bitmapSoftReference.get();    
  return bitmap;
}
return null;
}

經過這一番改造,垃圾回收器確實是可以起作用了,但是通過一些前輩們的研究,谷歌官方還是不推薦我們使用軟引用,官方文檔是這樣說的:

官方文檔截圖.png

摳腳的英語翻譯:
在過去,我們經常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向于回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程序的內存溢出并崩潰。
官方文檔鏈接地址:
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

而在文檔中則是推薦我們使用LruCache(使用v4包中的)這個類來做內存緩存,其實他的本質也是對HashMap的封裝。好吧,繼續改造

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

public class MemoryCacheUtils 
{
private LruCache<String,Bitmap> myMemoryCache;

public MemoryCacheUtils(){    
long maxMemory = Runtime.getRuntime().maxMemory();//獲取分配給每個App的內存大小    
myMemoryCache=new LruCache<String,Bitmap>((int) (maxMemory/8)){        
//返回每個對象的大小        
@Override        
protected int sizeOf(String key, Bitmap value) {           
 int byteCount = value.getByteCount();           
 return byteCount;        
       }    
   };
}
//設置內存緩存
public void setMemoryCache(String url,Bitmap bitmap)
{
  myMemoryCache.put(url,bitmap);
}
/獲取內存緩存
public Bitmap getMemoryCache(String url){ 
   return myMemoryCache.get(url);
}
  • 到此圖片三級緩存的三個層次基本上算是完成了,然在獲取圖片的工具方法中調用即可
import android.graphics.Bitmap;
import android.widget.ImageView;
/** 
* Created by 毛麒添 on 2017/1/14 0014. 
* 自己創建三級緩存加載圖片 
*/
public class BitmapUtils {    
//網絡獲取圖片工具類對象    
private NetCacheUtils netCacheUtils;   
private MemoryCacheUtils memoryCacheUtils;    
private LocalCacheUtils localCacheUtils;    
private Bitmap bitmap;    

public BitmapUtils(){       
 localCacheUtils=new LocalCacheUtils();        
memoryCacheUtils=new MemoryCacheUtils();       
 netCacheUtils=new NetCacheUtils(localCacheUtils,memoryCacheUtils);    
}    
/**     
* 顯示圖片的方法     
* @param imageView 需要設置圖片的圖片對象    
* @param url 請求地址    
* 優先從內存中加載圖片    
* 其次從本地中(Sdcard)加載圖片     
* 最后從網絡中獲取圖片     
*/    
public void displayBitmapImage(ImageView imageView, String url) {       
 //從緩存緩存中獲取圖片        
bitmap = memoryCacheUtils.getMemoryCache(url);       
 if(bitmap!=null){
//如果內存中圖片緩存不為空,直接將其設置給ImageView           
 imageView.setImageBitmap(bitmap);           
 return;        
}        
//從本地緩存中獲取圖片       
bitmap = localCacheUtils.getLocalBitmapCache(url);        
if(bitmap !=null){//如果本地圖片緩存不為空,直接將其設置給ImageView            
imageView.setImageBitmap(bitmap);           
 return;        
}       
 //從網絡獲取圖片        
netCacheUtils.getBitmapFromNet(imageView,url);   
 }
}

終于,圖片三級緩存的小框架已經搭建好,我們使用ListView或者ViewPager的控件加載圖片的時候在適配器getView()方法中給ImageView設置圖片就可以使用這個圖片三級緩存。

最后

xutils3 源碼結構.png

其實做了這么多的東西,在加載很多的圖片的時候還是會出現OOM,通過看Xutils3的源碼結構,其實他的圖片加載也是運用三級緩存結構,但是他做的事情的遠遠不至于我上面所說的這么簡單,學無止境,所以我們可以先把核心的東西給先弄明白了解,再繼續往里深入,菜鳥通過積累,總有一天會成為大鵬。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容