设计一个简易图片缓存组件

788 阅读2分钟

在 UI 界面加载一张图片时很简单,然而如果需要加载多张较大的图像,事情就会变得更加复杂。在许多情况下(如 ListView、RecyclerView 或 ViewPager 等的组件),屏幕上的图片的总数伴随屏幕的滚动会大大增加,且基本上是无限的。为了使内存使用保持在稳定范围内,防止出现 OOM,这些组件会在子 iew 划出屏幕后,对其进行资源回收,并重新显示新出现的图片,垃圾回收机制会释放掉不再显示的图片的内存空间。但是这样频繁地处理图片的加载和回收不利于操作的流畅性,而内存或者磁盘的 Cache 就会帮助解决这个问题,实现快速加载已加载的图片。在缓存上,主要有两种级别的 Cache:LruCache 和 DiskLruCache,即内存缓存与磁盘缓存。我们就来借助内存缓存和磁盘缓存来实现一个简易的图片缓存框架。

概要设计

BitMapLruCache.java:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class BitMapLruCache {
    private static MemoryCacheUtils memoryCacheUtils;
    private static ExecutorService executor;
    
    private static final String TAG = "BitMapLruCache";

    private BitMapLruCache(){}

    /**
     * 外部私有缓存文件夹
     */
    private static File cacheDir;

    /**
     * 初始化BitMapLruCache,必须在App启动完成后调用!
     * @param context context
     */
    public static void init(Context context){
        cacheDir = context.getExternalCacheDir();
        memoryCacheUtils = new MemoryCacheUtils();
        executor = new ThreadPoolExecutor(
                4, 8,
                10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(128));
    }

    /**
     * 根据URL获取一张图片
     * @param url URL
     * @param imageView {@link ImageView}
     * @param listener {@link LoadBitMapListener} 加载BitMap的监听器
     */
    public static void getBitMap(String url, ImageView imageView, LoadBitMapListener listener) {
        // 先从内存找
        Bitmap fromMemory = memoryCacheUtils.getBitmapFromMemory(url);
        if(fromMemory != null){
            listener.success(fromMemory, imageView);
        }else {
            // 找不到就从磁盘找
            Bitmap bitmap = getBitMapFromDisk(url);
            if(bitmap != null){
                listener.success(bitmap, imageView);
                // 找到就保存到内存
                memoryCacheUtils.setBitmapToMemory(url, bitmap);
            }else{
                // 找不到从网络加载 & 保存到内存
                GetFromNetWorkTask task = new GetFromNetWorkTask(url, imageView, listener);
                executor.submit(task);
            }
        }
    }

    /**
     * 根据URL从磁盘加载BitMap
     * @param url 图片URL
     * @return bitmap {@link Bitmap}
     */
    private static Bitmap getBitMapFromDisk(String url) {
        File file = new File(cacheDir, calcMD5(url));
        if(file.exists()){
            try {
                return BitmapFactory.decodeStream(new FileInputStream(file));
            } catch (IOException e) {
                Log.e(TAG, "getBitMapFromDisk: ", e);
            }
        }
        return null;
    }

    /**
     * 保存BitMap到磁盘
     * @param url 图片URL
     * @param bitmap {@link Bitmap}
     */
    private static void saveBitMapToDisk(String url, Bitmap bitmap) {
        //url convert
        try {
            File file = new File(cacheDir, calcMD5(url));
            Log.i(TAG, "saveBitMapToDisk: file' path = " + file.getAbsolutePath());
            FileOutputStream out = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
        } catch (IOException e) {
            Log.e(TAG, "saveBitMapToDisk: ", e);
        }
    }

    /**
     * 加载BitMap过程中的监听器
     */
    public interface LoadBitMapListener{
        void success(Bitmap bitmap, ImageView imageView);
        void failed();
    }

    /**
     * 从网络加载的Task
     */
    static private class GetFromNetWorkTask implements Runnable {
        String urlStr;
        ImageView imageView;
        LoadBitMapListener listener;

        public GetFromNetWorkTask(String urlStr, ImageView imageView, LoadBitMapListener listener) {
            this.urlStr = urlStr;
            this.imageView = imageView;
            this.listener = listener;
        }

        @Override
        public void run() {
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                InputStream connInputStream = conn.getInputStream();
                Bitmap baseBitmap = BitmapFactory.decodeStream(connInputStream);
                // 比例压缩
                Bitmap bitmap = imageCompressL(baseBitmap);
                listener.success(bitmap, imageView);
                memoryCacheUtils.setBitmapToMemory(urlStr, bitmap);
                saveBitMapToDisk(urlStr, bitmap);
            } catch (IOException e) {
                listener.failed();
            }
        }
    }

    /**
     * 将URL转换为MD5
     * @param str URL
     * @return MD5
     */
    private static String calcMD5(String str) {
        if (str == null || str.length() == 0) {
            throw new IllegalArgumentException("String cannot be null or zero length");
        }
        StringBuilder hexString = new StringBuilder();
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[] hash = md.digest();
            for (byte b : hash) {
                if ((0xff & b) < 0x10) {
                    hexString.append("0").append(Integer.toHexString((0xFF & b)));
                } else {
                    hexString.append(Integer.toHexString(0xFF & b));
                }
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return hexString.toString();
    }


    /**
     * 计算Bitmap大小,如果超过64kb,则进行压缩
     * @param bitmap 源BitMap {@link Bitmap}
     * @return 压缩后的Bitmap {@link Bitmap}
     */
    private static Bitmap imageCompressL(Bitmap bitmap) {
        double targetWidth = Math.sqrt(64.00 * 1000);
        if (bitmap.getWidth() > targetWidth || bitmap.getHeight() > targetWidth) {
            // 创建操作图片用的matrix对象
            Matrix matrix = new Matrix();
            // 计算宽高缩放率
            float x = (float) Math.max(targetWidth / bitmap.getWidth(), targetWidth / bitmap.getHeight());
            // 缩放图片动作
            matrix.postScale(x, x);
            bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                    bitmap.getHeight(), matrix, true);
        }
        return bitmap;
    }

    /**
     * LRUCache操作工具类
     */
    private static class MemoryCacheUtils {
        private static final String TAG = "MemoryCacheUtils";
        private final LruCache<String, Bitmap> mMemoryCache;

        public MemoryCacheUtils(){
            // 得到手机最大允许内存的1/8,即超过指定内存,则开始回收
            long maxMemory = Runtime.getRuntime().maxMemory()/8;
            Log.i(TAG, "MemoryCacheUtils: maxMemory = " + maxMemory);
            // 需要传入允许的内存最大值,虚拟机默认内存24M
            mMemoryCache = new LruCache<String,Bitmap>((int) maxMemory){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getByteCount();
                }
            };
        }

        /**
         * 从内存中读图片
         * @param url 图片URL
         * @return {@link Bitmap}
         */
        public Bitmap getBitmapFromMemory(String url) {
            return mMemoryCache.get(url);
        }

        /**
         * 往内存中写图片
         * @param url 图片URL
         * @param bitmap BitMap
         */
        public void setBitmapToMemory(String url, Bitmap bitmap) {
            mMemoryCache.put(url,bitmap);
        }
    }
}

LRUCache本身就是基于LinkHashMap实现的,关于更多LRUCache的内容可以看我的另一篇博客:《LinkHashMap与LRU》

需要注意的是使用此组件需要外部存储的权限与网络访问权限(如果是Android6.0之后的设备还需要动态权限申请):

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />