上週爲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
- 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) 算法
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.對下載的源圖片進行適屏處理,這也是必須的:) 網絡
- /**
- * 按使用設備屏幕和紋理尺寸適配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主線程的「專斷專行」。
有關工做線程的處理方式,這裏推薦使用定製線程池的方式,核心代碼以下: ide
- // 線程池初始容量
- 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。工具