流行网络库第(三)篇---动手写图片加载的三级缓存

版权声明:本文为LooperJing原创文章,转载请注明出处!

有很多第三方网络库都有三级缓存机制,比如BitmapUtil,ImageLoader等,今天来谈一谈是怎么实现的,首先三级指的是内存,文件,网络三级,严格来说网络不能算是缓存,但是更完整,常常把网络加在里面。OK下面开始写代码了。

对于图片的下载逻辑,我们使用AsyncTask,在这个ImageLoadTask先把总体的框架给写出来,暂时先不管内存缓存,磁盘缓存,与网络请求逻辑怎么实现。

public class ImageLoadTask extends AsyncTask {

    //使用弱引用包裹ImageView,支持GC,释放之后,不会在设置图片。支持一部分的释放操作:Activity结束,ListView销毁
    private final WeakReference imageViewWeakReference;
    private String url = null;

    public ImageLoadTask(ImageView imageView) {
        this.imageViewWeakReference = new WeakReference(imageView);
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        Bitmap ret = null;

        if (params != null && params.length > 0) {
            url = params[0];
            int picWidth = 0;
            int picHeight = 0;
            int argc = params.length;
         // 可选参数的执行方式;
            try {
                switch (argc){
                    case 2:
                        //url + width
                        picWidth = Integer.parseInt(params[1]);
                        picHeight = picWidth;
                        break;
                    case 3:
                        //                    url + width + height
                        picWidth = Integer.parseInt(params[1]);
                        picHeight = Integer.parseInt(params[2]);
                        break;
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
                picWidth = 0;
                picHeight = 0;
            }
            //0、先在内存中找
            ImageCache imageCache = ImageCache.getOurInstance();
            Bitmap bitmap = imageCache.get(url);
            if (bitmap == null) {
                //1.文件缓存,是否存在
                FileCache instance = FileCache.getInstance();
                byte[] bytes = instance.load(url);
                if (bytes == null) {
                    //文件不存在,需要从网上下载
                    bytes = HttpUtil.doGet(url);
                    //存文件缓存
                    FileCache.getInstance().save(url, bytes);
                }
                //文件存在,直接解码
                //ret = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                //TODO 检查图片是否过大,过大就缩小,节约内存
                ret = loadScaledBitmap(bytes, picHeight, picWidth);
                imageCache.put(url, ret);
                bytes = null;
            } else {
                ret = bitmap;
            }
        }
        return ret;
    }

    /**
     * 针对图片数据进行二次采样
     *
     * @param data     实际数据
     * @param toHeigth
     * @param toWidth
     * @return
     */
    private static Bitmap loadScaledBitmap(byte[] data, int toHeigth, int toWidth) {
        Bitmap ret = null;
        //1、获取尺寸
        //OPtions用来进行解码的配置,对象内部的变量会传递给底层解码,
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;//告诉解码器,只获取尺寸信息,不解析成Bitmap,不占用内存

        BitmapFactory.decodeByteArray(data, 0, data.length, options);

        int outHeight = options.outHeight;
        int outWidth = options.outWidth;

        //2缩小图片(使用Options进行缩小的采样)
        options.inJustDecodeBounds = false;//设置成false,代表正式解码图片,会返回Bitmap

        options.inSampleSize = calculateInSampleSize(options, toHeigth, toWidth);//图片采样比率,采样比率要求大于等于1,功能是:当设置成2的时候,代表宽度变为二分之一,高度变为二分之一,所以总共变为原始的图片的四分之一

        ret = BitmapFactory.decodeByteArray(data, 0, data.length, options);

        return ret;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (bitmap != null) {
            ImageView imageView = imageViewWeakReference.get();
            //看看内部对象有没有被释放
            if (imageView != null) {
                Object tag = imageView.getTag();
                if (tag != null) {
                    if (tag instanceof String) {
                        String str = (String) tag;
                        if (url.equals(str)) {
                            //TODO 检测是否需要tag处理错位
                            imageView.setImageBitmap(bitmap);
                        }
                    }
                }
            }
        }
    }


    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (reqWidth > 0 && reqHeight > 0) {
            if (height >reqHeight || width > reqWidth) {
                final int halfHeight = height / 2;
                final int halfWidth = width / 2;

                // Calculate the largest inSampleSize value that is a power of 2 and keeps both
                // height and width larger than the requested height and width.
                while ((halfHeight / inSampleSize) >= reqHeight
                        && (halfWidth / inSampleSize) >= reqWidth) {
                    inSampleSize *= 2;
                }
            }
        }
        return inSampleSize;
    }
}

这个Task中有图片的压缩逻辑,这个不是三级缓存的重点部分,只是为了性能优化使用。我们首先在内存缓存(ImageCache)中找,找不到去文件缓存(FileCache)中找,如果找到把它加入到内存缓存(ImageCache),如果找不到,就去网络请求,并且将请求之后的结果也放在文件缓存中(FileCache)。

现在看内存缓存怎么写,内存缓存的查找策略是:先从强引用缓存中查找,如果没有再从软引用缓存中查找, 如果在软引用缓存中找到了,就把它移入强引用缓存;如果强引用缓存满了,就会根据Lru算法把某些图片移入软引用缓存,如果软引用缓存也满了, 最早的软引用就会被删除。对JAVA四大引用还不熟悉的朋友,参考Android性能优化第(一)篇---基本概念。这里需要用到LRU算法,存储URL对应于Bitmap的映射。Lru:Least Recently Used 近期最少使用算法,是一种页面置换算法,其思想是在缓存的页面数目固定的情况下,那些最近使用次数最少的页面将被移出,对于我们的内存缓存来说,强引用缓存大小固定为4M,如果当缓存的图片大于4M的时候, 有些图片就会被从强引用缓存中删除,哪些图片会被删除呢,就是那些近期使用次数最少的图片。

public class   ImageCache {
    private static ImageCache ourInstance = null;

    /**
     * 采用LRU算法,存储URL对应于Bitmap的映射,这个对象允许设置存储对象占用的最大的内存空间,或者是条目的最大个数
     */
    private final LruCache cache;//强引用缓存

    private final HashMap> hashCahce;//软引用缓存

    private ImageCache() {
        //最多存30张图片
        cache = new LruCache(30);
        hashCahce = new LinkedHashMap>();
    }

    public static ImageCache getOurInstance() {
        if (ourInstance == null) {
            ourInstance = new ImageCache();
        }
        return ourInstance;
    }


    public Bitmap get(String url) {
        Bitmap ret = null;
        if (url != null) {
            ret = cache.get(url);
            if (ret == null) {
                //如果LRUcache没有,那就看看hashCache
                SoftReference reference = hashCahce.get(url);
                if (reference != null) {
                    ret = reference.get();//获取引用的对象
                    if (ret == null) {
                        hashCahce.remove(url);
                    } else {
                        //如果hashCache中有,LRU没有
                        cache.put(url, ret);
                    }
                }
            }
        }
        return ret;
    }

    public void put(String url, Bitmap bitmap) {
        if (url != null && bitmap != null) {
            cache.put(url, bitmap);
            hashCahce.put(url, new SoftReference(bitmap));
        }
    }
}

在看文件缓存怎么去写,关于文件缓存一个注意点就是如何保证每张图片的文件名称不重复,这里使用了MD5, 采用MD5算法,给一个URL,只要URL不一样,算出来一个数,永远不会重复。

public class FileCache {
    private static FileCache ourInstance;

    private Context context;

    private FileCache(Context context) {
        this.context = context;
    }

    /**
     * 利用Context来初始化FileCache单例对象
     *
     * @param context
     * @return
     */
    public static FileCache createInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new FileCache(context);
        }
         return ourInstance;
    }

    /**
     * 获取单例对象,这个方法调用之前,必须经过createInstace创建实例
     *
     * @return
     */
    public static FileCache getInstance() {
        if (ourInstance == null) {
            throw new IllegalStateException("please invoke createIstance(COntext) before this method");
        }
        return ourInstance;
    }

    /**
     * 采用MD5算法----消息摘要,给一个东西,算出来一个数,永远不会重复
     * 将网址映射成唯一的文件名
     *
     * @param url
     * @return
     */
    private static String mapFile(String url) {
        String ret = null;
        try {
            //消息摘要工具
            MessageDigest digest = MessageDigest.getInstance("MD5");
            //计算出消息摘要
            byte[] data = digest.digest(url.getBytes());
            //TODO 这个是错误的
            //ret=new String(data);
            //TODO 应该Hex编码,将字节数组的每一个字节利用16进制的形式转化成字符串
            ret = EncryptUtil.toHex(data);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return ret;
    }


    /**
     * 获取缓存文件夹,支持外部存储和内部存储
     *
     * @return
     */
    private File getCacheFolder() {
        File ret = null;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            ret = context.getExternalCacheDir();
        } else {
            ret = context.getCacheDir();
        }
        return ret;
    }

    /**
     * 加载缓存数据
     *
     * @param url
     * @return
     */

    public byte[] load(String url) {
        byte[] ret = null;
        if (url != null) {
            File folder = getCacheFolder();
            String fileName = mapFile(url);
            //文件名称由MD5生成
            File f = new File(folder, fileName);
            if (f.exists() && f.canRead()) {
                FileInputStream fin = null;
                try {
                    fin = new FileInputStream(f);
                    ret = StreamUtil.readStream(fin);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } finally {
                    StreamUtil.close(fin);
                }
            }
        }
        return ret;
    }


    public void save(String url, byte[] data) {
        if (url != null && data != null && data.length > 0) {
            File folder = getCacheFolder();
            String fileName = mapFile(url);
            File f = new File(folder, fileName);
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(f);
                fos.write(data);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                StreamUtil.close(fos);
            }
        }
    }
}
public final class EncryptUtil {
    public static String toHex(byte data[]) {
        StringBuilder ret = new StringBuilder();
        if (data != null) {
            int h, l;
            char ch, cl;
            for (byte b : data) {
                h = (b >> 4) & 0x0f;//高四位
                l = b & 0x0f;//低四位
                if (h > 9) {
                    ch = (char) ('A' + (h - 10));
                } else {
                    ch = (char) ('0' + h);
                }
                if (l > 9) {
                    cl = (char) ('A' + (l - 10));
                } else {
                    cl = (char) ('0' + l);
                }
                ret.append(ch).append(cl);
            }
        }
        return ret.toString();
    }
}

三级缓存写好了,最后在Adapter中使用如下。

if (url != null) {
      ImageLoadTask task = new ImageLoadTask(imageView);
      task.execute(url, "60", "60");//60是宽高
 }

Please accept mybest wishes for your happiness and success !

你可能感兴趣的:(流行网络库第(三)篇---动手写图片加载的三级缓存)