图片三级缓存工具类(基与LruCache和 DiskLruCache)

基与LruCache(内存缓存)和 DiskLruCache(硬盘缓存)的图片三级缓存工具类


转载请注明出处 http://blog.csdn.net/fight_0513/article/details/76915997

1、什么是三级缓存

  • 1.网络缓存, 不优先加载, 速度慢,浪费流量
  • 2.本地缓存, 次优先加载, 速度较快(DiskLruCache 第三方编写Google认证)
  • 3.内存缓存, 优先加载, 速度最快 (LruCache Google官方推荐)

2、缓存流程

  • 1.图片为第一次加载时,先访问网络加载图片并同时进入内存缓存,并保存在硬盘
  • 2.如果图片以前加载过,首先去内存中查找是否有改图片如果有就加载,没有的话继续去硬盘中查找,如果硬盘中的缓存被清除了,就开启异步任务去网络加载

3、具体实现及代码

  • 如果需要研究DiskLruCache 可以去看看弘扬大神(http://blog.csdn.net/lmj623565791/article/details/47251585)对其源码的解析,以及郭霖大神(http://blog.csdn.net/guolin_blog/article/details/28863651)对DisLruCachede 使用的完全解析
    工具类的编辑也是大量参考了郭霖大神的博客

    首先会跟大家简单介绍 DiskLruCache的使用

  • DiskLruCache可以自由地进行设定数据保存的位置,通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/应用包名/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。(图片是以十六进制的文件存储的)
  • 转为16进制的图片
  • 磁盘内存的建立
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

参数说明:directory 磁盘缓存位置;appVersion APP版本号;valueCount 指定同一个key可以对应多少个缓存文件,基本都是传1;maxSize 指定最多可以缓存多少字节的数据。
需要注意的是:每当版本号改变,缓存路径下存储的所有数据都会被清除掉。journal文件中记录磁盘缓存的读取记录
Journal文件解读:第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行一下的数据:有个前缀开始,后面紧跟着缓存图片的key,后缀就是图片的字节数。
DIRTY前缀,通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
READ前缀除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。
值得注意的是,在用户存取操作达到2000次的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内

  • 磁盘缓存的写入&读取
 DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                // 存入磁盘
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                   //downloadUrlToStream(String urlString, OutputStream outputStream)下载图片的方法
                    if (downloadUrlToStream(imageUrl, outputStream)) {
                        editor.commit();//这样就存入磁盘了
                    } else {
                        editor.abort();//中断写入
                    }
                    //mDiskLruCache.flush();  flush之后才会被记录到Journal文件中  
                }

注意:方法newOutputStream(int index)有参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

 FileDescriptor fileDescriptor = null;
 FileInputStream fileInputStream = null;
//读取磁盘缓存
 DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if (snapShot != null) {
                    fileInputStream = (FileInputStream) snapShot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                //将缓存数据解析成Bitmap对象
                Bitmap bitmap = null;
                if (imageView != null || view != null) {
                    if (fileDescriptor != null) {
                    //据说decodeFileDescriptor 解析速度会比较快
                        bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                    }
                }

其它API

  • close()
    这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
  • delete()
    将所有的缓存数据全部删除,如需手动清理缓存功能,只需要调用一下DiskLruCache的delete()方法就可以实现了。
  • size()
    返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。
  • flush
    用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
  • remove(String key)
    用于删除 传入的参数key对应的数据。

接下来在简单说说 LRUCache

  • 简介
    LRU是Least Recently Used的缩写,近期最少使用算法。
    它内部采用LinkedHashMap,并以强引用的方式存储外界的缓存对象,提供get和put方法来完成缓存的获取和添加操作。当缓存满时,LruCache会移除较早的缓存对象,然后再添加新的缓存对象

LRUCache没什么好说的 源码也比较简单 抽空大家自己看看

//创建内存缓存区域
// 获取应用程序最大可用内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        // 设置图片缓存大小为程序最大可用内存的1/8
LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
            }
        };

最后当然是上代码啦

  • 注:由于自身环境的原因 图片没有做压缩处理 ,需要的大家自己添加 会更节省内存!
  • 代码注释比较详细,就不做赘述了

完整代码

public class ImageCacheUtils {
    //需要依赖 compile 'com.jakewharton:disklrucache:2.0.2'
    private final String TAG = "ImageCacheUtils";
    /**
     * 记录所有正在下载或等待下载的任务。
     */
    private Set<BitmapWorkerTask> taskCollection;

    /**
     * 图片内存缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
     * 《官方推荐LruCache》
     */
    private LruCache<String, Bitmap> mMemoryCache;

    /**
     * 图片硬盘缓存核心类。
     */
    private DiskLruCache mDiskLruCache;


    public ImageCacheUtils(Context context) {
        taskCollection = new HashSet<>();
        // 获取应用程序最大可用内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        // 设置图片缓存大小为程序最大可用内存的1/8
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
            }
        };
        try {
            // 获取图片缓存路径
            File cacheDir = getDiskCacheDir(context, "thumb");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            // 创建DiskLruCache实例,初始化缓存数据
            mDiskLruCache = DiskLruCache
                    .open(cacheDir, getAppVersion(context), 1, 100 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将一张图片存储到LruCache中。
     *
     * @param key    LruCache的键,这里传入图片的URL地址。
     * @param bitmap LruCache的键,这里传入从网络上下载的Bitmap对象。
     */
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 从LruCache中获取一张图片,如果不存在就返回null。
     *
     * @param key LruCache的键,这里传入图片的URL地址。
     * @return 对应传入键的Bitmap对象,或者null。
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

    public void removeCache() {
        mMemoryCache.evictionCount();
        mMemoryCache.evictAll();
    }

    /**
     * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象,
     * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。
     *
     * @param imageView  对应显示的控件 imageView
     * @param imageUrl   图片地址
     * @param view  可以为空,如果是ListView GridVIew等 imageView需要设置Tag  img.setTag(imageUrl);
     */
    public void loadBitmaps(@NonNull ImageView imageView, @NonNull String imageUrl, ViewGroup view) {
        Bitmap bitmap;
        String key = hashKeyForDisk(imageUrl);
        bitmap = getBitmapFromMemoryCache(key);
        if (bitmap == null) {
            DiskLruCache.Snapshot snapshot;
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            try {
                snapshot = mDiskLruCache.get(key);
                if (snapshot == null) {
                    BitmapWorkerTask task;
                    if (view != null)
                        task = new BitmapWorkerTask(view);
                    else
                        task = new BitmapWorkerTask(imageView);
                    taskCollection.add(task);
                    task.execute(imageUrl);
                } else {
                    fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                    try {
                        bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                    } catch (OutOfMemoryError e) {
                        removeCache();
                    }
                    imageView.setImageBitmap(bitmap);
                    addBitmapToMemoryCache(key, bitmap);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (fileDescriptor == null && fileInputStream != null) {
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        } else {
            if (imageView != null)
                imageView.setImageBitmap(bitmap);
        }
    }

    /**
     * @param imageUrl url
     *                 可以用于后台预先缓存 不需ui显示
     */
    public void loadBitmaps(String imageUrl) {
        String key = hashKeyForDisk(imageUrl);
        DiskLruCache.Snapshot snapshot;
        try {
            snapshot = mDiskLruCache.get(key);
            if (snapshot == null) {
                BitmapWorkerTask task;
                task = new BitmapWorkerTask();
                taskCollection.add(task);
                task.execute(imageUrl);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * bitmap转byteArr
     *
     * @param bitmap bitmap对象
     * @return 字节数组
     */
    private static byte[] bitmap2Bytes(Bitmap bitmap) {
        if (bitmap == null) return null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
        return baos.toByteArray();
    }

    /**
     * @param imgUrl
     * @return  从缓存中获取Bitmap 转 Byte[]()
     */
    public byte[] getBitmapByte(String imgUrl) {
        Bitmap bitmap;
        byte[] bimapbyt;
        String key = hashKeyForDisk(imgUrl);
        DiskLruCache.Snapshot snapshot;
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        try {
            snapshot = mDiskLruCache.get(key);
            fileInputStream = (FileInputStream) snapshot.getInputStream(0);
            fileDescriptor = fileInputStream.getFD();
            bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
            bimapbyt = bitmap2Bytes(bitmap);
            return bimapbyt;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fileDescriptor == null && fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 取消所有正在下载或等待下载的任务。
     */
    public void cancelAllTasks() {
        if (taskCollection != null) {
            for (BitmapWorkerTask task : taskCollection) {
                task.cancel(false);
            }
        }
    }

    /**
     * 根据传入的uniqueName获取硬盘缓存的路径地址。
     */
    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 获取当前应用程序的版本号。
     */
    private int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 1;
    }

    /**
     * 使用MD5算法对传入的key进行加密并返回。
     */
    private String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
            // Log.e(TAG, "hashKeyForDisk: ***********************MD5  " + key + " ?? " + cacheKey);
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
    /**
     * 将缓存记录同步到journal文件中。
     */
    public void fluchCache() {
        if (mDiskLruCache != null) {
            try {
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }




//    private 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 (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;
//        }
//        return inSampleSize;
//    }

    /**
     * 异步下载图片的任务。
     *
     * @author guolin
     */
    private class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {

        private ViewGroup view;
        private ImageView imageView;

        private BitmapWorkerTask(ViewGroup view) {
            this.view = view;
        }

        private BitmapWorkerTask(ImageView imageView) {
            this.imageView = imageView;
        }

        private BitmapWorkerTask() {
        }

        /**
         * 图片的URL地址
         */
        private String imageUrl;

        /**
         * @param params 图片地址
         * @return bitmap
         */
        @Override
        protected Bitmap doInBackground(String... params) {
            imageUrl = params[0];
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            DiskLruCache.Snapshot snapShot;
            try {
                final String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                // 存入磁盘
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    if (downloadUrlToStream(imageUrl, outputStream)) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }
                }
                // 缓存被写入后,再次查找key对应的缓存
                snapShot = mDiskLruCache.get(key);
                if (snapShot != null) {
                    fileInputStream = (FileInputStream) snapShot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                //将缓存数据解析成Bitmap对象
                Bitmap bitmap = null;
                if (imageView != null || view != null) {
                    if (fileDescriptor != null) {
                        bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                    }
                }
                return bitmap;
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fileDescriptor == null && fileInputStream != null) {
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                    }
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            // 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。
            if (view != null) {
                ImageView v = (ImageView) view.findViewWithTag(imageUrl);
                if (v != null && bitmap != null) {
                    v.setImageBitmap(bitmap);
                    addBitmapToMemoryCache(hashKeyForDisk(imageUrl), bitmap);
                }
            } else {
                if (imageView != null && bitmap != null) {
                    imageView.setImageBitmap(bitmap);
                    addBitmapToMemoryCache(hashKeyForDisk(imageUrl), bitmap);
                }
            }
            //下载完成 移除任务
            taskCollection.remove(this);
        }

        /**
         * 建立HTTP请求,并获取Bitmap对象。
         *
         * @return 解析后的Bitmap对象
         */
        private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
            HttpURLConnection urlConnection = null;
            BufferedOutputStream out = null;
            BufferedInputStream in = null;
            try {
                final URL url = new URL(urlString);
                urlConnection = (HttpURLConnection) url.openConnection();
                // 设置连接主机超时时间
                urlConnection.setConnectTimeout(5 * 1000);
                //设置从主机读取数据超时
                urlConnection.setReadTimeout(5 * 1000);
                urlConnection.connect();
                in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
                out = new BufferedOutputStream(outputStream, 8 * 1024);
                int b;
                while ((b = in.read()) != -1) {
                    out.write(b);
                }
                return true;
            } catch (final IOException e) {
                e.printStackTrace();
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                try {
                    if (out != null) {
                        out.close();
                    }
                    if (in != null) {
                        in.close();
                    }
                } catch (final IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }

    }
}

处女作,欢迎点评,谢谢点赞——by Luk

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值