傳送門 ☞ 輪子的專欄 ☞ 轉載請註明 ☞ http://blog.csdn.net/leverage_1229
html
上週爲360全景項目引入了圖片緩存模塊。由於是在Android4.0平臺以上運做,出於慣性,都會在設計以前查閱相關資料,儘可能避免拿一些之前2.3平臺積累的經驗來進行類比處理。開發文檔中有一個BitmapFun的示例,仔細拜讀了一下,雖然說圍繞着Bitmap的方方面面講得都很深刻,但感受很難引入到當前項目中去。java
如今的圖片服務提供者基本上都來源於網絡。對於應用平臺而言,訪問網絡屬於耗時操做。尤爲是在移動終端設備上,它的顯著表現爲系統的延遲時間變長、用戶交互性變差等。能夠想象,一個攜帶着這些問題的應用在市場上是很難與同類產品競爭的。
說明一下,本文借鑑了 Keegan小鋼和安卓巴士的處理模板,主要針對的是4.0以上平臺應用。2.3之前平臺執行效果未知,請斟酌使用或直接略過:),固然更歡迎您把測試結果告知筆者。
1、圖片加載流程
首先,咱們談談加載圖片的流程,項目中的該模塊處理流程以下:
1.在UI主線程中,從內存緩存中獲取圖片,找到後返回。找不到進入下一步;
2.在工做線程中,從磁盤緩存中獲取圖片,找到即返回並更新內存緩存。找不到進入下一步;
3.在工做線程中,從網絡中獲取圖片,找到即返回並同時更新內存緩存和磁盤緩存。找不到顯示默認以提示。 android
2、內存緩存類(PanoMemCache)算法
這裏使用Android提供的LruCache類,該類保存一個強引用來限制內容數量,每當Item被訪問的時候,此Item就會移動到隊列的頭部。當cache已滿的時候加入新的item時,在隊列尾部的item會被回收。數組
public class PanoMemoryCache { // LinkedHashMap初始容量 private static final int INITIAL_CAPACITY = 16; // LinkedHashMap加載因子 private static final int 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)
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; }
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(); }
3.對下載的源圖片進行適屏處理,這也是必須的:) ide
/** * 按使用設備屏幕和紋理尺寸適配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。