Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_37639900/article/details/88575083

三级缓存概述

缓存是一种通用的思想可以用在很多场景中,但在实际的开发中经常用于Bitmap的缓存,用于提高图片的加载效率、提升产品的用户体验和节省用户流量。目前常见的缓存策略是LruCache和DiskLruCachey用它们分别实现内存缓存和硬盘缓存。Lru是Least Recently Used的缩写即最近最少使用算法,这种算法思想是:当缓存容量快满时,会删除最近做少使用的缓存对象。在这里提醒大家一句三级缓存一般针对的是加载网络图片时常用的缓存策略。
附上我自定义的ImageLoader的代码地址去理解三级缓存原理:https://github.com/mayanhu/ImageLoader

三级缓存的流程

当我们要加载一张网络上的图片时一般流程:

  1. 按照一定的规则生成一个缓存Key,生成可以的算法自己可以定义保证唯一性就行,需要注意的是我们的key不要直接用图片的URL因为URL中的的特殊字符不方便处理
  2. 先去内存中通过key去读取内存中缓存的读取图片资源,如果内存中有则直接返回该对象
  3. 如果内存中没有则根据key从磁盘中读取,如果磁盘缓存有则:1. 返回该资源 2.将该资源重新放入内存缓存中 3. 将改资源从磁盘缓存中移除
  4. 如果磁盘缓存中也没有改图片资源,则发起网络请求,从网络获取图片资源

强引用 、弱引用 、软引用、虚引用的区别:

因为LruCache内部采用LinkedHashMap以强引用的形式缓存外界的对象,所以在讲LruCache前需要先了解Java对象(即堆内存中对象)引用的四个级别以及各自的特点,以便我们能更好的掌握内存缓存LruCache的实现原理。

  1. 强引用:是指我们直接引用的对象,如String s=new Stirng(“abc”),特点是:只要引用存在,垃圾回收器永远不会回收,如果无限制的使用new 强对象会抛出OOM异常。
  2. 软引用WeakReference:特点是:当一个对象只有软引用存在时,当系统内存不足时此对象会被GC回收。
  3. 弱引用SoftReference:当一个对象只有弱引用存在时,此对象随时会被GC回收。
  4. 虚引用PhantomReference:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
  5. 在此我只是简单描述了一下对象引用的特点,以便理解LruCache的实现原理,详细的用法和特点需要大家自行查阅资料,在这里不做过多描述。

内存缓存LruCache

首先简单说一下三级缓存为什么优先从内存中加载:

  • 内存的加载速度时最快的,比从磁盘加载快的多
  • 另一方面内存缓存的是Bitmap对象,可直接使用,而磁盘缓存的是文件需要我们先转换成对象才能使用

LruCache内部采用LinkedHashMap以强引用的形式缓存外界的对象,就是以键值对的形式缓存我们的Bitmap对象。LruCache是一个范型类,它是线程安全的因为它对数据的操作都加了锁。它的使用也很简单代码如下:

    LruCache<String,Bitmap> mLruCache;
    public void init(){
        //当前进程的可用内存
        int mxaMeory=(int)(Runtime.getRuntime().maxMemory()/1024);
        //当前进程的可用内存/8为缓存大小
        int meoryChache=mxaMeory/8;
        //初始化缓存大小 复写sizeOf计算缓存对象的大小,单位要和总容量的一直
        mLruCache=new LruCache<String, Bitmap>(meoryChache){
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes()*bitmap.getHeight()/1024;//单位要和meoryChache保持一致
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //此方法有需要是复写,做一些资源回收动做,在LruCache移除最近最少使用的对象时自动调用
                //比如为了提高内存缓存的效率,我可在用一个弱引用的LinkHashMap去存储不常使用的对象,
                //实现进一步的缓存
                super.entryRemoved(evicted, key, oldValue, newValue);
            }
        };
    }

除了创建外LruCache还提供了:

  //存储
   		mLruCache.put(key,value);
  //获取
       // mLruCache.get(key);
  删除
//        mLruCache.remove(key)

DiskLruCache

第二级缓存,硬盘缓存DiskLruCache,它是通过将缓存对象写入文件系统从而实现缓存,需要注意的是DiskLruCache它不是AndroidSDK的源码,但是他得到了官方文档的推荐,使用它时需要从如下网址下载:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java (注意需要翻墙)。

1:创建DiskLruCache

DiskLruCache不能通过构造方法创建,它提供了open方法用于创建自身,如下是open()方法的源码:

/**
   * Opens the cache in {@code directory}, creating a cache if none exists
   * there.
   *
   * @param directory  缓存目录
   * @param  appVersion 表示应用版本号一般设置为1即可,当其发生变化时DiskLruCache会清空之前所有的缓存文件
   * @param valueCount  表示一个键对应几个值,即一个缓存Key对应几个缓存文件, 一般设置为1即可
   * @maxSize  最大缓存容量
   */
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
      throw new IllegalArgumentException("valueCount <= 0");
    }

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
      File journalFile = new File(directory, JOURNAL_FILE);
      // If journal file also exists just delete backup file.
      if (journalFile.exists()) {
        backupFile.delete();
      } else {
        renameTo(backupFile, journalFile, false);
      }
    }

	//初始化如果存在则执行文件尾加操作,否者创建Dir创建在创建DiskLruCache的实例
    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
      try {
        cache.readJournal();
        cache.processJournal();
        cache.journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

2:DiskLruCache的增加图片操作

DiskLruCache的缓存操作是通过Editor完成,通过缓存key区获取Editor对象,Editor表示缓存对象的编辑对象,通过Editor对象获取一个输出流指向缓存文件,实现添加缓存操作,这里需要注意的是图片的缓存一般不直接使用该图片的URL,因为URL中有可能有特殊字符影响使用。一般采用URL的MD5作为key。添加缓存操作代码如下
1: 先去获取缓存key

   
    /**
     * 根据url  获取MD5key  因为url中可能含有特殊字符 影响在Android中直接使用
     * @param url
     * @return
     */
    private String hashKeyFormUrl(String url){
        String cacheKey;
        try {
            MessageDigest messageDigest=MessageDigest.getInstance("MD5");
            messageDigest.update(url.getBytes());
            cacheKey=bytesToHexString(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            cacheKey=String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    /**
     *
     * @param digest
     * @return
     */
    private String bytesToHexString(byte[] digest) {
        StringBuilder sb=new StringBuilder();
        for (int i = 0; i < digest.length; i++) {
            String hex=Integer.toHexString(0xFF & digest[i]);
            if (hex.length()==1){
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

2:通过缓存key获取Editor对象实现添加操作:

 /**
     * 添加一个缓存图片
     * @param urlKey
     */
    public void addDiskCacheEdit(String urlKey){
        if (mDiskLruCache==null) {
            return;
        }
        String key = hashKeyFormUrl(url);
        try {
            //通过key拿到edit对象---》outputStream
            DiskLruCache.Editor edit = mDiskLruCache.edit(key);
            if (edit != null) {
                OutputStream outputStream = edit.newOutputStream(DISK_CACHE_INDEX);
                //因为open方法中设置一个键对应一个值,所以DISK_CACHE_INDEX一设置为0 即可
                //如果文件下载成功提交编辑
                if (downloadUrlToStream(url,outputStream)){
                    //提交写操作进行提交到文件系统
                    edit.commit();
                }else {
                    //图片下载异常 回退整个操作
                    edit.abort();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
     * 下载图片资源时,将期存入硬盘缓存目录
     * @param urlImage
     * @param outputStream
     * @return
     */
    private boolean downloadUrlToStream(String urlImage, OutputStream outputStream) {
        HttpURLConnection urlConnection=null;
        BufferedInputStream in=null;
        BufferedOutputStream out=null;
        try {
            URL url=new URL(urlImage);
            urlConnection= (HttpURLConnection) url.openConnection();
            in=new BufferedInputStream(urlConnection.getInputStream());
            out =new BufferedOutputStream(outputStream);
            int b;
            while ((b=in.read())!=-1){
                out.write(b);
            }
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
           release(urlConnection,in,out);
        }
        return false;
    }

3:DiskLruCache的获取图片操作

和图片的添加操作类似,查找操作也是需要将url转换为key,然后通过DiskLruCache的get(String key)获取Snapshot对象,通过Snapshot获取缓存文件的输入流,通过输入流去获取缓存的Bitmap对象去使用,只是在使用时存入了内存缓存中。下面是我的获取硬盘缓存的代码:

/**
     * 获取一个硬盘缓存图片  并且通过控件宽高进行缩放加载  避免OOM
     * @param imageUrl
     * @return
     */
    public Bitmap getBitmapChache(String imageUrl,int reqWith,int reqHeight){
        Bitmap bitmap=null;
        String keyFormUrl = hashKeyFormUrl(imageUrl);
        FileInputStream inputStream=null;
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keyFormUrl);
            if (snapshot != null) {
                //inputStream是一种有序的文件流,通过Options缩放存在问题,两次decodeStream影响了文件流的位置属性,第二次decodeStream时得到的的为null
                 inputStream= (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                //解决方案是通过文件夹流来得到对应文件的描述符让后通过BitmapFactory.decodeFileDescriptor来加载一张缩略图
                FileDescriptor fileDescriptor = inputStream.getFD();
                bitmap=BitmapUtils.decodeSampledBitmapFileDescriptor(fileDescriptor,reqWith,reqHeight);
                if (bitmap != null) {
                    // TODO: 2019/3/6 添加到内存缓存  自定义ImageLoader
                }
               return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }

/**
     * 文件夹流
     * @param fileDescriptor  文件夹流
     * @param reqWidth
     * @param reqHeight
     * @return
     */
  public static Bitmap decodeSampledBitmapFileDescriptor(FileDescriptor fileDescriptor,int reqWidth,int reqHeight){
      BitmapFactory.Options options=new BitmapFactory.Options();
      //inJustDecodeBounds=true只会解析图片的原始宽高信息,不会真正的去加载图片
      options.inJustDecodeBounds=true;
      //第一次decode  加载原始图片数据
      BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
      //计算缩放比例
      options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
      //重新设置加载图片
      options.inJustDecodeBounds=false;
      return BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
  }

4:删除缓存文件

 /**
     * 删除指定的缓存文件
     * @param url
     */
    private void reloveDiskCache(String url){
        String key = hashKeyFormUrl(url);
        try {
            mDiskLruCache.remove(key);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 删除所有的缓存文件
     * @param 
     */
    private void clearALLCache(){
       
        try {
            mDiskLruCache.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

总结:

  1. 内存缓存LruCache和DiskLruCache都是通过LRU 算法实现 ,对数据的操作都是用来同步锁机制,即都是线程安全的。
  2. 内存缓存LruCache内部采用LinkedHashMap以强引用的形式缓存外界的对象,就是以键值对的形式缓存我们的Bitmap对象。
  3. DiskLruCache添加缓存图片操作是通过缓存key获取Eidtor对象,并通过Editor对象获取指向改缓存文件的输出流,通过该输出流实现添加操作。
  4. DiskLruCache获取缓存图片的操作是通过缓存key获取Snapshot对象,通过Snapshot对象可得到缓存文件输出流,通过该流获取缓存对象。
  5. 三级缓存的核心就是:LruCache和DiskLruCache的使用,即先从内存缓存中获取,如果获取不到再从硬盘缓存中获取,如果在硬盘中获取到了则:添加到内存缓存中,并从硬盘缓存中移除,如果磁盘中也没有获取到则发起网络请求进行图片下载下载成功后进行缓存。

这里只是简单的描述了三级缓存的的原理和使用, 它也是现在主流的图片加载框架GLide、ImageLoader实现图片缓思想,只是它们的每一步做的更加精密严谨,使用更加方便,内部使用了多种设计模式实现了代码的高度解耦。

猜你喜欢

转载自blog.csdn.net/weixin_37639900/article/details/88575083