轉 如何高效使用和管理Bitmap--圖片緩存管理模塊的設計與實現

上週爲360全景項目引入了圖片緩存模塊。由於是在Android4.0平臺以上運做,出於慣性,都會在設計以前查閱相關資料,儘可能避免拿一些之前2.3平臺積累的經驗來進行類比處理。開發文檔中有一個 BitmapFun的示例,仔細拜讀了一下,雖然說圍繞着Bitmap的方方面面講得都很深刻,但感受很難引入到當前項目中去。         如今的圖片服務提供者基本上都來源於網絡。對於應用平臺而言,訪問網絡屬於耗時操做。尤爲是在移動終端設備上,它的顯著表現爲系統的延遲時間變長、用戶交互性變差等。能夠想象,一個攜帶着這些問題的應用在市場上是很難與同類產品競爭的。 
        說明一下,本文借鑑了 Keegan小鋼和安卓巴士的處理模板,主要針對的是4.0以上平臺應用。2.3之前平臺執行效果未知,請斟酌使用或直接略過:),固然更歡迎您把測試結果告知筆者。 
1、圖片加載流程 
        首先,咱們談談加載圖片的流程,項目中的該模塊處理流程以下: 
1.在UI主線程中,從內存緩存中獲取圖片,找到後返回。找不到進入下一步; 
2.在工做線程中,從磁盤緩存中獲取圖片,找到即返回並更新內存緩存。找不到進入下一步; 
3.在工做線程中,從網絡中獲取圖片,找到即返回並同時更新內存緩存和磁盤緩存。找不到顯示默認以提示。 html

2、內存緩存類(PanoMemCache)java

        這裏使用Android提供的LruCache類,該類保存一個強引用來限制內容數量,每當Item被訪問的時候,此Item就會移動到隊列的頭部。當cache已滿的時候加入新的item時,在隊列尾部的item會被回收。android

[java]  view plain  copy  print  ?
  1. public class PanoMemoryCache {  
  2.   
  3.     // LinkedHashMap初始容量  
  4.     private static final int INITIAL_CAPACITY = 16;  
  5.     // LinkedHashMap加載因子  
  6.     private static final int LOAD_FACTOR = 0.75f;  
  7.     // LinkedHashMap排序模式  
  8.     private static final boolean ACCESS_ORDER = true;  
  9.   
  10.     // 軟引用緩存  
  11.     private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;  
  12.     // 硬引用緩存  
  13.     private static LruCache<String, Bitmap> mLruCache;  
  14.       
  15.     public PanoMemoryCache() {  
  16.     // 獲取單個進程可用內存的最大值  
  17.     // 方式一:使用ActivityManager服務(計量單位爲M)  
  18.         /*int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();*/  
  19.     // 方式二:使用Runtime類(計量單位爲Byte)  
  20.         final int memClass = (int) Runtime.getRuntime().maxMemory();  
  21.         // 設置爲可用內存的1/4(按Byte計算)  
  22.         final int cacheSize = memClass / 4;  
  23.         mLruCache = new LruCache<String, Bitmap>(cacheSize) {  
  24.             @Override  
  25.             protected int sizeOf(String key, Bitmap value) {  
  26.                 if(value != null) {  
  27.                     // 計算存儲bitmap所佔用的字節數  
  28.                     return value.getRowBytes() * value.getHeight();  
  29.                 } else {  
  30.                     return 0;  
  31.                 }  
  32.             }  
  33.               
  34.             @Override  
  35.             protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {  
  36.                 if(oldValue != null) {  
  37.                     // 當硬引用緩存容量已滿時,會使用LRU算法將最近沒有被使用的圖片轉入軟引用緩存  
  38.                     mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));  
  39.                 }  
  40.             }  
  41.         };  
  42.           
  43.     /* 
  44.     * 第一個參數:初始容量(默認16) 
  45.     * 第二個參數:加載因子(默認0.75) 
  46.     * 第三個參數:排序模式(true:按訪問次數排序;false:按插入順序排序) 
  47.     */  
  48.         mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(INITIAL_CAPACITY, LOAD_FACTOR, ACCESS_ORDER) {  
  49.             private static final long serialVersionUID = 7237325113220820312L;  
  50.             @Override  
  51.             protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {  
  52.                 if(size() > SOFT_CACHE_SIZE) {  
  53.                     return true;  
  54.                 }  
  55.                 return false;  
  56.             }  
  57.         };  
  58.     }  
  59.       
  60.     /** 
  61.      * 從緩存中獲取Bitmap 
  62.      * @param url 
  63.      * @return bitmap 
  64.      */  
  65.     public Bitmap getBitmapFromMem(String url) {  
  66.         Bitmap bitmap = null;  
  67.         // 先從硬引用緩存中獲取  
  68.         synchronized (mLruCache) {  
  69.             bitmap = mLruCache.get(url);  
  70.             if(bitmap != null) {  
  71.                 // 找到該Bitmap以後,將其移到LinkedHashMap的最前面,保證它在LRU算法中將被最後刪除。  
  72.                 mLruCache.remove(url);  
  73.                 mLruCache.put(url, bitmap);  
  74.                 return bitmap;  
  75.             }  
  76.         }  
  77.   
  78.   
  79.         // 再從軟引用緩存中獲取  
  80.         synchronized (mSoftCache) {  
  81.             SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);  
  82.             if(bitmapReference != null) {  
  83.                 bitmap = bitmapReference.get();  
  84.                 if(bitmap != null) {  
  85.                     // 找到該Bitmap以後,將它移到硬引用緩存。並從軟引用緩存中刪除。  
  86.                     mLruCache.put(url, bitmap);  
  87.                     mSoftCache.remove(url);  
  88.                     return bitmap;  
  89.                 } else {  
  90.                     mSoftCache.remove(url);  
  91.                 }  
  92.             }  
  93.         }  
  94.         return null;  
  95.     }  
  96.       
  97.     /** 
  98.      * 添加Bitmap到內存緩存 
  99.      * @param url 
  100.      * @param bitmap 
  101.      */  
  102.     public void addBitmapToCache(String url, Bitmap bitmap) {  
  103.         if(bitmap != null) {  
  104.             synchronized (mLruCache) {  
  105.               mLruCache.put(url, bitmap);    
  106.             }  
  107.         }  
  108.     }  
  109.       
  110.     /** 
  111.      * 清理軟引用緩存 
  112.      */  
  113.     public void clearCache() {  
  114.         mSoftCache.clear();  
  115.     mSoftCache = null;  
  116.     }  
  117. }  

        補充一點,因爲4.0平臺之後對SoftReference類引用的對象調整了回收策略,因此該類中的軟引用緩存實際上沒什麼效果,能夠去掉。2.3之前平臺建議保留。 
3、磁盤緩存類(PanoDiskCache) 算法

[java]  view plain  copy  print  ?
  1. public class PanoDiskCache {  
  2.       
  3.     private static final String TAG = "PanoDiskCache";  
  4.   
  5.     // 文件緩存目錄  
  6.     private static final String CACHE_DIR = "panoCache";  
  7.     private static final String CACHE_FILE_SUFFIX = ".cache";  
  8.       
  9.     private static final int MB = 1024 * 1024;  
  10.     private static final int CACHE_SIZE = 10; // 10M  
  11.     private static final int SDCARD_CACHE_THRESHOLD = 10;  
  12.       
  13.     public PanoDiskCache() {  
  14.         // 清理文件緩存  
  15.         removeCache(getDiskCacheDir());  
  16.     }  
  17.       
  18.     /** 
  19.      * 從磁盤緩存中獲取Bitmap 
  20.      * @param url 
  21.      * @return  
  22.      */  
  23.     public Bitmap getBitmapFromDisk(String url) {  
  24.         String path = getDiskCacheDir() + File.separator + genCacheFileName(url);  
  25.         File file = new File(path);  
  26.         if(file.exists()) {  
  27.             Bitmap bitmap = BitmapFactory.decodeFile(path);  
  28.             if(bitmap == null) {  
  29.                 file.delete();  
  30.             } else {  
  31.                 updateLastModified(path);  
  32.                 return bitmap;  
  33.             }  
  34.         }  
  35.         return null;  
  36.     }  
  37.       
  38.     /** 
  39.      * 將Bitmap寫入文件緩存 
  40.      * @param bitmap 
  41.      * @param url 
  42.      */  
  43.     public void addBitmapToCache(Bitmap bitmap, String url) {  
  44.         if(bitmap == null) {  
  45.             return;  
  46.         }  
  47.         // 判斷當前SDCard上的剩餘空間是否足夠用於文件緩存  
  48.         if(SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {  
  49.             return;  
  50.         }  
  51.         String fileName = genCacheFileName(url);  
  52.         String dir = getDiskCacheDir();  
  53.         File dirFile = new File(dir);  
  54.         if(!dirFile.exists()) {  
  55.             dirFile.mkdirs();  
  56.         }  
  57.         File file = new File(dir + File.separator + fileName);  
  58.         try {  
  59.             file.createNewFile();  
  60.             FileOutputStream out = new FileOutputStream(file);  
  61.             bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);  
  62.             out.flush();  
  63.             out.close();  
  64.         } catch (FileNotFoundException e) {  
  65.             Log.e(TAG, "FileNotFoundException");  
  66.         } catch (IOException e) {  
  67.             Log.e(TAG, "IOException");  
  68.         }  
  69.     }  
  70.       
  71.     /** 
  72.      * 清理文件緩存 
  73.      * 當緩存文件總容量超過CACHE_SIZE或SDCard的剩餘空間小於SDCARD_CACHE_THRESHOLD時,將刪除40%最近沒有被使用的文件 
  74.      * @param dirPath 
  75.      * @return  
  76.      */  
  77.     private boolean removeCache(String dirPath) {  
  78.         File dir = new File(dirPath);  
  79.         File[] files = dir.listFiles();  
  80.         if(files == null || files.length == 0) {  
  81.             return true;  
  82.         }  
  83.         if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {  
  84.             return false;  
  85.         }  
  86.           
  87.         int dirSize = 0;  
  88.         for (int i = 0; i < files.length; i++) {  
  89.             if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {  
  90.                 dirSize += files[i].length();  
  91.             }  
  92.         }  
  93.         if(dirSize > CACHE_SIZE * MB || SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {  
  94.             int removeFactor = (int) (0.4 * files.length + 1);  
  95.             Arrays.sort(files, new FileLastModifiedSort());  
  96.             for (int i = 0; i < removeFactor; i++) {  
  97.                 if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {  
  98.                     files[i].delete();  
  99.                 }  
  100.             }  
  101.         }  
  102.           
  103.         if(calculateFreeSpaceOnSd() <= SDCARD_CACHE_THRESHOLD) {  
  104.             return false;  
  105.         }  
  106.         return true;  
  107.     }  
  108.       
  109.     /** 
  110.      * 更新文件的最後修改時間 
  111.      * @param path 
  112.      */  
  113.     private void updateLastModified(String path) {  
  114.         File file = new File(path);  
  115.         long time = System.currentTimeMillis();  
  116.         file.setLastModified(time);  
  117.     }  
  118.       
  119.     /** 
  120.      * 計算SDCard上的剩餘空間 
  121.      * @return  
  122.      */  
  123.     private int calculateFreeSpaceOnSd() {  
  124.         StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());  
  125.         double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;  
  126.         return (int) sdFreeMB;  
  127.     }  
  128.   
  129.   
  130.     /** 
  131.      * 生成統一的磁盤文件後綴便於維護 
  132.      * 從URL中獲得源文件名稱,併爲它追加緩存後綴名.cache 
  133.      * @param url 
  134.      * @return 文件存儲後的名稱 
  135.      */  
  136.     private String genCacheFileName(String url) {  
  137.         String[] strs = url.split(File.separator);  
  138.         return strs[strs.length - 1] + CACHE_FILE_SUFFIX;  
  139.     }  
  140.       
  141.     /** 
  142.      * 獲取磁盤緩存目錄 
  143.      * @return  
  144.      */  
  145.     private String getDiskCacheDir() {  
  146.         return getSDPath() + File.separator + CACHE_DIR;  
  147.     }  
  148.       
  149.     /** 
  150.      * 獲取SDCard目錄 
  151.      * @return  
  152.      */  
  153.     private String getSDPath() {  
  154.         File sdDir = null;  
  155.         // 判斷SDCard是否存在  
  156.         boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);  
  157.         if(sdCardExist) {  
  158.             // 獲取SDCard根目錄  
  159.             sdDir = Environment.getExternalStorageDirectory();  
  160.         }  
  161.         if(sdDir != null) {  
  162.             return sdDir.toString();  
  163.         } else {  
  164.             return "";  
  165.         }  
  166.     }  
  167.       
  168.     /** 
  169.      * 根據文件最後修改時間進行排序 
  170.      */  
  171.     private class FileLastModifiedSort implements Comparator<File> {  
  172.         @Override  
  173.         public int compare(File lhs, File rhs) {  
  174.             if(lhs.lastModified() > rhs.lastModified()) {  
  175.                 return 1;  
  176.             } else if(lhs.lastModified() == rhs.lastModified()) {  
  177.                 return 0;  
  178.             } else {  
  179.                 return -1;  
  180.             }  
  181.         }  
  182.     }  
  183. }  

4、圖片工具類(PanoUtils) 
1.從網絡上獲取圖片:downloadBitmap() 數組

[java]  view plain  copy  print  ?
  1. /** 
  2.     * 從網絡上獲取Bitmap,並進行適屏和分辨率處理。 
  3.     * @param context 
  4.     * @param url 
  5.     * @return  
  6.     */  
  7.    public static Bitmap downloadBitmap(Context context, String url) {  
  8.        HttpClient client = new DefaultHttpClient();  
  9.        HttpGet request = new HttpGet(url);  
  10.          
  11.        try {  
  12.            HttpResponse response = client.execute(request);  
  13.            int statusCode = response.getStatusLine().getStatusCode();  
  14.            if(statusCode != HttpStatus.SC_OK) {  
  15.                Log.e(TAG, "Error " + statusCode + " while retrieving bitmap from " + url);  
  16.                return null;  
  17.            }  
  18.              
  19.            HttpEntity entity = response.getEntity();  
  20.            if(entity != null) {  
  21.                InputStream in = null;  
  22.                try {  
  23.                    in = entity.getContent();  
  24.                    return scaleBitmap(context, readInputStream(in));  
  25.                } finally {  
  26.                    if(in != null) {  
  27.                        in.close();  
  28.                        in = null;  
  29.                    }  
  30.                    entity.consumeContent();  
  31.                }  
  32.            }  
  33.        } catch (IOException e) {  
  34.            request.abort();  
  35.            Log.e(TAG, "I/O error while retrieving bitmap from " + url, e);  
  36.        } catch (IllegalStateException e) {  
  37.            request.abort();  
  38.            Log.e(TAG, "Incorrect URL: " + url);  
  39.        } catch (Exception e) {  
  40.            request.abort();  
  41.            Log.e(TAG, "Error while retrieving bitmap from " + url, e);  
  42.        } finally {  
  43.            client.getConnectionManager().shutdown();  
  44.        }  
  45.        return null;  
  46.    }     

2.從輸入流讀取字節數組,看起來是否是很眼熟啊! 緩存

[java]  view plain  copy  print  ?
  1. public static byte[] readInputStream(InputStream in) throws Exception {  
  2.         ByteArrayOutputStream out = new ByteArrayOutputStream();  
  3.         byte[] buffer = new byte[1024];  
  4.         int len = 0;  
  5.         while((len = in.read(buffer)) != -1) {  
  6.             out.write(buffer, 0, len);  
  7.         }  
  8.         in.close();  
  9.         return out.toByteArray();  
  10.     }      

3.對下載的源圖片進行適屏處理,這也是必須的:) 網絡

[java]  view plain  copy  print  ?
  1. /** 
  2.      * 按使用設備屏幕和紋理尺寸適配Bitmap 
  3.      * @param context 
  4.      * @param in 
  5.      * @return  
  6.      */  
  7.     private static Bitmap scaleBitmap(Context context, byte[] data) {  
  8.           
  9.         WindowManager windowMgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
  10.         DisplayMetrics outMetrics = new DisplayMetrics();  
  11.         windowMgr.getDefaultDisplay().getMetrics(outMetrics);  
  12.         int scrWidth = outMetrics.widthPixels;  
  13.         int scrHeight = outMetrics.heightPixels;  
  14.           
  15.         BitmapFactory.Options options = new BitmapFactory.Options();  
  16.         options.inJustDecodeBounds = true;  
  17.         Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);  
  18.         int imgWidth = options.outWidth;  
  19.         int imgHeight = options.outHeight;  
  20.           
  21.         if(imgWidth > scrWidth || imgHeight > scrHeight) {  
  22.             options.inSampleSize = calculateInSampleSize(options, scrWidth, scrHeight);  
  23.         }  
  24.         options.inJustDecodeBounds = false;  
  25.         bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);  
  26.           
  27.         // 根據業務的須要,在此處還能夠進一步作處理  
  28.         ...  
  29.   
  30.         return bitmap;  
  31.     }  
  32.       
  33.     /** 
  34.      * 計算Bitmap抽樣倍數 
  35.      * @param options 
  36.      * @param reqWidth 
  37.      * @param reqHeight 
  38.      * @return  
  39.      */  
  40.     public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {  
  41.         // 原始圖片寬高  
  42.         final int height = options.outHeight;  
  43.         final int width = options.outWidth;  
  44.         int inSampleSize = 1;  
  45.       
  46.         if (height > reqHeight || width > reqWidth) {  
  47.       
  48.             // 計算目標寬高與原始寬高的比值  
  49.             final int heightRatio = Math.round((float) height / (float) reqHeight);  
  50.             final int widthRatio = Math.round((float) width / (float) reqWidth);  
  51.       
  52.             // 選擇兩個比值中較小的做爲inSampleSize的值  
  53.             inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
  54.             if(inSampleSize < 1) {  
  55.                 inSampleSize = 1;  
  56.             }  
  57.         }  
  58.   
  59.         return inSampleSize;  
  60.     }  

5、使用decodeByteArray()仍是decodeStream()? 
        講到這裏,有童鞋可能會問我爲何使用BitmapFactory.decodeByteArray(data, 0, data.length, opts)來建立Bitmap,而非使用BitmapFactory.decodeStream(is, null, opts)。你這樣作不是要多寫一個靜態方法readInputStream()嗎? 
        沒錯,decodeStream()確實是該使用情景下的首選方法,可是在有些情形下,它會致使圖片資源不能即時獲取,或者說圖片被它偷偷地緩存起來,交 還給咱們的時間有點長。可是延遲性是致命的,咱們等不起。因此在這裏選用decodeByteArray()獲取,它直接從字節數組中獲取,貼近於底層 IO、脫離平臺限制、使用起來風險更小。 
6、引入緩存機制後獲取圖片的方法 多線程

[java]  view plain  copy  print  ?
  1. /** 
  2.      * 加載Bitmap 
  3.      * @param url 
  4.      * @return  
  5.      */  
  6.     private Bitmap loadBitmap(String url) {  
  7.         // 從內存緩存中獲取,推薦在主UI線程中進行  
  8.         Bitmap bitmap = memCache.getBitmapFromMem(url);  
  9.         if(bitmap == null) {  
  10.             // 從文件緩存中獲取,推薦在工做線程中進行  
  11.             bitmap = diskCache.getBitmapFromDisk(url);  
  12.             if(bitmap == null) {  
  13.                 // 從網絡上獲取,不用推薦了吧,地球人都知道~_~  
  14.                 bitmap = PanoUtils.downloadBitmap(this, url);  
  15.                 if(bitmap != null) {  
  16.                     diskCache.addBitmapToCache(bitmap, url);  
  17.                     memCache.addBitmapToCache(url, bitmap);  
  18.                 }  
  19.             } else {  
  20.                 memCache.addBitmapToCache(url, bitmap);  
  21.             }  
  22.         }  
  23.         return bitmap;  
  24.     }  

7、工做線程池化 
        有關多線程的切換問題以及在UI線程中執行loadBitmap()方法無效的問題,請參見另外一篇博文: 使用嚴苛模式打破Android4.0以上平臺應用中UI主線程的「專斷專行」。 
有關工做線程的處理方式,這裏推薦使用定製線程池的方式,核心代碼以下: ide

[java]  view plain  copy  print  ?
  1. // 線程池初始容量  
  2. private static final int POOL_SIZE = 4;  
  3. private ExecutorService executorService;  
  4. @Override  
  5. public void onCreate(Bundle savedInstanceState) {  
  6.     super.onCreate(savedInstanceState);  
  7.   
  8.     // 獲取當前使用設備的CPU個數  
  9.     int cpuNums = Runtime.getRuntime().availableProcessors();  
  10.     // 預開啓線程池數目  
  11.     executorService = Executors.newFixedThreadPool(cpuNums * POOL_SIZE);  
  12.   
  13.     ...  
  14.     executorService.submit(new Runnable() {  
  15.         // 此處執行一些耗時工做,不要涉及UI工做。若是遇到,直接轉交UI主線程  
  16.         pano.setImage(loadBitmap(url));  
  17.     });  
  18.     ...  
  19.   
  20. }  

        咱們知道,線程構造也是比較耗資源的。必定要對其進行有效的管理和維護。千萬不要隨意而行,一張圖片的工做線程不搭理也許沒什麼,當使用場景變爲 ListView和GridView時,線程池化工做就顯得尤其重要了。Android不是提供了AsyncTask嗎?爲何不用它?其實 AsyncTask底層也是靠線程池支持的,它默認分配的線程數是128,是遠大於咱們定製的executorService。工具

相關文章
相關標籤/搜索