Android图片的压缩与缓存策略

874 阅读19分钟

首先我们先了解一下Bitmap的基本知识点,在Android3.0之前,Bitmap的对象是放在Java堆中,而Bitmap的像素是放置在Native内存中,这个时候需要手动的去调用recycle,才能去回收Native内存;在Android3.0到Android7.0,Bitmap对象和像素都是放置到Java堆中,这个时候即使不调用recycle,Bitmap内存也会随着对象一起被回收。虽然Bitmap内存可以很容易被回收,但是Java堆的内存有很大的限制,也很容易造成GC。在Android8.0的时候,Bitmap内存又重新放置到了Native中。Bitmap造成OOM很多时候也是因为对Bitmap的资源没有得到很好的利用,同时没有做到及时的释放。

* On Android Android 2.2 (API level 8) and lower, when garbage collection occurs, your app's threads get stopped. This causes a lag that can degrade performance. Android 2.3 adds concurrent garbage collection, which means that the memory is reclaimed soon after a bitmap is no longer referenced.
*   On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. From Android 3.0 (API level 11) through Android 7.1 (API level 25), the pixel data is stored on the Dalvik heap along with the associated bitmap. In Android 8.0 (API level 26), and higher, the bitmap pixel data is stored in the native heap.

</div>

一、Bitmap占用内存大小

Android 中提供一下几种Bitmap编码:

其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。

  • ALPHA_8 表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
  • ARGB_4444 表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
  • ARGB_8888 表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
  • RGB_565 表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节

通过**inPreferredConfig配置项,可以选择不同的编码方式,**如果采用ARGB_8888这种编码格式,那么常见的1080*1920的图片内存占用就是:1920 x 1080 x 4 = 7.9M

二、Bitmap的几种压缩方式

1、质量压缩

在保持像素的前提下通过JEPG、PNG和WEBP图片格式的压缩算法来改变图片的存储大小,这样适合去传递二进制的图片数据,比如分享图片,要传入二进制数据过去,限制在XX kb之内。存在两个特点:

(1)bitmap图片占用的内存大小并不会改变;

(2)bytes.length是随着quality变小而变小的。

/**
 * 一种质量压缩方法
 *
 * @param src           源图片
 * @param maxByteSize   允许最大值字节数
 * @param recycle       是否回收
 * @return              质量压缩压缩过的图片
 */
public static Bitmap compressByQuality(final Bitmap src, final long maxByteSize, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0 || maxByteSize <= 0) {
        return null;
    }
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] bytes;
    if (baos.size() <= maxByteSize) {// 最好质量的不大于最大字节,则返回最佳质量
        bytes = baos.toByteArray();
    } else {
        baos.reset();
        src.compress(Bitmap.CompressFormat.JPEG, 0, baos);
        if (baos.size() >= maxByteSize) { // 最差质量不小于最大字节,则返回最差质量
            bytes = baos.toByteArray();
        } else {
            // 二分法寻找最佳质量
            int st = 0;
            int end = 100;
            int mid = 0;
            while (st < end) {
                mid = (st + end) / 2;
                baos.reset();
                src.compress(Bitmap.CompressFormat.JPEG, mid, baos);
                int len = baos.size();
                if (len == maxByteSize) {
                    break;
                } else if (len > maxByteSize) {
                    end = mid - 1;
                } else {
                    st = mid + 1;
                }
            }
            if (end == mid - 1) {
                baos.reset();
                src.compress(Bitmap.CompressFormat.JPEG, st, baos);
            }
            bytes = baos.toByteArray();
        }
    }
    if (recycle && !src.isRecycled()){
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    return bitmap;
}

2、采样压缩

通过查看BitmapFactory.Options类的源码,我们可以看到通过设置inJustDecodeBounds的模式,不将Bitmap加载到内存也可以获取到待解码图片的宽高,然后通过设置inSampleSize的值(int类型)来改变加载图片的采样率,假如设为n,则宽和高都为原来的1/n,宽高都减少,内存占用将会降低。

注意:官方文档指出:inSampleSize的取值应该总是2的指数,如1,2,4,8等,如果外界传入的inSampleSize的孩子不为2的指数,那么系统会向下取整并选择一个最接近2的指数代替。

我们需要获取加载图片的宽高信息,然后交给inSampleSize参数选择缩放比缩放。那么如何能先不加载图片却能获取图片的宽高信息,通过inJustDecodeBounds=true,然后加载图片就可以实现只解析图片的宽高信息,并不会真正的加载图片,所以这个操作时轻量级的,当获取了宽高信息,计算出缩放比后,然后再将inJustDecodeBounds=false,再重新加载图片,就可以加载缩小后的图片。

public static Bitmap compressBySampleSize(final Bitmap src, final int maxWidth, final int maxHeight, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] bytes = baos.toByteArray();
    BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
    options.inJustDecodeBounds = false;
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    return bitmap;
}

/**
 * 计算获取缩放比例inSampleSize
 */
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    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 = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    final float totalPixels = width * height;
    final float totalReqPixelsCap = reqWidth * reqHeight * 2;
    while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
        inSampleSize++;
    }
    return inSampleSize;
}

3、缩放压缩

Android SDK 提供了Matrix类,用来对图像进行缩放、旋转、平移、斜切等操作,同样的这个类也可以运用于bitmap。放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。但是相较于采样压缩,它的优势在于采样率可以取任意值,而不是限制在2的指数,劣势是压缩的对象必须是已经加载到内存中的Bitmap,而不能是存在磁盘中的源文件。

private static Bitmap scale(final Bitmap src, final float scaleWidth, final float scaleHeight, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    Matrix matrix = new Matrix();
    matrix.setScale(scaleWidth, scaleHeight);
    Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    return ret;
}

三、Bitmap内存的复用

* Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。* 使用这个字段有几点限制: * 声明可被复用的Bitmap必须设置inMutable为true; * Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用; * Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig; * Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存; * Android4.4(API 19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1
* getByteCount() * getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。* getAllocationByteCount() * API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。
* getByteCount()与getAllocationByteCount()的返回值大小比较: * 一般情况下两者是相等的; * 通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。* 在复用Bitmap的情况下,getAllocationByteCount()可能会比getByteCount()大。

四、Bitmap的缓存

我们常说的图片三级缓存:内存缓存、硬盘缓存、网络缓存。

不管是从网络上下载图片,还是直接从SD卡中读取图片,缓存对于图片加载的优化起到了至关重要的作用。当我们首次从网络上或者SD卡读取图片,会对图片进行相应的压缩处理。在处理过后不加入缓存,下一次请求图片还是直接从网络上或者USB中直接读取,不仅消耗了用户的流量还重复对图片进行压缩处理,占用多余内存的同时加载图片也很缓慢。

对于缓存,目前的策略是内存缓存和存储设备缓存。当加载一张图片时,首先会从内存中去读取,如果没有就接着在存储设备中读,最后才直接从网络或者SD卡中读取。接下来就聊一聊这两种缓存的具体内容。

LRU是用于实现内存缓存的一种常见算法,LRU也叫做最近最少使用算法,通俗来讲就是当缓存满了的时候,就会优先的去淘汰最近最少使用的缓存对象。首先对LruCache进行初始化,获取当前进程可用的内存,然后将内存缓存的容量制定为可用内存的1/8,同时对Bitmap对象进行大小的计算。接着构造出两个对外的方法,一个是根据Key从Cache中获取数据,一个是将数据存储到cache中,简单的3步也就完成了LruCache的使用。

//1.初始化LruCache.
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mCache = new LruCache<Integer,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(Integer key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

    }

    //2.从Cache中获取数据
    public Bitmap getDataFromCache(int key) {
        if (mCache.size() != 0) {
            return mCache.get(key);
        }
        return null;
    }

    //3.将数据存储到Cache中
    public void putDataToCache(int key, Bitmap bitmap) {
        if (getDataFromCache(key) == null) {
            mCache.put(key,bitmap);
        }
    }

磁盘缓存所使用的算法为DiskLruCache,它的使用比内存缓存要复杂一点,但是还是离不开上面的3步,初始化,查找和添加。DiskLruCache的创建是DiskLruCache.open()来创建,其中会传入4个参数,第一个参数表示磁盘缓存所要存储的路径,一般来说,如果外部设备存在,那么存储路径放置在 /storage/emulated/0/Android/data/package_name/cache 中;反之就放置在 /data/data/package_name/cache 这个目录下。存储路径可以根据自己的实际要求进行制定,值得注意的是,如果缓存路径选择SD卡上的缓存目录,即 /storage/emulated/0/Android/data/package_name/cache,那么当应用被卸载时,该目录也会被删除。

//初始化DiskLruCache。
        File directory = getFile(this,"DiskCache");
        if (!directory.exists()) {
            directory.mkdirs();
        }
        try {
            mDiskLruCache = DiskLruCache.open(directory, 1, 1, DISK_MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }

第二个参数表示应用的版本号,直接设置为1即可;第三个参数表示单个节点所对应的数据的个数,设置为1即可;第四个参数表示磁盘缓存的容量大小。

DiskLruCache的添加主要是由DiskLruCache.Editor来完成,首先我们会采用url的md5值来作为key,通过.Editor和key获取一个文件输出流,下载好图片通过这个文件输出流写入到文件系统中,最后通过editor.commit()的方法将文件提交才算真正将图片写入文件系统。
private void addDataToDisk(String url) {
        //采用url的md5值作为key。
        String key = hashKeyFromUrl(url);
        try {
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(0);
                if (downloadDataFromNet(url, outputStream)) {
                    //提交至缓存
                    editor.commit();
                } else {
                    //回退整个操作
                    editor.abort();
                }
                mDiskLruCache.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

private String hashKeyFromUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(url.getBytes());

            cacheKey = bytesToHexString(digest.digest());

        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.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();
    }

DiskLruCache的添加是通过Editor来完成,而查找是由DiskLruCache.Snapshot来完成的。首先通过url获取到当前文件的key值,初始化Snapshot后获取一个文件输入流,最后通过该文件输入流来解析出当前缓存的文件。

private Bitmap getDataFromDisk(String url) {
        Bitmap bitmap = null;
        String key = hashKeyFromUrl(url);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);
                return BitmapFactory.decodeStream(inputStream);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

五、主流的开源图片加载框架

Picasso:2013年Square发布的图片加载框架,Square公司开源了很多优秀的框架,如:okhttp,retrofit等,正所谓Square出品必属精品;

Glide:2014年Google员工提出的,并且在很多Google APP中大量采用,也是Google官方推荐的图片加载框架;

Fresco:2015年Facebook开源的图片框架。

1、Picasso设计与流程分析

 Picasso 收到加载及显示图片的任务,创建 Request 并将它交给 Dispatcher,Dispatcher 分发任务到具体 RequestHandler,任务通过 MemoryCache 及 Handler(数据获取接口) 获取图片,图片获取成功后通过 PicassoDrawable 显示到 Target 中。

注:Dispatcher 负责分发和处理 Action,包括提交、暂停、继续、取消、网络状态变化、重试等等。

2、Glide设计与流程分析

Glide是一个高效、开源、 Android设备上的媒体管理框架,Glide具有获取、解码和展示视频剧照、图片、动画等功能,它还有灵活的API,这些API使开发者能够将Glide应用在几乎任何网络协议栈里。创建Glide的主要目的有两个,一个是实现平滑的图片列表滚动效果(滚动流畅),另一个是支持远程图片的获取、大小调整和展示。简单的讲就是 Glide 收到加载及显示资源的任务,创建 Request 并将它交给RequestManager(任务管理器),Request 启动 Engine(数据获取引擎) 去数据源获取资源(通过 Fetcher(数据获取器) ),获取到后 Transformation(图片处理) 处理后交给 Target(目标)。

特点

  • GIF动画的解码:通过调用Glide.with(context).load(“图片路径“)方法,GIF动画图片可以自动显示为动画效果。如果想有更多的控制,还可以使用Glide.with(context).load(“图片路径“).asBitmap()方法加载静态图片,使用Glide.with(context).load(“图片路径“).asGif()方法加载动画图片
  • 本地视频剧照的解码:通过调用Glide.with(context).load(“视频路径“)方法,Glide能够支持Android设备中的所有视频剧照的加载和展示
  • 缩略图的支持:为了减少在同一个view组件里同时加载多张图片的时间,可以调用Glide.with(context).load(“图片路径“).thumbnail(“缩略比例“).into(“view组件“)方法加载一个缩略图,还可以控制thumbnail()中的参数的大小,以控制显示不同比例大小的缩略图
  • Activity生命周期的集成:当Activity暂停和重启时,Glide能够做到智能的暂停和重新开始请求,并且当Android设备的连接状态变化时,所有失败的请求能够自动重新请求
  • 转码的支持:Glide的toBytes() 和transcode() 两个方法可以用来获取、解码和变换背景图片,并且transcode() 方法还能够改变图片的样式
  • 动画的支持:新增支持图片的淡入淡出动画效果(调用crossFade()方法)和查看动画的属性的功能
  • OkHttp和Volley的支持:默认选择HttpUrlConnection作为网络协议栈,还可以选择OkHttp和Volley作为网络协议栈
  • 其他功能:如在图片加载过程中,使用Drawables对象作为占位符、图片请求的优化、图片的宽度和高度可重新设定、缩略图和原图的缓存等功能

3、Picasso 与 Glide对比

1)缓存方式不同

首先Picasso是2级缓存,它支持内存缓存而不支持磁盘缓存;而Glide是3级缓存,也就是说依次按照内存 > 磁盘 >网络的优先级来加载图片。再者,二者图片缓存的策略不同。将同一张网络图片加载到相同大小的ImageView中,Glide 加载的图片质量是不如Picasso的,原因是:Glide 加载图片默认的 Bitmap 格式是 RGB-565,一个像素点占32位 ,而 Picasso默认的格式是ARGB-8888 ,一个像素点占16位,所以Glide的内存开销要小一半。当然 Glide也可以通过GlideModule 将 Bitmap 格式转换到 ARGB-8888。还有一点, Picasso 是加载了全尺寸的图片到内存,下次在任何ImageView中加载图片的时候,全尺寸的图片将从缓存中取出,重新调整大小,然后缓存。而 Glide 可以按 ImageView 的大小来缓存的。**结论:Glide的这种方式优点是加载显示非常快,但同时也需要更大的空间来缓存,**加载图片是占用内存,glide几乎是Picasso的2倍。

2)针对生命周期的优化

Glide 的 with() 方法不光接受 Context,还能接收 Activity 和 Fragment的实例,,这样做的好处是:图片加载会和 Activity/Fragment 的生命周期保持一致,比如 Paused 状态在暂停加载,在 Resumed 的时候又自动重新加载。个人建议传参的时候传递Activity 和 Fragmen的实例t给Glide,而不是Context。

3)对GIF图片的支持

Glide可以加载GIF动态图,而Picasso不能。而且Glide加载动图的时候不需要做特别的配置,正常传入动图的url即可,它会自动识别。

4)需要引用库的大小

Picasso的大小大概100k,而Glide的大小大概500k。单纯这个大小还好,更重要的是Picasso和Glide的方法个数分别是840和2678个,这个差距还是很大的,对于DEX文件65535个方法的限制来说,2678是一个相当大的数字了,建议在使用Glide的时候开启ProGuard。

4、Fresco的介绍

Fresco与前面2者的差异较大,而且相对比较复杂,但是它可以说是综合了前面两个开源图片框架的有点,如果对图片要求比较高的应用,推荐使用它。

  • 优点:
  1. 图片存储在安卓系统的匿名共享内存, 而不是虚拟机的堆内存中, 图片的中间缓冲数据也存放在本地堆内存,所以, 应用程序有更多的内存使用, 不会因为图片加载而导致 oom, 同时也减少垃圾回收器频繁调用回收 Bitmap导致的界面卡顿, 性能更高.

  2. 渐进式加载 JPEG 图片, 支持图片从模糊到清晰加载,尤其是慢网络有极大的利好,可带来更好的用户体验。

  3. 图片可以以任意的中心点显示在 ImageView, 而不仅仅是图片的中心.

  4. JPEG 图片改变大小也是在 native 进行的, 不是在虚拟机的堆内存, 同样减少 OOM

  5. 很好的支持 GIF 图片的显示

  • 缺点
    1.包较大(2~3M)
    2.用法复杂

5、三大开源图片框架总结

       Glide能做到Picasso所能做到的一切,两者的区别是 Picasso 比 Glide 体积小很多且图像质量比 Glide 高,但Glide 的速度比 Picasso 更快,Glide 的长处是处理大型的图片流,如 gif、video,如果要制作视频类应用,Glide 当为首选。Fresco 可以说是综合了之前图片加载库的优点,但它的包很大,用法比较复杂,API不够简洁。 Fresco 在图片较多的应用中更能凸显其价值,如果应用没有太多图片需求,还是不推荐使用 Fresco,Glide基本就能满足你的需求

六、Glide框架原理分析

缓存一般有三级,内存缓存、硬盘、网络。

由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

Glide 必然也需要多个线程池,看下源码是不是这样

public final class GlideBuilder {
  ...
  private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
  private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
  ...
  private GlideExecutor animationExecutor; //动画线程池

图片异步加载成功,需要在主线程去更新ImageView,Glide与许多开源框架一样,需要通过Handler从子线程切换到Android主线程。

    class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
	  private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
	  //创建Handler
	  private static final Handler MAIN_THREAD_HANDLER =
	      new Handler(Looper.getMainLooper(), new MainThreadCallback());

Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。

// -> GlideBuilder#build
if (memoryCache == null) {
  memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
LruCache 采用**最近最少使用算法**,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。
    public class LruCache<K, V> {
	// 数据最终存在 LinkedHashMap 中
    private final LinkedHashMap<K, V> map;
	...
	public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
		// 创建一个LinkedHashMap,accessOrder 传true
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。

先看看LinkedHashMap 的原理吧

LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构

LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBeforeremove 方法,用于新增和删除链表节点。

LinkHashMap 的 put方法和get方法最后会调用trimToSize方法,LruCache 重写trimToSize方法,判断内存如果超过一定大小,则移除最老的数据。

LruCache小结:

  • LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
  • LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。

DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作:

	// DiskLruCache 内部也是用LinkedHashMap
	private final LinkedHashMap<String, Entry> lruEntries =
      	new LinkedHashMap<String, Entry>(0, 0.75f, true);
	...

    public synchronized boolean remove(String key) throws IOException {
	    checkNotClosed();
	    validateKey(key);
	    Entry entry = lruEntries.get(key);
	    if (entry == null || entry.currentEditor != null) {
	      return false;
	    }

            //一个key可能对应多个value,hash冲突的情况
	    for (int i = 0; i < valueCount; i++) {
	      File file = entry.getCleanFile(i);
            //通过 file.delete() 删除缓存文件,删除失败则抛异常
	      if (file.exists() && !file.delete()) {
	        throw new IOException("failed to delete " + file);
	      }
	      size -= entry.lengths[i];
	      entry.lengths[i] = 0;
	    }
	    ...
	    return true;
  }

可以看到 DiskLruCache 同样是利用LinkHashMap的特点,只不过数组里面存的 Entry 有点变化,Editor 用于操作文件。

private final class Entry {
    private final String key;

    private final long[] lengths;

    private boolean readable;

    private Editor currentEditor;

    private long sequenceNumber;
	...
}