安卓OOM和Bitmap图片二级缓存机制(二)

本文出自 “阿敏其人” 博客,转载或引用请注明出处。

在上一篇:安卓OOM和Bitmap图片二级缓存机制(一)中,已经讨论了安卓中OOM发生的原因,情况和如何有效加载高清图片的。现在在此回顾一下:

  • 安卓OOM发生的原因:图片分辨率过大,导致加载图片所需的内存超过系统给进程(app)分配的运行内存,内存爆掉,产生OOM
  • 核心解决办法: 利用BitmapFactory。Options的inSimpleSize,计算出合适的图片采样率,减小图片分辨率。

再续前缘,接下来的这篇博客里面我们说图片的缓存机制。
缓存机制,也叫二级缓存,实际上也就是一个图片存储策略,软件中二级缓存是一个很常见图片存取策略。
实现二级缓存只要使用的Lru机制(Least Recently Used),即为最近最少使用


  • Lru策略主要涉及两个类:
    1. LruCache 用作内存缓存
    2. DiskLruCache 用作存储设备缓存

一、安卓图片二级缓存机制

1、什么是二级缓存

    移动联通电信是有钱的,用户的流量是花钱买的,wifi不是7*24小时连着的,所以耗流量下载的图片是需要缓存起来的,不能没去都去重新联网获取图片,一来速度慢二来耗流量。
    
    所以,图片要缓存,缓存很重要。


1.1、二级缓存,内存缓存 --> 存储设备缓存 --> 网络

    二级缓存,就是  内存缓存 --> 存储设备缓存 --> 网络

每次二级走,每次都这么拿图片,直到再拿到图片的那一级就停下。

  • 比如有图片A:
* 1.第一次我们加载这样图片的时候,发现内存缓存拿不到,就去存储设备缓存拿,如果发现存储设备缓存拿不到,那么就去网络拿图片,到了网络就肯定能拿到图片,除非url错误或者网络问题。
**当我们成功拿到图片之后,就会把这个图片往内存缓存和存储设备缓存都存一份**
* 2.第二次拿图片的时候,路还是这么走,先去 *内存缓存* 里面找,发现,哎呀,找到了,那么就停下来了,拿着图片高兴地和ImageView过上了幸福的生活,加载显示出给用户看了。
第3第4次也是这么走,反正就是 *内存缓存* 、 *本地缓存*、 *网络* 这么三个循序一条道走到底,知道拿到为止。
那么什么时候会拿到 本地缓存 呢,拿不到内存缓存的时候就拿不到内存缓存呗,那么明明从网络拿到图片的时候我们就往 内存缓存 和 本地缓存 都存了一份备份的图片,那么为什么会拿不到 内存缓存 呢?因为内存缓存是缓存,是缓存都可能被清掉,空间不足等情况或者使用较少(Lru)却占着茅坑不拉屎的时候就会被清掉;同样的,本地缓存也有可能被清掉,反正只要是缓存都有可能被清掉。

1.2、缓存看得见吗,在哪里

  • 对于内存缓存(路径不是自己指定,是谷歌指定的)
    * 1.在真机上是看不见的(除非你拿真机去刷机然后折腾各种命令然后一番浴血奋战,就可以看到了)
    * 2.在模拟器的时看得到。目录是 data/data/包名/cache
  • 对于存储设备缓存
    这个不管是在真机上还是在模拟器上都是可以看见的,路径都是我们自己指定的

补充一下关于安卓手机内部缓存的一点知识

  • 如何获得手机内部存储的路径
    • 1.getFileDir(); 获取自己的文件夹 /data/data/包名/files
    • 2.getCacheDir(); /data/data/包名/cache

files 目录存放重要的应用程序数据.手机不会自动清理files目录的文件,除非用户到 应用管理 手动清楚程序数据,这个目录下的数据才会被清除
cache 目录是存放临时的不重要的数据.这个目录有特定,当手机内存空间不足的时候会自动清理cache目录的文件 不管是cache还是files,都是属于手机的内部存储空间


1.3、为什么用我们的二级存储要采用Lru机制

首先我们按照最基本的思想开始思考,在没有使用Lru(Least Recent Used)之前,我们可能会想,直接简单粗暴按照顺序把图片放进缓存的文件夹,假设设定存放存放本地缓存的文件夹为30m(内存缓存没办法自定好像),那么先进来的先出去,想队列看一样不好吗?其实确实不好,因为有的图片虽然进来的慢,但是却需要经常出现,如果直接按照循序来决定谁先被清出缓存,那么就直接暴力了,这样肯定不好,这时候,Lru机制出现了,他说,在最近的一段时间里面,谁被使用得最少,谁先出局,这个规则挺好的,然后大家都认同了。

1.4、Lru的 取 存 删 三个操作

  • 说到底,我就是想从缓存里面拿图片来显示,那么现明显,需要有一个 的过程。
  • 那么,既然需要 取 图片,那么在取之前一定要一个动作,那么就是 的过程
  • 存放图片缓存的地方大小是有限的,既然是有限的,那么没用的或者说少用的多余的图片,就需要被清理出去,那么就会有一个 的过程

凡是对数据测操作,无非跟数据库的基本操作原理一样,终究逃不过 增、删、改、查 着四个方面,但是我们实事求是,根据需要确定我们一个操作数据的流程到底需要具备增删改查的那几个方面,就可以最终确定我们需要的进行的操作了,总结下来,就是 取、存、删 三个过程

只要弄清楚了LruCache和DiskLruCache的 各自三个过程是如何实现的,二级缓存机制我们几乎都可以宣告完成了。

做一个小结:二级缓存的主要使用LruCache算法,而Lru主要就是使用LruCache和DiskLruCache这两个类。
要使用着两个类,就是要明白着两个类具体的 存 取 删 分别是怎么进行的。

2、LruCache类

LruCahce是Android3.0提供给我们的一个缓存类,我们可以通过support-v4包最低兼容到Android2.2,所以我们在使用的时候最好调用的V4包得LruCache,为了兼容嘛。


内存缓存技术小历史
在没有Lru这套内存缓存算法之前,(也就是安卓3.0之前),人们的做内存缓存主要利用的是 弱引用 和 软引用 ,从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃,所以在Android2.3之后再采用软引用弱引用就不再合适了,这是也是L如出现的原因之一,因为LruCache是以强引用的方式引用外界的缓存对象的,被强引用的对象gc(垃圾回收机制)是没有办法对其进行回收的。
强引用: 直接的对象引用,不会被gc回收
软引用:(SoftReference) 当一个对象只有弱引用存在时,当系统内存不足就gc会随时回收这个对象
弱引用:(WeakReference)当一个对象只有弱引用存在时,随时可能被系统回收,弱爆了



LruCache是一个泛型类,他内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来进行缓存的获取存储当缓存满时,LruCache会移除最近最少使用的对象,然后再添加新的对象

LruCache是线程安全的

public class LruCache{
    private final LinkedHashMap map;
...
}

LruCache源码参考

1、 LruCache的创建

下面这个代码简单说明了LruCache的初始化过程:

int maxMemory = (int)(Runtime.getRuntime().maxMemory)/1024);
int cacheSize = maxMemory/8;


mMemoryCache = new LruCache(cacheSize){
    @Override
    protected int sizeOf(String key,Bitmap bitmap){
        return bitmap.getRowBytes()*bitmap.getHeight()/1024;
    }
 
}

从过上面的代码我们知道,其实我们需要提供的只是缓存的容量总大小和重写sizeOf()方法即可。

  • siseOf方法的作用是计算 缓存对象的大小。(这里的单位规格必须要和总容量一直,统一为KB比较好)
    需要注意的一点是,LruCache移除旧的缓存的时候会调用entryRemoved方法,如果需要的话可以在entryRemoved方法里面做一些相关的操作。

补充一下的Runtime的相关知识:
java.lang.Object.Runtime官方API

  • Runtime.getRuntime().maxMemory 当先虚拟机给每个进程分配最大限额的内存
  • Runtime.getRuntime().totalMemory 当前进程已经占用过的内存

  • Runtime.getRuntime().freeMemory 当前申请过又没用上空闲下来的可用内存

这三者返回的单位就是byte,除以1024得出kb,接着再除以1024,得出m
下面是新建一个程序调用者三个方法的输出结果(单位转成M)

10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: maxMemory:32
10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: freeMemory:2
10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: totalMemory:5

谈谈Runtime类中的freeMemory,totalMemory,maxMemory几个方法

经过上面的这一个分析,我们应该比较清楚上面那段关于LruCache的代码的含义了。
就是我们把我们当前安卓app所能申请到的最大内存(maxMemory)的8分之1来用作内存缓存。这个就是我们指定的内存缓存的容器的大小,大小是自己指定的。

2、 LruCache的 存 取 删

存储
mMemoryCache.get(key)

获取
mMemoryCache.put(key,bitmap)

删除
mMemory.remove(key)

3、DiskLruCache类

DiskLruCache是用来做存储设备缓存的(区别于内存缓存),也就是磁盘缓存。
DiskLruCache源码

3.1.a、DiskLruCache的创建

DiskLruCache不能通过构造方法创建,他提供了Open方法用来创建自身,如下所示:

/**
第一个参数:缓存文件的存放路径(重要)
第二个参数:一般写 1 就好
第二个参数: 一般写 1 就好
第四个参数:存放缓存文件的容器的容量大小(重要)
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

一个完整的open方法

    /**
     * Opens the cache in {@code directory}, creating a cache if none exists
     * there.
     *
     * @param directory a writable directory
     * @param appVersion
     * @param valueCount the number of values per cache entry. Must be positive.
     * @param maxSize the maximum number of bytes this cache should use to store
     * @throws java.io.IOException if reading or writing the cache directory fails
     */
    **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");
        }
 
        // 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 FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("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;
    }

3.1.b、open的四个方法进行分析:

第一个参数:File directory
表示磁盘缓存的存储路径,缓存路径可以选择的SD卡作为缓存路径,具体是指
/sdcard/Android/data/包名/cache 目录
为什么选择这个目录,因为指定为这个目录时,当应用被卸载的时候,这个文件夹的内容会被一起删除掉。当然我们要是不想这么干的话可以指定的我们自己想要的路径。

第二个参数 int appVerasion
表示的应用的版本号,这个一般来说写 1 就好。
这个参数的作用是当检测到版本号发生改变的实惠,就把缓存文件清空,但是这个特性在实际开发中并不实用,因为发现很多时候就算我们的版本号写着发生改变了缓存文件仍然是有限的,所以一般这个我们写1就好

第三个参数 int valueCount
这个还是一般都写 1 就好。
这个表示单个节点对应的数据的个数。

第四个节点 存放缓存文件的总空间的大小
比如我们设定为50M,那么当缓存文件超出50M之后DiskLruCache就会自动根据算法删除一些缓存文件,腾出位置给新的缓存文件存放。

知道了这四个参数的作用之后,一个典型的DiskLruCache应该这么创建:

private static final long DISK_CACHE_SIZE= 1024 *1024 * 50; // 50M
try {  
    File cacheDir = getDiskCacheDir(context, "bitmap");  
    if (!cacheDir.exists()) {  
        cacheDir.mkdirs();  
    }  
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, DISK_CACHE_SIZE);  
} catch (IOException e) {  
    e.printStackTrace();  
}  


3.2、DiskLruCache的存储(缓存添加)

DiskLruCache 的缓存添加是通过Editor完成的。
Editor表示一个缓存对象的编辑对象,我们这里以图片缓存为例子,首先需要获取图片的url所对应的key,然后根据key就可以通过edit()来获取Editor对象。如果这个缓存正在被编辑,那么edit()就会返回null,也就是说, DiskLruCache不允许同时编辑一个缓存对象
图片的url不能只为作为key(因为url往往含有乱码),一般都是以图片的url的MD5的值作为key


3.2.a、将图片url的MD5值作为缓存的key

将url转换为MD5的代码:


// 传入图片url,返回将图片的url的MD5的值(这个返回值的将作为存储缓存的 key)
private String hashKeyFormUrl(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(url.hashCode());
    }
    // 返回将作为缓存的key值
    return cacheKey;
}

// 将byte[]数组转为字符串的的方法  
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();
}


3.2.b、DiskLruCache的可用的Editor对象,利用的Editor获得输入流将文件写入缓存目标地址

将url转成key之后,就可以捕获的Editor对象了(前提是当前不存爱其他Editor对象)。
我们获得这个Editor对象的目的就是想要拿到一个输出流。

注意: 由于前面的我们在open方法中参数的第三个参数里面设置一个节点只能对应一个数据,所以啊,在输入流的参数 DISK_CACHE_INDEX 中我们把常量设置为 0 ,如下所示:

        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            
        }



得到输入流了,我们的目标就是把图片写入缓存目录,这是时候我们来看一下:下载图片并得写入文件系统的方法:

    public 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();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.e(TAG, "downloadBitmap failed." + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(out);
            MyUtils.close(in);
        }
        return false;
    }

经过上面的步骤,我们还差一步就可以真正的将图片写入到缓存的文件系统了,还差哪一步 —— editor.commit()

通过commit来提交写入操作,如果图片下载的过程发生了异常,那么还可以通过Editor的abort()来回退整个操作,具体代码如下:

OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();



DiskLruCache进行 缓存添加 小结

  • 第一步,将图片的url的MD5值作为缓存的key
  • 第二步,得到key之后获得Editor对象获得输出流
  • 第三步,commit最终确定把图片写入到缓存,如果下载试下那么就利用abort进行回退

3.3、DiskLruCache的缓存查找 (获取缓存)

缓存查找的过程(和DiskLruCache的写入相似):

  • 第一步,将图片的url的MD5值作为缓存查找的的key
  • 第二步,利用DiskLruCache的get方法得到一个Snapshot对象今儿得到文件输入流
  • 得到输入流了,自然可以得到Bitmap对象了



注意:
为了避免OOM,我们一般都不直接加载原图,都会利用BitmapFactory.Options来加载一张合适分辨率的图片,但是这个方式针对特殊的FileInputStream却行不通,因为FileInputStream是一种有序的文件流,而两次的decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null,为了解决这个问题,可以通过文件流来得到他所对应的 文件描述符 ,然后在通过 BitmapFactory.decodeFileDescriptor方法来加载一张缩放过的图片
过程如下所示:

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
                    reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }

3.4、缓存的删除

主要涉及两个方法,remove和delete。

remove方法,指定删除哪一个缓存

现在已经是老油条了
第一步,获取url的MD5,作为key
第二步,调用 remove 方法,进行删除

大概代码如下:

try {  
    String imageUrl = "http:XXX.jpg";    
    String key = hashKeyForDisk(imageUrl);    
    mDiskLruCache.remove(key);  
} catch (IOException e) {  
    e.printStackTrace();  
}



delete()方法,删除所有缓存

这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。


4、缓存机制的应用

4.1、 简单的图片加载框架(ImageLoader)的打造

一个简洁的ImageLoader小图片加载框架,主要参考《Android开发艺术探索》一书。

4.1.a、ImageLoader应该具备的功能

  • 一个相对优秀的开发图片框架应该具备如下功能:
    • 1、同步加载
    • 2、异步加载
    • 3、图片的压缩
    • 4、二级缓存(内存缓存、磁盘缓存、网络拉取)

同步加载:指图片能够以同步的方式想调用者提供图片,至于提供的方式,可能是内存缓存,可能是磁盘缓存,也可能是网络拉取。

异步加载:异步是安卓相当重要的一个操作,我们不能让主线程干太多事,耗时的操作柏旭交给异步的去做,比如加载图片这种事情是耗时的,所以我们的加载图片的耗时操作最好交给异步来执行。

压缩:压缩没什么好说的,就是利用Options计算inSimpleSize

至于二级缓存,肯定就是一个图片框架的核心了

图片框架的注意的地方:复用,不管是ListView还是GridView,复用无疑是一个很好地设计,但是在图片加载框架的里面就是一个需要特别思考对待的问题,比如情况是这样子的,ListView的itemA正在加载一张图片,这个时候突然用户快速滑动屏幕,那么很可能itemB就复用上了A的图片,其实图片A不是itemB想要的,itemB明显是要显示图片B的。


4.1.b、抽出压缩类 ImageResizer,

专门用来计算合适的inSimpleSize,合理地压缩图片,减少内存压力

这个类主要由两个方法:

  • decodeSampledBitmapFromResource
  • decodeSampledBitmapFromFileDescriptor


import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
// 图片压缩类
public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {
    }


    // 传入资源文件,计算合适的大小,返回一个Bitmap对象
    public Bitmap decodeSampledBitmapFromResource(Resources res,
            int resId, int reqWidth, int reqHeight) {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    
    /**
        为了加载文件系统的文件避免二次decodeStream返回null而专门调整的方法decode方法
        解释:
        FileInputStream是一种有序的文件流,而两次的decodeStream调用影响了文件流的位置属性,
        导致了第二次decodeStream时得到的是null,为了解决这个问题,可以通过文件流来得到他所对应的文件描述符,
        然后在通过 BitmapFactory.decodeFileDescriptor方法来加载一张缩放过的图片
    */
    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    public int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d(TAG, "origin, w= " + width + " h=" + height);
        int inSampleSize = 1;

        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;
            }
        }

        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }
}



4.1.c、内存缓存和磁盘缓存的实现



在构造函数里面对LruCache和DiskLruCache进行创建
图片的框架的主要实现功能的类 ImageLoader类 的构造方法进就行对 LruCacheDiskLruCache初始化

构造方法如下:

    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

内存缓存空间:这里我们指定为系统分配给app最大的内存的八分之一,这里要怎么分配都行,不太大太夸张就行。

磁盘缓存空间:这里我们设定为50M,这是当系统磁盘内存足够多情况。
但是当系统磁盘内存不足的时候,这个DiskLruCache就会失效,因为系统剩余 的内存空间比我们想要申请的还小。当然,我们也可以指定为20M或者30M,都行,看实际情况。



内存缓存的 存 和 取
内存的缓存的 存 和 取 相对比较简单

    // 内存缓存 的 添加
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    // 内存缓存的 获取
    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

磁盘缓存的 存 和 取
这个相对特殊一些,说明一下:

**存(添加):loadBitmapFormHttp**

磁盘缓存的添加需要通过Editor来完成,Editor利用commit和abort方法来提交和撤销文件系统的写入操作,我们定义了loadBitmapFormHttp方法


**取(获取): loadBitmapFormDiskCache**

磁盘的读取需要通过 Snapshot 来完成,通过 Snapshot 磁盘缓存对象对应的 FileInputStream,但是 FileInputStream 无法边界地挖宝从各部分压缩,所以我们利用专门在压缩类里面专门为其打造的 FileDescriptor 来加载压缩图片,最后成功获取Bitmap。

代码呈上:



    // 添加图片到 磁盘缓存 。(从网络加载后,添加缓存到 磁盘缓存)
    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }


    // 获取图片,从磁盘缓存处 
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
            int reqHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
                    reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }

        return bitmap;
    }

关于图片缓存就写到这里了
如果对图片缓存的实现例子有兴趣可以查看接着查看这篇文章:
造简单的图片加载框架——ImageLoader的实现
本篇完。


相关参考:
《Android开发艺术探索》一书

你可能感兴趣的:(安卓OOM和Bitmap图片二级缓存机制(二))