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

傳送門 ☞ 輪子的專欄 ☞ 轉載請註明 ☞ 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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章