Android学习总结之Glide自定义三级缓存(实战篇)

发布于:2025-05-15 ⋅ 阅读:(79) ⋅ 点赞:(0)

一、为什么需要三级缓存

内存缓存(Memory Cache)

内存缓存旨在快速显示刚浏览过的图片,例如在滑动列表时来回切换的图片。在 Glide 中,内存缓存使用 LruCache 算法(最近最少使用),能自动清理长时间未使用的图片,以此确保内存的合理利用。通常,内存缓存限制在手机可用内存的 15%。举例来说,若手机拥有 8GB 内存,内存缓存大约为 1.2GB。同时,为了进一步优化,图片会按屏幕尺寸进行压缩,比如原图为 2000px,而手机屏幕为 1000px,那么只存储 1000px 版本的图片。当内存缓存超出限制时,会自动清理超出部分的图片。

代码实现

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;

public class CustomGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 获取设备的最大内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        // 计算内存缓存的大小,这里设置为最大内存的15%
        int memoryCacheSize = maxMemory / 1024 / 1024 * 15;
        // 创建LruResourceCache对象
        builder.setMemoryCache(new LruResourceCache(memoryCacheSize));
    }
}

磁盘缓存(Disk Cache)

磁盘缓存用于存储常用但当前不在内存中的图片,像用户经常访问的商品详情页图片。Glide 通过 DiskLruCache 将图片存储在手机硬盘上,总容量一般设置为 100MB,并且优先存储高质量图片。为了优化存储,图片按 URL 哈希值命名文件,这样可以避免重复存储相同图片。同时,对于超过 7 天未使用的图片,会自动进行清理。

代码实现

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
import com.bumptech.glide.module.AppGlideModule;

public class CustomDiskCacheGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 设置磁盘缓存的路径
        String diskCachePath = context.getCacheDir().getPath() + "/glide_cache";
        // 设置磁盘缓存的大小为100MB
        int diskCacheSize = 1024 * 1024 * 100;
        // 创建DiskLruCacheFactory对象
        builder.setDiskCache(new DiskLruCacheFactory(diskCachePath, diskCacheSize));
    }
}

网络缓存(Network Cache)

网络缓存的作用是避免重复从服务器下载相同图片,这需要结合 HTTP 缓存头来实现。Glide 借助 OkHttp 的缓存机制,将图片存储在路由器或基站缓存中,总容量设置为 50MB,优先存储高频访问的图片。通过根据 HTTP 的 Cache-Control 头设置缓存时间(例如设置为 1 天),以及在图片 URL 中添加版本号(如 image_v2.jpg),当版本更新时强制重新下载,从而实现高效的网络缓存管理。

代码实现

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import okhttp3.Cache;
import okhttp3.OkHttpClient;

import java.io.InputStream;

public class CustomNetworkCacheGlideModule extends AppGlideModule {
    @Override
    public void registerComponents(Context context, Glide glide, Registry registry) {
        // 设置网络缓存的路径
        Cache cache = new Cache(context.getCacheDir(), 1024 * 1024 * 50);
        // 创建OkHttpClient对象并设置缓存
        OkHttpClient client = new OkHttpClient.Builder()
               .cache(cache)
               .build();
        // 注册OkHttpUrlLoader,让Glide使用OkHttp进行网络请求
        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
    }
}

整合代码: 

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;

/**
 * 自定义Glide内存缓存配置
 * 通过LruCache算法实现最近最少使用的图片自动回收
 */
public class CustomGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 获取应用可使用的最大内存(单位:字节)
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        // 计算内存缓存大小(15%的可用内存)
        int memoryCacheSize = maxMemory / 1024 / 1024 * 15;
        // 创建LruResourceCache并设置缓存大小
        builder.setMemoryCache(new LruResourceCache(memoryCacheSize));
    }
}

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
import com.bumptech.glide.module.AppGlideModule;

/**
 * 自定义Glide磁盘缓存配置
 * 使用DiskLruCache将图片持久化到本地存储
 */
public class CustomDiskCacheGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 设置磁盘缓存路径(应用缓存目录下的glide_cache文件夹)
        String diskCachePath = context.getCacheDir().getPath() + "/glide_cache";
        // 设置磁盘缓存大小(100MB)
        int diskCacheSize = 1024 * 1024 * 100;
        // 创建DiskLruCache工厂并设置路径和大小
        builder.setDiskCache(new DiskLruCacheFactory(diskCachePath, diskCacheSize));
    }
}

import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import okhttp3.Cache;
import okhttp3.OkHttpClient;

import java.io.InputStream;

/**
 * 自定义Glide网络缓存配置
 * 结合OkHttp实现HTTP级别的网络缓存
 */
public class CustomNetworkCacheGlideModule extends AppGlideModule {
    @Override
    public void registerComponents(Context context, Glide glide, Registry registry) {
        // 创建OkHttp缓存(50MB,位于应用缓存目录)
        Cache cache = new Cache(context.getCacheDir(), 1024 * 1024 * 50);
        // 构建带缓存的OkHttpClient
        OkHttpClient client = new OkHttpClient.Builder()
               .cache(cache)
               .build();
        // 注册OkHttp为Glide的网络请求引擎
        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
    }
}

常见问题解决方案

  1. 缓存穿透:缓存穿透指查询一个一定不存在的数据,由于缓存不命中需要从数据库查询,查不到数据则不写入缓存,导致该不存在的数据每次请求都要到数据库查询,给数据库带来压力。在 Glide 中,可以通过设置错误占位图、加载占位图和空值占位图来解决部分问题。例如:
Glide.with(context)
   .load(url)
   .error(R.drawable.ic_error) // 设置错误占位图
   .placeholder(R.drawable.ic_loading) // 设置加载占位图
   .fallback(R.drawable.ic_fallback) // 设置空值占位图
   .into(imageView);

在大厂面试中,关于缓存穿透常被问到的问题有:“请简述缓存穿透的概念以及可能的解决方案”。回答时,除了像上述代码那样通过 Glide 的占位图设置来应对外,还可以提及如使用布隆过滤器(Bloom Filter)等方案。布隆过滤器是一种空间效率极高的概率型数据结构,它利用位数组和哈希函数来判断一个元素是否在一个集合中。将所有已存在的数据 key 放入布隆过滤器中,当新的请求到来时,先通过布隆过滤器判断该 key 是否存在。如果不存在,直接返回,避免查询数据库,从而有效减少不必要的数据库查询,提高系统性能。

  1. 缓存雪崩:缓存雪崩是指在某一时刻,大量缓存同时失效,导致大量请求直接访问数据库,造成数据库压力过大甚至崩溃。可以通过设置不同的缓存过期时间来避免,例如:
int cacheDuration = TimeUnit.HOURS.toMillis(24) + new Random().nextInt(3600000);

面试中可能会被问到:“如何防止缓存雪崩的发生”。除了上述设置随机过期时间的方法外,还可以采用二级缓存策略,即设置主缓存和备用缓存。主缓存失效后,先从备用缓存获取数据,同时对主缓存进行异步更新,这样可以在一定程度上缓解大量请求直接冲击数据库的问题。另外,使用互斥锁也是一种思路,在缓存失效时,只有一个线程能够获取锁去更新缓存,其他线程等待,避免大量线程同时查询数据库。

  1. OOM 预防:OOM(Out Of Memory,内存溢出)在图片加载中较为常见,因为图片占用内存较大。可以通过使用 RGB_565 格式减少内存占用,例如:
// 使用RGB_565格式减少内存占用
Glide.with(context)
   .load(url)
   .format(DecodeFormat.PREFER_RGB_565)
   .into(imageView);

面试官可能会问:“在 Glide 中,如何预防 OOM 问题”。除了设置图片格式外,还可以根据设备内存情况动态调整图片尺寸。例如,获取设备的可用内存,当内存较低时,对图片进行更大比例的压缩。同时,合理配置 Glide 的内存缓存大小也很关键,避免缓存占用过多内存。此外,及时释放不再使用的图片资源,Glide 通过与 Activity 或 Fragment 的生命周期绑定,在界面不可见时及时清理相关图片资源,防止内存泄漏。

关键指标的获取途径

  1. 冷启动加载时间:借助 Android Profiler 的 Timeline 功能来精准测量。在应用启动时,启动 Profiler 并记录图片加载所耗费的时长。代码示例如下:
long startTime = System.currentTimeMillis();
Glide.with(this).load(url).into(imageView);
long duration = System.currentTimeMillis() - startTime;
Log.d("GlideTest", "加载耗时: " + duration + "ms");
  1. 内存峰值占用情况:使用 Android Profiler 的 Memory Monitor 进行监测。在滑动列表时,留意 Heap Size 的变化趋势,对比开启缓存前后 Bitmap 内存占用的差异,以此来优化内存使用。
  2. 缓存命中率计算:通过 Glide 的日志输出(设置 Glide.get (context).setLogLevel (Log.DEBUG)),从日志中筛选出 Fetched 和 Decoded 相关的条目。缓存命中率 = (内存命中数 + 磁盘命中数)÷ 总请求数 × 100%。
  3. FPS 帧率监控:采用 Android Profiler 的 FrameMetrics 功能。在滑动列表的过程中,记录丢帧的数量,确保平均帧率稳定在 55fps 以上,以保证流畅的用户体验。

二、自定义图片缓存框架

设计思路

  1. 内存缓存:运用 LruCache(Least Recently Used Cache,最近最少使用缓存)实现内存缓存,它能够自动回收最近最少使用的图片,保障内存的合理使用。
  2. 磁盘缓存:利用 DiskLruCache 实现磁盘缓存,将图片持久化到本地磁盘,方便在网络不可用或需要重复使用图片时快速获取。
  3. 多级缓存策略:首先从内存缓存中查找图片,若未找到则从磁盘缓存中查找,最后才从网络请求图片。当从网络获取到图片后,同时将其存入内存缓存和磁盘缓存。

代码实现

import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * 内存缓存实现
 * 使用LruCache(最近最少使用)算法管理内存中的图片
 */
public class MemoryCache {
    // LruCache实例,用于存储图片(键为图片URL,值为Bitmap)
    private LruCache<String, Bitmap> lruCache;

    public MemoryCache() {
        // 获取应用最大可用内存(KB)
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 设置缓存大小为最大内存的1/8
        int cacheSize = maxMemory / 8;

        // 初始化LruCache并重写sizeOf方法计算每个Bitmap的大小
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 返回Bitmap占用的内存大小(KB)
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    // 向缓存添加图片
    public void put(String key, Bitmap bitmap) {
        if (get(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    // 从缓存获取图片
    public Bitmap get(String key) {
        return lruCache.get(key);
    }

    // 从缓存移除图片
    public void remove(String key) {
        lruCache.remove(key);
    }
}

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import com.jakewharton.disklrucache.DiskLruCache;

import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 磁盘缓存实现
 * 使用DiskLruCache将图片持久化到本地存储
 */
public class DiskCache {
    // 应用版本(用于缓存版本控制)
    private static final int APP_VERSION = 1;
    // 每个缓存项对应的值数量
    private static final int VALUE_COUNT = 1;
    // 磁盘缓存最大容量(10MB)
    private static final long CACHE_SIZE = 10 * 1024 * 1024;

    // DiskLruCache实例
    private DiskLruCache diskLruCache;

    public DiskCache(Context context) {
        try {
            // 获取缓存目录
            File cacheDir = getDiskCacheDir(context, "bitmap");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            // 打开DiskLruCache实例
            diskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, CACHE_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 向磁盘缓存添加图片
    public void put(String key, Bitmap bitmap) {
        DiskLruCache.Editor editor = null;
        try {
            // 获取缓存编辑器
            editor = diskLruCache.edit(hashKeyForDisk(key));
            if (editor != null) {
                // 获取输出流并写入图片(JPEG格式,质量100%)
                OutputStream outputStream = editor.newOutputStream(0);
                if (bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
                outputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 从磁盘缓存获取图片
    public Bitmap get(String key) {
        try {
            // 获取缓存快照
            DiskLruCache.Snapshot snapshot = diskLruCache.get(hashKeyForDisk(key));
            if (snapshot != null) {
                // 从输入流解码Bitmap
                InputStream inputStream = snapshot.getInputStream(0);
                return BitmapFactory.decodeStream(inputStream);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 从磁盘缓存移除图片
    public void remove(String key) {
        try {
            diskLruCache.remove(hashKeyForDisk(key));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 获取磁盘缓存目录
    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);
    }

    // 生成URL的MD5哈希值作为缓存键
    private String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            // 使用MD5算法生成哈希值
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            // 若不支持MD5,使用普通哈希码
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    // 字节数组转十六进制字符串
    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xFF & b);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

import android.content.Context;
import android.graphics.Bitmap;

/**
 * 多级缓存管理器
 * 统一管理内存缓存和磁盘缓存
 */
public class ImageCacheManager {
    // 内存缓存实例
    private MemoryCache memoryCache;
    // 磁盘缓存实例
    private DiskCache diskCache;

    public ImageCacheManager(Context context) {
        memoryCache = new MemoryCache();
        diskCache = new DiskCache(context);
    }

    // 同时存入内存缓存和磁盘缓存
    public void put(String key, Bitmap bitmap) {
        memoryCache.put(key, bitmap);
        diskCache.put(key, bitmap);
    }

    // 优先从内存缓存获取,再从磁盘缓存获取
    public Bitmap get(String key) {
        Bitmap bitmap = memoryCache.get(key);
        if (bitmap != null) {
            return bitmap;
        }
        bitmap = diskCache.get(key);
        if (bitmap != null) {
            // 从磁盘读取后存入内存,提升下次访问速度
            memoryCache.put(key, bitmap);
        }
        return bitmap;
    }
}
  1. 请简述三级缓存(内存缓存、磁盘缓存、网络缓存)的作用和原理。

    • 内存缓存:旨在快速显示刚浏览过的图片,使用 LruCache 算法(最近最少使用),自动清理长时间未使用的图片,确保内存的合理利用。通常限制在手机可用内存的 15%。
    • 磁盘缓存:用于存储常用但当前不在内存中的图片,通过 DiskLruCache 将图片存储在手机硬盘上,设置总容量(如 100MB),优先存储高质量图片,按 URL 哈希值命名文件以避免重复存储,超过 7 天未使用的图片会自动清理。
    • 网络缓存:避免重复从服务器下载相同图片,结合 HTTP 缓存头,借助 OkHttp 的缓存机制,将图片存储在路由器或基站缓存中,设置总容量(如 50MB),优先存储高频访问的图片,根据 HTTP 的 Cache-Control 头设置缓存时间,并在图片 URL 中添加版本号以强制重新下载。
  2. 在自定义图片缓存框架中,LruCache 和 DiskLruCache 分别是如何实现的?

    • LruCache:在内存缓存类中,获取应用程序运行时的最大可用内存,使用最大可用内存的一部分(如 1/8)作为 LruCache 的缓存大小。重写 sizeOf 方法,计算每个图片对象占用的内存大小,通过 put 方法添加图片到缓存,get 方法获取图片,remove 方法移除图片。
    • DiskLruCache:在磁盘缓存类中,初始化时获取磁盘缓存的目录,打开 DiskLruCache 实例。put 方法通过获取编辑器和输出流,将图片以 JPEG 格式压缩并写入;get 方法通过获取快照和输入流,将输入流解码为 Bitmap 对象;remove 方法移除指定的图片。对键进行 MD5 哈希处理,确保键的唯一性。
  3. 如何防止缓存穿透、缓存雪崩和 OOM 问题?

    • 缓存穿透:在 Glide 中,可以通过设置错误占位图、加载占位图和空值占位图来解决部分问题。另外,可以使用布隆过滤器,将所有已存在的数据 key 放入布隆过滤器中,当新的请求到来时,先通过布隆过滤器判断该 key 是否存在,避免不必要的数据库查询。
    • 缓存雪崩:可以通过设置不同的缓存过期时间来避免,例如在设置缓存过期时间时,添加一个随机值。另外,采用二级缓存策略,设置主缓存和备用缓存,主缓存失效后,先从备用缓存获取数据,同时对主缓存进行异步更新。使用互斥锁,在缓存失效时,只有一个线程能够获取锁去更新缓存,其他线程等待。
    • OOM:在 Glide 中,可以使用 RGB_565 格式减少内存占用,根据设备内存情况动态调整图片尺寸,合理配置 Glide 的内存缓存大小,避免缓存占用过多内存。及时释放不再使用的图片资源,Glide 通过与 Activity 或 Fragment 的生命周期绑定,在界面不可见时及时清理相关图片资源,防止内存泄漏。

网站公告

今日签到

点亮在社区的每一天
去签到