Android 圖片緩存設計

1.簡介

大傢都知道,在我們Android 開發的過程中,對於圖片的處理,是非常重要的,而對於我們如果每次都重網絡去拉去圖片,那樣會造成,現在android應用中不可避免的要使用圖片,有些圖片是可以變化的,需要每次啟動時從網絡拉取,這種場景在有廣告位的應用以及純圖片應用(比如淘寶,qq的照片墻)中比較多。

現在有一個問題:假如每次啟動的時候都從網絡拉取圖片的話,勢必會消耗很多流量。在當前的狀況下,對於非wifi用戶來說,流量還是很貴的,一個很耗流量的應用,其用戶數量級肯定要受到影響。當然,我想,向百度美拍這樣的應用,必然也有其內部的圖片緩存策略。總之,圖片緩存是很重要而且是必須的。

2.圖片緩存的原理

實現圖片緩存也不難,需要有相應的cache策略。這裡我采用 內存-文件-網絡 三層cache機制,其中內存緩存包括強引用緩存和軟引用緩存(SoftReference),其實網絡不算cache,這裡姑且也把它劃到緩存的層次結構中。當根據url向網絡拉取圖片的時候,先從內存中找,如果內存中沒有,再從緩存文件中查找,如果緩存文件中也沒有,再從網絡上通過http請求拉取圖片。在鍵值對(key-value)中,這個圖片緩存的key是圖片url的hash值,value就是bitmap。所以,按照這個邏輯,隻要一個url被下載過,其圖片就被緩存起來瞭。

關於Java中對象的軟引用(SoftReference),如果一個對象具有軟引用,內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足瞭,就會回收這些對象的內存。隻要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高 速緩存。使用軟引用能防止內存泄露,增強程序的健壯性。

3.實例源碼

 

(1)內存緩存

 

package com.zengtao.tools;

import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * 內存緩存:兩層緩存
 * 
 * @author zengtao 2015年4月27日 上午10:39:23
 */
public class MemoryCache {

	private final static int SOFT_CACHE_SIZE = 15; // 軟引用緩存容量
	private static LruCache mLruCache; // 硬引用緩存
	private static LinkedHashMap> mSoftCache; // 軟引用緩存

	@SuppressLint("NewApi")
	public MemoryCache(Context context) {
		int memClass = ((ActivityManager) context
				.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
		int cacheSize = 1024 * 1024 * memClass / 4; // 獲取系統的1/4的空間 作為緩存大小
		mLruCache = new LruCache(cacheSize) {

			@Override
			protected int sizeOf(String key, Bitmap value) {
				if (value != null) {
					return value.getRowBytes() * value.getHeight();
				}
				return 0;
			}

			@Override
			protected void entryRemoved(boolean evicted, String key,
					Bitmap oldValue, Bitmap newValue) {
				if (oldValue != null) {
					// 硬引用緩存滿的時候,會根據lru算法把最近沒有被使用的圖片抓入軟引用
					mSoftCache.put(key, new SoftReference(oldValue));
				}
			}
		};
		mSoftCache = new LinkedHashMap>(
				SOFT_CACHE_SIZE, 0.75f, true) {

			private static final long serialVersionUID = 1L;

			@Override
			protected boolean removeEldestEntry(
					java.util.Map.Entry> eldest) {
				if (size() > SOFT_CACHE_SIZE) {
					return true;
				}
				return false;
			}
		};
	}

	/**
	 * 存儲圖片到緩存
	 * 
	 * @param url
	 *            :key
	 * @param bitmap
	 *            : 圖片
	 */
	@SuppressLint("NewApi")
	public void saveBitmap(String url, Bitmap bitmap) {
		if (bitmap != null) {
			synchronized (bitmap) {
				mLruCache.put(url, bitmap);
			}
		}
	}

	/**
	 * 獲取緩存圖片
	 * 
	 * @param url
	 *            :url
	 * @return
	 */
	@SuppressLint("NewApi")
	public Bitmap getBitmap(String url) {
		Bitmap bitmap = null;
		// 從硬引用找
		synchronized (mLruCache) {
			// 從硬引用中獲取
			bitmap = mLruCache.get(url);
			if (bitmap != null) {
				// 如果找到瞭,將元素移動到linkendHashMap的最前面,從而保證lrd算法中的是最後刪除
				mLruCache.remove(url);
				mLruCache.put(url, bitmap);
				return bitmap;
			}
		}
		// 硬引用沒找到,從軟引用找
		synchronized (mSoftCache) {
			SoftReference softReference = mSoftCache.get(url);
			if (softReference != null) {
				bitmap = softReference.get();
				// 如果找到瞭,重新添加到硬緩存中
				mLruCache.put(url, bitmap);
				mSoftCache.remove(url);
				return bitmap;
			} else {
				mSoftCache.remove(url);
			}
		}
		return null;
	}

	public void clearCache() {
		mSoftCache.clear();
	}
}

 

 

(2)文件緩存

 

package com.zengtao.tools;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.os.StatFs;

/**
 * 文件緩存
 * 
 * @author zengtao 2015年4月27日 上午11:49:52
 */
public class FileCache {
	private final static String IMAGECACHE = "ImageCache";
	private final static String lASTPATHNAME = ".cache"; // 文件名

	private final static int MB = 1024 * 1024;
	private final static int CACHESIZE = 10;
	private final static int SDCARD_FREE_SPANCE_CACHE = 10;

	public FileCache() {
		removeCache(getDirectory());
	}

	/**
	 * 將圖片存入緩存
	 * 
	 * @param url
	 *            : 地址
	 * @param bitmap
	 *            : 圖片
	 */
	public void saveBitmap(String url, Bitmap bitmap) {
		if (bitmap == null) {
			return;
		}
		if (SDCARD_FREE_SPANCE_CACHE > caluateSDCardFreeSpance()) {
			return; // 空間不足
		}
		String fileName = convertUrlToFileName(url);
		String dirPath = getDirectory();
		File dirFile = new File(dirPath);
		if (dirFile.exists()) {
			dirFile.mkdirs();
		}
		File file = new File(dirPath + "/" + fileName);
		try {
			file.createNewFile();
			OutputStream outputStream = new FileOutputStream(file);
			bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
			outputStream.flush();
			outputStream.close();
		} catch (Exception e) {
			System.out.println("文件未找到或者io異常");
		}
	}

	/**
	 * 獲取文件緩存圖片
	 * 
	 * @param url
	 *            : 地址
	 * @return : bitmap
	 */
	public Bitmap getBitmap(final String url) {
		Bitmap bitmap = null;
		final String path = getDirectory() + convertUrlToFileName(url);
		File file = new File(path);
		if (file.exists()) {
			bitmap = BitmapFactory.decodeFile(path);
			if (bitmap == null) {
				file.delete();
			} else {
				updateFileTime(path);
			}
		}
		return bitmap;
	}

	/**
	 * 獲取sdCard路徑
	 * 
	 * @return :路徑地址
	 */
	private String getSDCardPath() {
		String path = "";
		File file = null;
		boolean isSDCardExist = Environment.getExternalStorageState()
				.toString().equals(android.os.Environment.MEDIA_MOUNTED); // 判斷是否有sdCard
		if (isSDCardExist) {
			file = Environment.getExternalStorageDirectory();
		}
		if (file != null) {
			path = file.toString();
		}
		return path;
	}

	/**
	 * 計算存儲目錄下的文件大小,
	 * 當文件總大小大於規定的CACHE_SIZE或者sdcard剩餘空間小於FREE_SD_SPACE_NEEDED_TO_CACHE的規定
	 * 那麼刪除40%最近沒有被使用的文件
	 */
	private boolean removeCache(String dirPath) {
		File dir = new File(dirPath);
		File[] files = dir.listFiles();
		if (files == null) {
			return true;
		}

		if (!android.os.Environment.getExternalStorageState().equals(
				android.os.Environment.MEDIA_MOUNTED)) {
			return false;
		}

		int dirSize = 0;
		for (int i = 0; i < files.length; i++) {
			if (files[i].getName().contains(lASTPATHNAME)) {
				dirSize += files[i].length();
			}
		}

		if (dirSize > CACHESIZE * MB
				|| SDCARD_FREE_SPANCE_CACHE > caluateSDCardFreeSpance()) {
			int removeFactor = (int) ((0.4 * files.length) + 1);
			Arrays.sort(files, new FileLastModifSort());
			for (int i = 0; i < removeFactor; i++) {
				if (files[i].getName().contains(lASTPATHNAME)) {
					files[i].delete();
				}
			}
		}

		if (caluateSDCardFreeSpance() <= CACHESIZE) {
			return false;
		}

		return true;
	}

	/**
	 * 獲取緩存目錄
	 * 
	 * @return : 目錄
	 */
	private String getDirectory() {
		return getSDCardPath() + "/" + IMAGECACHE;
	}

	/**
	 * 將url轉換成文件名
	 * 
	 * @param url
	 *            : 地址
	 * @return : 文件名
	 */
	private String convertUrlToFileName(final String url) {
		String[] strs = url.split("/");
		return strs[strs.length - 1] + lASTPATHNAME;
	}

	/**
	 * 計算sdCard上的空閑空間
	 * 
	 * @return : 大小
	 */
	@SuppressLint("NewApi")
	private int caluateSDCardFreeSpance() {
		int freespance = 0;
		StatFs start = new StatFs(Environment.getExternalStorageDirectory()
				.getPath());
		long blocksize = start.getBlockSizeLong();
		long availableBlocks = start.getAvailableBlocksLong();
		freespance = Integer.parseInt(blocksize * availableBlocks + "");
		return freespance;
	}

	/** 修改文件的最後修改時間 **/
	public void updateFileTime(String path) {
		File file = new File(path);
		long lastTime = System.currentTimeMillis();
		file.setLastModified(lastTime);
	}

	/** 根據文件的最後修改時間進行排序 **/
	private class FileLastModifSort implements Comparator {
		public int compare(File arg0, File arg1) {
			if (arg0.lastModified() > arg1.lastModified()) {
				return 1;
			} else if (arg0.lastModified() == arg1.lastModified()) {
				return 0;
			} else {
				return -1;
			}
		}
	}
}

 

 

(3)http緩存

 

package com.zengtao.tools;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

/**
 * 網絡緩存
 * 
 * @author zengtao 2015年4月27日 下午3:32:34
 */
public class HttpCache {
	private static final String LOG_TAG = "ImageGetFromHttp";

	public static Bitmap downloadBitmap(String url) {
		final HttpClient client = new DefaultHttpClient();
		final HttpGet getRequest = new HttpGet(url);

		try {
			HttpResponse response = client.execute(getRequest);
			final int statusCode = response.getStatusLine().getStatusCode();
			if (statusCode != HttpStatus.SC_OK) {
				Log.w(LOG_TAG, "Error " + statusCode
						+ " while retrieving bitmap from " + url);
				return null;
			}

			final HttpEntity entity = response.getEntity();
			if (entity != null) {
				InputStream inputStream = null;
				try {
					inputStream = entity.getContent();
					FilterInputStream fit = new FlushedInputStream(inputStream);
					return BitmapFactory.decodeStream(fit);
				} finally {
					if (inputStream != null) {
						inputStream.close();
						inputStream = null;
					}
					entity.consumeContent();
				}
			}
		} catch (IOException e) {
			getRequest.abort();
			Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e);
		} catch (IllegalStateException e) {
			getRequest.abort();
			Log.w(LOG_TAG, "Incorrect URL: " + url);
		} catch (Exception e) {
			getRequest.abort();
			Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e);
		} finally {
			client.getConnectionManager().shutdown();
		}
		return null;
	}

	/**
	 * InputStream流有個小bug在慢速網絡的情況下可能產生中斷,可以考慮重寫FilterInputStream處理skip方法來解決這個bug
	 * BitmapFactory類的decodeStream方法在網絡超時或較慢的時候無法獲取完整的數據,這裡我
	 * 們通過繼承FilterInputStream類的skip方法來強制實現flush流中的數據
	 * ,主要原理就是檢查是否到文件末端,告訴http類是否繼續。
	 * 
	 * @author zengtao 2015年4月27日 下午6:33:17
	 */
	static class FlushedInputStream extends FilterInputStream {
		public FlushedInputStream(InputStream inputStream) {
			super(inputStream);
		}

		@Override
		public long skip(long n) throws IOException {
			long totalBytesSkipped = 0L;
			while (totalBytesSkipped < n) {
				long bytesSkipped = in.skip(n - totalBytesSkipped);
				if (bytesSkipped == 0L) {
					int b = read();
					if (b < 0) {
						break; // we reached EOF
					} else {
						bytesSkipped = 1; // we read one byte
					}
				}
				totalBytesSkipped += bytesSkipped;
			}
			return totalBytesSkipped;
		}
	}
}

(4)主函數中調用

 

 

@SuppressLint("HandlerLeak")
	private Handler handler = new Handler() {
		public void handleMessage(Message msg) {
			if (msg.arg1 == 0x1) {
				if (msg.obj != null) {
					image.setImageBitmap((Bitmap) msg.obj);
				}
			}
		};
	};

	class MyThread extends Thread {
		@Override
		public void run() {
			bitmap = getBitmap("/wp-content/images1/20190303/77c6a7efce1b9d16f5022b7ef1deb48f8d5464e315.gif");
			Message message = new Message();
			message.arg1 = 0x1;
			message.obj = bitmap;
			handler.sendMessage(message);
		}
	}

	/*** 獲得一張圖片,從三個地方獲取,首先是內存緩存,然後是文件緩存,最後從網絡獲取 ***/
	public Bitmap getBitmap(String url) {
		// 1.從內存緩存中獲取圖片
		Bitmap resultBitmap = memoryCache.getBitmap(url);
		if (resultBitmap == null) {
			// 2.文件緩存中獲取
			resultBitmap = fileCache.getBitmap(url);
			if (resultBitmap == null) {
				// 3.從網絡獲取
				resultBitmap = HttpCache.downloadBitmap(url);
				if (resultBitmap != null) {
					fileCache.saveBitmap(url, resultBitmap);
					memoryCache.saveBitmap(url, resultBitmap);
					System.out.println("3.網絡緩存中獲取圖片");
				}
			} else {
				// 添加到內存緩存
				memoryCache.saveBitmap(url, resultBitmap);
				System.out.println("2.文件緩存中獲取圖片");
			}
		} else {
			System.out.println("1.內存緩存中獲取圖片");
		}
		return resultBitmap;
	}

4.總結

 

以上就完成瞭一套緩存的設計,值得註意的是,當去網絡獲取圖片的時候,圖片過於龐大,一定要做去異步線程中獲取圖片,或者做本地緩存,這樣不會讓用戶感覺自己的app卡死,是的用戶體驗效果更加。

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *