爲Volley編寫一個完整的圖片二級緩存擴展

package xxx.xxx.xxx.xxx;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.util.LruCache;
import android.widget.ImageView;

import com.android.volley.toolbox.ImageLoader;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class BitmapCache implements ImageLoader.ImageCache {

    private static final int DISK_CACHE_SIZE = 1024 *1024 * 50;
    private static final int DISK_CACHE_INDEX = 0;

    private LruCache<String, Bitmap> lruCache;
    private DiskLruCache diskLruCache;

    public BitmapCache(Context context) {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(context, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public Bitmap getBitmap(String key) {
        Bitmap bitmap = lruCache.get(key);
        if (bitmap == null) {
            try {
                DiskLruCache.Snapshot snapshot = diskLruCache.get(hashKeyForDisk(key));
                if (snapshot != null) {
                    FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                    FileDescriptor fileDescriptor = fileInputStream.getFD();
                    bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                    if (bitmap != null) {
                        lruCache.put(key, bitmap);
                    }
                    return bitmap;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    @Override
    public void putBitmap(String key, Bitmap bitmap) {
        lruCache.put(key, bitmap);
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(hashKeyForDisk(key));
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                if (bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
                diskLruCache.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getDiskCacheDir(Context context, String uniqueName) {
        final String cachePath;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return stats.getBlockSizeLong() * stats.getAvailableBlocksLong();
    }

    private String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xFF & b);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    public static String getCacheKey(String url, ImageView imageView) {
        return "#W" + imageView.getWidth()
                + "#H" + imageView.getHeight()
                + "#S" + imageView.getScaleType().ordinal()
                + url;
    }

    private 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 = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

}

至於我們爲什麼需要圖片緩存,以及什麼是內存緩存,什麼是磁盤緩存,什麼是Volley這些東西我就不想寫了,我附上幾篇郭神的博客,可以用作參考。


Android Volley完全解析(二),使用Volley加載網絡圖片

 Android高效加載大圖、多圖解決方案,有效避免程序OOM

 Android DiskLruCache完全解析,硬盤緩存的最佳方案


除了這些博客外,《Android開發藝術探索》一書第十二章對圖片緩存這一塊也講的比較詳細。

還有題外話就是以上的博客和書中所給的DiskLruCache源碼的下載地址,下載下來的源碼是不能直接編譯成功的,還需要使用者自己做一些修改,比較麻煩。但是Android Developer官網給出的源碼是直接可用的,我這裏直接給出地址(需要科學上網,大家都懂的):

https://developer.android.com/samples/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.html


然後簡單說一下我的代碼,如果使用的話,直接複製粘貼就行了,不過最上面類的包名自己要寫下,再一個就是如果DiskLruCache和本類如果不在一個包,還需要import一下。

本類實現了Volley的ImageCache接口,絕大部分實現細節都來自以上教程的整合。

重寫接口的getBitmap()方法,大致邏輯爲,首先從內存緩存中取出bitmap,如果沒有,即當返回null的時候,從磁盤緩存讀取,如果從硬盤成功讀取,根據LRU算法的近期最少使用的原則,我們應該吧從磁盤讀取的bitmap重新加入內存緩存。

重寫接口的putBitmap()方法,大致邏輯爲,拿到從網絡加載來的bitmap以後,先寫入內存緩存,再寫入磁盤緩存。與以上教程不同的地方爲,教程中是把網絡請求來的數據流直接寫入輸出流,但是由於我們這個是Volley的擴展,所以我們要考慮使用Volley的情況,NetworkImageView會自動壓縮bitmap,所以我們在使用的時候不必考慮壓縮的問題,其次,由於Volley已經做過處理,所以我們直接會從此回調方法中得到bitmap,因此就不是直接從網絡數據流寫入輸出流了,而是把bitmap寫入輸出流,這裏使用了bitmap的compress()方法。

關於緩存使用的鍵值問題:Volley的鍵值不僅包含url,還包含寬,高,ScaleType三個信息。原本的鍵值在內存緩存中使用時沒有任何問題的,但是使用在磁盤緩存中會出現問題,因爲URL中包含的 “ / ”這個斜槓符號會對存儲路徑造成影響,導致目錄出現問題,所以即使Volley有自己的鍵值生成的方法我們在使用磁盤緩存時仍需把原鍵MD5化。關於Volley的鍵是怎麼生成的,我在代碼中的最下面三個靜態方法中的第一個已經給出。後面兩個靜態方法是從Resource中使用圖片的時候進行壓縮的兩個工具方法,上面給出的博客中有介紹。

此外,bytesToHexString()方法中的for循環改爲了更有逼格的foreach循環(好吧,這沒什麼值得說的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章