傳送門 ☞ 輪子的專欄 ☞ 轉載請註明 ☞ http://blog.csdn.net/leverage_1229
上週爲360全景項目引入了圖片緩存模塊。因爲是在Android4.0平臺以上運作,出於慣性,都會在設計之前查閱相關資料,儘量避免拿一些以前2.3平臺積累的經驗來進行類比處理。開發文檔中有一個BitmapFun的示例,仔細拜讀了一下,雖說圍繞着Bitmap的方方面面講得都很深入,但感覺很難引入到當前項目中去。
現在的圖片服務提供者基本上都來源於網絡。對於應用平臺而言,訪問網絡屬於耗時操作。尤其是在移動終端設備上,它的顯著表現爲系統的延遲時間變長、用戶交互性變差等。可以想象,一個攜帶着這些問題的應用在市場上是很難與同類產品競爭的。說明一下,本文借鑑了Keegan小鋼和安卓巴士的處理模板,主要針對的是4.0以上平臺應用。2.3以前平臺執行效果未知,請斟酌使用或直接略過:),當然更歡迎您把測試結果告知筆者。
1圖片加載流程
首先,我們談談加載圖片的流程,項目中的該模塊處理流程如下: 在UI主線程中,從內存緩存中獲取圖片,找到後返回。找不到進入下一步;
在工作線程中,從磁盤緩存中獲取圖片,找到即返回並更新內存緩存。找不到進入下一步;
在工作線程中,從網絡中獲取圖片,找到即返回並同時更新內存緩存和磁盤緩存。找不到顯示默認以提示。
2內存緩存類(PanoMemCache)
這裏使用Android提供的LruCache類,該類保存一個強引用來限制內容數量,每當Item被訪問的時候,此Item就會移動到隊列的頭部。當cache已滿的時候加入新的item時,在隊列尾部的item會被回收。
public class PanoMemoryCache {
// LinkedHashMap初始容量
private static final int INITIAL_CAPACITY = 16;
// LinkedHashMap加載因子
private static final float LOAD_FACTOR = 0.75f;
// LinkedHashMap排序模式
private static final boolean ACCESS_ORDER = true;
// 軟引用緩存
private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;
// 硬引用緩存
private static LruCache<String, Bitmap> mLruCache;
public PanoMemoryCache() {
// 獲取單個進程可用內存的最大值
// 方式一:使用ActivityManager服務(計量單位爲M)
/*int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();*/
// 方式二:使用Runtime類(計量單位爲Byte)
final int memClass = (int) Runtime.getRuntime().maxMemory();
// 設置爲可用內存的1/4(按Byte計算)
final int cacheSize = memClass / 4;
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
if(value != null) {
// 計算存儲bitmap所佔用的字節數
return value.getRowBytes() * value.getHeight();
} else {
return 0;
}
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if(oldValue != null) {
// 當硬引用緩存容量已滿時,會使用LRU算法將最近沒有被使用的圖片轉入軟引用緩存
mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
}
}
};
/*
* 第一個參數:初始容量(默認16)
* 第二個參數:加載因子(默認0.75)
* 第三個參數:排序模式(true:按訪問次數排序;false:按插入順序排序)
*/
mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(INITIAL_CAPACITY, LOAD_FACTOR, ACCESS_ORDER) {
private static final long serialVersionUID = 7237325113220820312L;
@Override
protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {
if(size() > SOFT_CACHE_SIZE) {
return true;
}
return false;
}
};
}
/**
* 從緩存中獲取Bitmap
* @param url
* @return bitmap
*/
public Bitmap getBitmapFromMem(String url) {
Bitmap bitmap = null;
// 先從硬引用緩存中獲取
synchronized (mLruCache) {
bitmap = mLruCache.get(url);
if(bitmap != null) {
// 找到該Bitmap之後,將其移到LinkedHashMap的最前面,保證它在LRU算法中將被最後刪除。
mLruCache.remove(url);
mLruCache.put(url, bitmap);
return bitmap;
}
}
// 再從軟引用緩存中獲取
synchronized (mSoftCache) {
SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);
if(bitmapReference != null) {
bitmap = bitmapReference.get();
if(bitmap != null) {
// 找到該Bitmap之後,將它移到硬引用緩存。並從軟引用緩存中刪除。
mLruCache.put(url, bitmap);
mSoftCache.remove(url);
return bitmap;
} else {
mSoftCache.remove(url);
}
}
}
return null;
}
/**
* 添加Bitmap到內存緩存
* @param url
* @param bitmap
*/
public void addBitmapToCache(String url, Bitmap bitmap) {
if(bitmap != null) {
synchronized (mLruCache) {
mLruCache.put(url, bitmap);
}
}
}
/**
* 清理軟引用緩存
*/
public void clearCache() {
mSoftCache.clear();
mSoftCache = null;
}
}
補充一點,由於4.0平臺以後對SoftReference類引用的對象調整了回收策略,所以該類中的軟引用緩存實際上沒什麼效果,可以去掉。2.3以前平臺建議保留。3磁盤緩存類(PanoDiskCache)
public class PanoDiskCache {
private static final String TAG = "PanoDiskCache";
// 文件緩存目錄
private static final String CACHE_DIR = "panoCache";
private static final String CACHE_FILE_SUFFIX = ".cache";
private static final int MB = 1024 * 1024;
private static final int CACHE_SIZE = 10; // 10M
private static final int SDCARD_CACHE_THRESHOLD = 10;
public PanoDiskCache() {
// 清理文件緩存
removeCache(getDiskCacheDir());
}
/**
* 從磁盤緩存中獲取Bitmap
* @param url
* @return
*/
public Bitmap getBitmapFromDisk(String url) {
String path = getDiskCacheDir() + File.separator + genCacheFileName(url);
File file = new File(path);
if(file.exists()) {
Bitmap bitmap = BitmapFactory.decodeFile(path);
if(bitmap == null) {
file.delete();
} else {
updateLastModified(path);
return bitmap;
}
}
return null;
}
/**
* 將Bitmap寫入文件緩存
* @param bitmap
* @param url
*/
public void addBitmapToCache(Bitmap bitmap, String url) {
if(bitmap == null) {
return;
}
// 判斷當前SDCard上的剩餘空間是否足夠用於文件緩存
if(SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {
return;
}
String fileName = genCacheFileName(url);
String dir = getDiskCacheDir();
File dirFile = new File(dir);
if(!dirFile.exists()) {
dirFile.mkdirs();
}
File file = new File(dir + File.separator + fileName);
try {
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
} catch (FileNotFoundException e) {
Log.e(TAG, "FileNotFoundException");
} catch (IOException e) {
Log.e(TAG, "IOException");
}
}
/**
* 清理文件緩存
* 當緩存文件總容量超過CACHE_SIZE或SDCard的剩餘空間小於SDCARD_CACHE_THRESHOLD時,將刪除40%最近沒有被使用的文件
* @param dirPath
* @return
*/
private boolean removeCache(String dirPath) {
File dir = new File(dirPath);
File[] files = dir.listFiles();
if(files == null || files.length == 0) {
return true;
}
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return false;
}
int dirSize = 0;
for (int i = 0; i < files.length; i++) {
if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {
dirSize += files[i].length();
}
}
if(dirSize > CACHE_SIZE * MB || SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {
int removeFactor = (int) (0.4 * files.length + 1);
Arrays.sort(files, new FileLastModifiedSort());
for (int i = 0; i < removeFactor; i++) {
if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {
files[i].delete();
}
}
}
if(calculateFreeSpaceOnSd() <= SDCARD_CACHE_THRESHOLD) {
return false;
}
return true;
}
/**
* 更新文件的最後修改時間
* @param path
*/
private void updateLastModified(String path) {
File file = new File(path);
long time = System.currentTimeMillis();
file.setLastModified(time);
}
/**
* 計算SDCard上的剩餘空間
* @return
*/
private int calculateFreeSpaceOnSd() {
StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;
return (int) sdFreeMB;
}
/**
* 生成統一的磁盤文件後綴便於維護
* 從URL中得到源文件名稱,併爲它追加緩存後綴名.cache
* @param url
* @return 文件存儲後的名稱
*/
private String genCacheFileName(String url) {
String[] strs = url.split(File.separator);
return strs[strs.length - 1] + CACHE_FILE_SUFFIX;
}
/**
* 獲取磁盤緩存目錄
* @return
*/
private String getDiskCacheDir() {
return getSDPath() + File.separator + CACHE_DIR;
}
/**
* 獲取SDCard目錄
* @return
*/
private String getSDPath() {
File sdDir = null;
// 判斷SDCard是否存在
boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
if(sdCardExist) {
// 獲取SDCard根目錄
sdDir = Environment.getExternalStorageDirectory();
}
if(sdDir != null) {
return sdDir.toString();
} else {
return "";
}
}
/**
* 根據文件最後修改時間進行排序
*/
private class FileLastModifiedSort implements Comparator<File> {
@Override
public int compare(File lhs, File rhs) {
if(lhs.lastModified() > rhs.lastModified()) {
return 1;
} else if(lhs.lastModified() == rhs.lastModified()) {
return 0;
} else {
return -1;
}
}
}
}
4圖片工具類(PanoUtils)
4.1從網絡上獲取圖片:downloadBitmap()
/**
* 從網絡上獲取Bitmap,並進行適屏和分辨率處理。
* @param context
* @param url
* @return
*/
public static Bitmap downloadBitmap(Context context, String url) {
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
try {
HttpResponse response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode != HttpStatus.SC_OK) {
Log.e(TAG, "Error " + statusCode + " while retrieving bitmap from " + url);
return null;
}
HttpEntity entity = response.getEntity();
if(entity != null) {
InputStream in = null;
try {
in = entity.getContent();
return scaleBitmap(context, readInputStream(in));
} finally {
if(in != null) {
in.close();
in = null;
}
entity.consumeContent();
}
}
} catch (IOException e) {
request.abort();
Log.e(TAG, "I/O error while retrieving bitmap from " + url, e);
} catch (IllegalStateException e) {
request.abort();
Log.e(TAG, "Incorrect URL: " + url);
} catch (Exception e) {
request.abort();
Log.e(TAG, "Error while retrieving bitmap from " + url, e);
} finally {
client.getConnectionManager().shutdown();
}
return null;
}
4.2從輸入流讀取字節數組,看起來是不是很眼熟啊!
public static byte[] readInputStream(InputStream in) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
return out.toByteArray();
}
4.3對下載的源圖片進行適屏處理,這也是必須的:)
/**
* 按使用設備屏幕和紋理尺寸適配Bitmap
* @param context
* @param in
* @return
*/
private static Bitmap scaleBitmap(Context context, byte[] data) {
WindowManager windowMgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
windowMgr.getDefaultDisplay().getMetrics(outMetrics);
int scrWidth = outMetrics.widthPixels;
int scrHeight = outMetrics.heightPixels;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
int imgWidth = options.outWidth;
int imgHeight = options.outHeight;
if(imgWidth > scrWidth || imgHeight > scrHeight) {
options.inSampleSize = calculateInSampleSize(options, scrWidth, scrHeight);
}
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
// 根據業務的需要,在此處還可以進一步做處理
...
return bitmap;
}
/**
* 計算Bitmap抽樣倍數
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始圖片寬高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 計算目標寬高與原始寬高的比值
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 選擇兩個比值中較小的作爲inSampleSize的值
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
if(inSampleSize < 1) {
inSampleSize = 1;
}
}
return inSampleSize;
}
5使用decodeByteArray()還是decodeStream()?
講到這裏,有童鞋可能會問我爲什麼使用BitmapFactory.decodeByteArray(data, 0, data.length, opts)來創建Bitmap,而非使用BitmapFactory.decodeStream(is, null, opts)。你這樣做不是要多寫一個靜態方法readInputStream()嗎?沒錯,decodeStream()確實是該使用情景下的首選方法,但是在有些情形下,它會導致圖片資源不能即時獲取,或者說圖片被它偷偷地緩存起來,交還給我們的時間有點長。但是延遲性是致命的,我們等不起。所以在這裏選用decodeByteArray()獲取,它直接從字節數組中獲取,貼近於底層IO、脫離平臺限制、使用起來風險更小。
6引入緩存機制後獲取圖片的方法
/**
* 加載Bitmap
* @param url
* @return
*/
private Bitmap loadBitmap(String url) {
// 從內存緩存中獲取,推薦在主UI線程中進行
Bitmap bitmap = memCache.getBitmapFromMem(url);
if(bitmap == null) {
// 從文件緩存中獲取,推薦在工作線程中進行
bitmap = diskCache.getBitmapFromDisk(url);
if(bitmap == null) {
// 從網絡上獲取,不用推薦了吧,地球人都知道~_~
bitmap = PanoUtils.downloadBitmap(this, url);
if(bitmap != null) {
diskCache.addBitmapToCache(bitmap, url);
memCache.addBitmapToCache(url, bitmap);
}
} else {
memCache.addBitmapToCache(url, bitmap);
}
}
return bitmap;
}
7工作線程池化管理
有關多線程的切換問題以及在UI線程中執行loadBitmap()方法無效的問題,請參見另一篇博文:使用嚴苛模式打破Android4.0以上平臺應用中UI主線程的“獨斷專行”。有關工作線程的處理方式,這裏推薦使用定製線程池的方式,核心代碼如下:
// 線程池初始容量
private static final int POOL_SIZE = 4;
private ExecutorService executorService;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 獲取當前使用設備的CPU個數
int cpuNums = Runtime.getRuntime().availableProcessors();
// 預開啓線程池數目
executorService = Executors.newFixedThreadPool(cpuNums * POOL_SIZE);
...
executorService.submit(new Runnable() {
// 此處執行一些耗時工作,不要涉及UI工作。如果遇到,直接轉交UI主線程
pano.setImage(loadBitmap(url));
});
...
}
我們知道,線程構造也是比較耗資源的。一定要對其進行有效的管理和維護。千萬不要隨意而行,一張圖片的工作線程不搭理也許沒什麼,當使用場景變爲ListView和GridView時,線程池化工作就顯得尤爲重要了。Android不是提供了AsyncTask嗎?爲什麼不用它?其實AsyncTask底層也是靠線程池支持的,它默認分配的線程數是128,是遠大於我們定製的executorService。