最近在學習的時候接觸到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的源碼結構,其實他的圖片加載也是運用三級緩存結構,但是他做的事情的遠遠不至于我上面所說的這么簡單,學無止境,所以我們可以先把核心的東西給先弄明白了解,再繼續往里深入,菜鳥通過積累,總有一天會成為大鵬。