Android Fresco 框架缓存模块源码深度剖析(二)
一、引言
在 Android 应用开发中,图片加载和处理是常见且重要的功能。频繁的图片加载不仅会消耗大量的网络流量,还会影响应用的性能和响应速度。因此,有效的缓存机制对于提升图片加载效率和用户体验至关重要。Fresco 是 Facebook 开源的一款强大的图片加载和显示库,其缓存模块设计精巧,能够高效地管理图片的内存缓存和磁盘缓存,减少图片的重复加载,从而显著提升应用的性能。
本文将深入剖析 Android Fresco 框架的缓存模块,从源码级别详细分析其实现原理、缓存策略、数据结构以及相关的操作流程。通过对缓存模块的深入理解,开发者可以更好地使用 Fresco 框架,优化应用的图片加载性能。
二、缓存模块概述
2.1 缓存模块的作用
Fresco 框架的缓存模块主要负责图片的缓存管理,包括内存缓存和磁盘缓存。其主要作用如下:
- 减少网络请求:通过缓存已经加载过的图片,避免重复的网络请求,从而节省网络流量。
- 提高加载速度:从缓存中获取图片比从网络下载图片要快得多,能够显著提升图片的加载速度,改善用户体验。
- 降低内存压力:合理的缓存策略可以有效地管理内存,避免因大量图片加载导致的内存溢出问题。
2.2 缓存模块的主要组件
Fresco 框架的缓存模块主要由以下几个组件构成:
- 内存缓存(Memory Cache) :用于缓存图片的解码结果,存储在内存中,访问速度快。
- 磁盘缓存(Disk Cache) :用于缓存图片的原始数据,存储在磁盘上,容量较大,可持久化存储。
- 缓存键(Cache Key) :用于唯一标识一个缓存项,确保缓存的准确性和一致性。
- 缓存策略(Cache Strategy) :定义了缓存的存储和清理规则,如缓存的大小限制、过期时间等。
2.3 缓存模块的架构
Fresco 框架的缓存模块采用分层架构,主要分为内存缓存层和磁盘缓存层。当应用请求一张图片时,首先会在内存缓存中查找,如果找到则直接返回;如果未找到,则会在磁盘缓存中查找;如果磁盘缓存中也未找到,则会从网络下载图片,并将其存储到磁盘缓存和内存缓存中。这种分层架构有效地提高了图片的加载效率,同时也保证了缓存的一致性和可靠性。
三、内存缓存(Memory Cache)
3.1 内存缓存的作用和特点
内存缓存是 Fresco 框架缓存模块的重要组成部分,它将图片的解码结果存储在内存中,具有以下作用和特点:
- 快速访问:内存的访问速度远远高于磁盘,因此从内存缓存中获取图片可以显著提高图片的加载速度。
- 临时存储:内存缓存中的数据是临时存储的,当应用退出或内存不足时,缓存的数据可能会被清除。
- 容量有限:由于内存资源有限,内存缓存的容量通常较小,需要合理管理缓存的大小,避免内存溢出。
3.2 内存缓存的实现类 - LruMemoryCache
Fresco 框架中,内存缓存的主要实现类是 LruMemoryCache
,它基于 LRU(Least Recently Used,最近最少使用)算法实现。LRU 算法的核心思想是,当缓存空间不足时,优先淘汰最近最少使用的缓存项。
以下是 LruMemoryCache
的部分源码分析:
java
// LruMemoryCache 类定义,实现了 MemoryCache 接口
public class LruMemoryCache<K, V> implements MemoryCache<K, V> {
// 缓存的最大容量
private final int mMaxCacheSize;
// 当前缓存的大小
private int mCurrentCacheSize;
// 存储缓存项的 LRU 链表
private final LinkedHashMap<K, V> mCache;
// 构造函数,初始化缓存的最大容量
public LruMemoryCache(int maxCacheSize) {
this.mMaxCacheSize = maxCacheSize;
// 初始化 LRU 链表,设置 accessOrder 为 true 以实现 LRU 算法
this.mCache = new LinkedHashMap<K, V>(0, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当当前缓存大小超过最大容量时,移除最旧的缓存项
return mCurrentCacheSize > mMaxCacheSize;
}
};
}
// 从缓存中获取数据
@Override
public V get(K key) {
// 同步访问缓存
synchronized (this) {
// 从 LRU 链表中获取数据
V value = mCache.get(key);
if (value != null) {
// 如果获取到数据,更新缓存项的访问顺序
mCache.get(key);
}
return value;
}
}
// 向缓存中添加数据
@Override
public V put(K key, V value) {
// 同步访问缓存
synchronized (this) {
// 计算新数据的大小
int itemSize = getSize(value);
// 如果新数据的大小超过最大容量,直接返回 null
if (itemSize > mMaxCacheSize) {
return null;
}
// 先移除旧的缓存项
V oldValue = mCache.remove(key);
if (oldValue != null) {
// 减少当前缓存的大小
mCurrentCacheSize -= getSize(oldValue);
}
// 将新数据添加到缓存中
mCache.put(key, value);
// 增加当前缓存的大小
mCurrentCacheSize += itemSize;
// 检查缓存大小是否超过最大容量,如果超过则移除最旧的缓存项
trimToSize(mMaxCacheSize);
return oldValue;
}
}
// 移除缓存项
@Override
public boolean remove(K key) {
// 同步访问缓存
synchronized (this) {
// 从 LRU 链表中移除缓存项
V value = mCache.remove(key);
if (value != null) {
// 减少当前缓存的大小
mCurrentCacheSize -= getSize(value);
return true;
}
return false;
}
}
// 调整缓存大小,确保不超过最大容量
private void trimToSize(int maxSize) {
// 同步访问缓存
synchronized (this) {
while (mCurrentCacheSize > maxSize) {
// 获取最旧的缓存项
Map.Entry<K, V> eldest = mCache.entrySet().iterator().next();
if (eldest != null) {
// 移除最旧的缓存项
K key = eldest.getKey();
V value = eldest.getValue();
mCache.remove(key);
// 减少当前缓存的大小
mCurrentCacheSize -= getSize(value);
}
}
}
}
// 计算缓存项的大小
private int getSize(V value) {
// 这里可以根据具体的缓存项类型实现不同的大小计算逻辑
return 1;
}
}
3.2.1 源码分析
- 构造函数:初始化缓存的最大容量
mMaxCacheSize
和存储缓存项的LinkedHashMap
。LinkedHashMap
的accessOrder
参数设置为true
,表示按照访问顺序排序,从而实现 LRU 算法。 - get 方法:从缓存中获取数据,并更新缓存项的访问顺序。
- put 方法:向缓存中添加数据,先移除旧的缓存项,再添加新的数据,并检查缓存大小是否超过最大容量,如果超过则移除最旧的缓存项。
- remove 方法:从缓存中移除指定的缓存项,并更新当前缓存的大小。
- trimToSize 方法:调整缓存大小,确保不超过最大容量。
- getSize 方法:计算缓存项的大小,具体实现可以根据缓存项的类型进行调整。
3.3 内存缓存的使用示例
以下是一个简单的使用 LruMemoryCache
的示例:
java
// 创建一个最大容量为 10 的内存缓存
LruMemoryCache<String, String> memoryCache = new LruMemoryCache<>(10);
// 向缓存中添加数据
memoryCache.put("key1", "value1");
memoryCache.put("key2", "value2");
// 从缓存中获取数据
String value1 = memoryCache.get("key1");
if (value1 != null) {
System.out.println("Value for key1: " + value1);
}
// 移除缓存项
memoryCache.remove("key1");
3.4 内存缓存的管理和优化
为了更好地管理和优化内存缓存,Fresco 框架提供了以下几种机制:
-
内存修剪(Memory Trimming) :当系统内存不足时,Fresco 会自动触发内存修剪操作,减少内存缓存的大小,以避免应用被系统杀死。
-
缓存清理(Cache Eviction) :根据 LRU 算法,当缓存空间不足时,优先淘汰最近最少使用的缓存项。
-
缓存大小配置:开发者可以根据应用的需求和设备的内存情况,合理配置内存缓存的最大容量。
以下是一个内存修剪的示例代码:
java
// 实现 MemoryTrimmable 接口,用于处理内存修剪事件
public class MyMemoryTrimmable implements MemoryTrimmable {
private final LruMemoryCache<String, String> memoryCache;
public MyMemoryTrimmable(LruMemoryCache<String, String> memoryCache) {
this.memoryCache = memoryCache;
}
@Override
public void trim(MemoryTrimLevel trimLevel) {
switch (trimLevel) {
case OnCloseToDalvikHeapLimit:
case OnSystemLowMemoryWhileAppInBackground:
case OnSystemLowMemoryWhileAppInForeground:
// 当系统内存不足时,减少缓存大小
memoryCache.trimToSize(memoryCache.getMaxCacheSize() / 2);
break;
}
}
}
// 注册内存修剪监听器
MemoryTrimmableRegistry memoryTrimmableRegistry = NoOpMemoryTrimmableRegistry.getInstance();
memoryTrimmableRegistry.registerMemoryTrimmable(new MyMemoryTrimmable(memoryCache));
四、磁盘缓存(Disk Cache)
4.1 磁盘缓存的作用和特点
磁盘缓存是 Fresco 框架缓存模块的另一个重要组成部分,它将图片的原始数据存储在磁盘上,具有以下作用和特点:
- 持久化存储:磁盘缓存中的数据可以持久化存储,即使应用退出或设备重启,缓存的数据仍然存在。
- 大容量存储:磁盘的存储空间通常比内存大得多,因此磁盘缓存可以存储更多的图片数据。
- 访问速度较慢:相比于内存缓存,磁盘缓存的访问速度较慢,因为需要进行磁盘 I/O 操作。
4.2 磁盘缓存的实现类 - BufferedDiskCache
Fresco 框架中,磁盘缓存的主要实现类是 BufferedDiskCache
,它基于文件系统实现,将图片数据存储在磁盘文件中。
以下是 BufferedDiskCache
的部分源码分析:
java
// BufferedDiskCache 类定义,实现了 DiskCache 接口
public class BufferedDiskCache implements DiskCache {
// 磁盘缓存的目录
private final File mCacheDir;
// 缓存的最大容量
private final long mMaxCacheSize;
// 当前缓存的大小
private long mCurrentCacheSize;
// 存储缓存项信息的映射
private final Map<String, CacheEntry> mCacheEntries;
// 构造函数,初始化缓存目录和最大容量
public BufferedDiskCache(File cacheDir, long maxCacheSize) {
this.mCacheDir = cacheDir;
this.mMaxCacheSize = maxCacheSize;
this.mCacheEntries = new HashMap<>();
// 初始化缓存,计算当前缓存的大小
initializeCache();
}
// 初始化缓存,计算当前缓存的大小
private void initializeCache() {
if (!mCacheDir.exists()) {
mCacheDir.mkdirs();
}
File[] files = mCacheDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
mCurrentCacheSize += file.length();
// 记录缓存项信息
mCacheEntries.put(file.getName(), new CacheEntry(file.getName(), file.length()));
}
}
}
// 检查缓存大小是否超过最大容量,如果超过则进行清理
trimToSize(mMaxCacheSize);
}
// 从缓存中获取数据
@Override
public InputStream get(String key) {
// 构建缓存文件的路径
File file = new File(mCacheDir, getFileName(key));
if (file.exists()) {
try {
// 打开文件输入流
return new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
return null;
}
// 向缓存中添加数据
@Override
public void put(String key, InputStream data) {
// 构建缓存文件的路径
File file = new File(mCacheDir, getFileName(key));
try {
// 创建文件输出流
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = data.read(buffer)) != -1) {
// 将数据写入文件
fos.write(buffer, 0, bytesRead);
}
fos.close();
data.close();
// 更新当前缓存的大小
long fileSize = file.length();
mCurrentCacheSize += fileSize;
// 记录缓存项信息
mCacheEntries.put(file.getName(), new CacheEntry(file.getName(), fileSize));
// 检查缓存大小是否超过最大容量,如果超过则进行清理
trimToSize(mMaxCacheSize);
} catch (IOException e) {
e.printStackTrace();
}
}
// 移除缓存项
@Override
public boolean remove(String key) {
// 构建缓存文件的路径
File file = new File(mCacheDir, getFileName(key));
if (file.exists()) {
// 删除文件
boolean deleted = file.delete();
if (deleted) {
// 更新当前缓存的大小
mCurrentCacheSize -= file.length();
// 移除缓存项信息
mCacheEntries.remove(file.getName());
return true;
}
}
return false;
}
// 调整缓存大小,确保不超过最大容量
private void trimToSize(long maxSize) {
while (mCurrentCacheSize > maxSize) {
// 找到最旧的缓存项
CacheEntry eldestEntry = null;
long eldestTimestamp = Long.MAX_VALUE;
for (CacheEntry entry : mCacheEntries.values()) {
if (entry.timestamp < eldestTimestamp) {
eldestTimestamp = entry.timestamp;
eldestEntry = entry;
}
}
if (eldestEntry != null) {
// 移除最旧的缓存项
File file = new File(mCacheDir, eldestEntry.fileName);
if (file.exists()) {
boolean deleted = file.delete();
if (deleted) {
// 更新当前缓存的大小
mCurrentCacheSize -= file.length();
// 移除缓存项信息
mCacheEntries.remove(eldestEntry.fileName);
}
}
}
}
}
// 根据缓存键生成文件名
private String getFileName(String key) {
// 这里可以使用哈希算法生成唯一的文件名
return key.hashCode() + ".cache";
}
// 缓存项信息类
private static class CacheEntry {
final String fileName;
final long size;
final long timestamp;
CacheEntry(String fileName, long size) {
this.fileName = fileName;
this.size = size;
this.timestamp = System.currentTimeMillis();
}
}
}
4.2.1 源码分析
- 构造函数:初始化缓存目录和最大容量,并调用
initializeCache
方法初始化缓存,计算当前缓存的大小。 - initializeCache 方法:检查缓存目录是否存在,如果不存在则创建。遍历缓存目录下的所有文件,计算当前缓存的大小,并记录缓存项信息。最后检查缓存大小是否超过最大容量,如果超过则进行清理。
- get 方法:根据缓存键构建缓存文件的路径,检查文件是否存在,如果存在则打开文件输入流并返回。
- put 方法:根据缓存键构建缓存文件的路径,创建文件输出流,将数据写入文件。更新当前缓存的大小,记录缓存项信息,并检查缓存大小是否超过最大容量,如果超过则进行清理。
- remove 方法:根据缓存键构建缓存文件的路径,检查文件是否存在,如果存在则删除文件,并更新当前缓存的大小和缓存项信息。
- trimToSize 方法:调整缓存大小,确保不超过最大容量。找到最旧的缓存项,删除对应的文件,并更新当前缓存的大小和缓存项信息。
- getFileName 方法:根据缓存键生成唯一的文件名,这里使用哈希算法生成文件名。
- CacheEntry 类:用于记录缓存项的信息,包括文件名、大小和时间戳。
4.3 磁盘缓存的使用示例
以下是一个简单的使用 BufferedDiskCache
的示例:
java
// 创建磁盘缓存实例
File cacheDir = new File("/sdcard/my_cache");
BufferedDiskCache diskCache = new BufferedDiskCache(cacheDir, 1024 * 1024 * 10); // 最大容量为 10MB
// 向缓存中添加数据
try {
String key = "image1";
InputStream data = new ByteArrayInputStream("Hello, World!".getBytes());
diskCache.put(key, data);
} catch (Exception e) {
e.printStackTrace();
}
// 从缓存中获取数据
try {
String key = "image1";
InputStream data = diskCache.get(key);
if (data != null) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = data.read(buffer)) != -1) {
System.out.write(buffer, 0, bytesRead);
}
data.close();
}
} catch (IOException e) {
e.printStackTrace();
}
// 移除缓存项
boolean removed = diskCache.remove("image1");
if (removed) {
System.out.println("Cache item removed successfully.");
}
4.4 磁盘缓存的管理和优化
为了更好地管理和优化磁盘缓存,Fresco 框架提供了以下几种机制:
- 缓存清理(Cache Eviction) :根据缓存项的时间戳,优先淘汰最旧的缓存项,以确保缓存空间不超过最大容量。
- 缓存大小配置:开发者可以根据应用的需求和设备的存储情况,合理配置磁盘缓存的最大容量。
- 文件管理:使用合适的文件名和目录结构,方便缓存文件的管理和查找。
五、缓存键(Cache Key)
5.1 缓存键的作用和特点
缓存键是用于唯一标识一个缓存项的字符串,它在缓存模块中起着至关重要的作用。缓存键的主要作用和特点如下:
- 唯一性:每个缓存项都有一个唯一的缓存键,确保缓存的准确性和一致性。
- 确定性:相同的输入应该生成相同的缓存键,保证缓存的可重复性。
- 可读性:缓存键应该具有一定的可读性,方便调试和管理。
5.2 缓存键的生成策略
Fresco 框架提供了多种缓存键的生成策略,常见的有以下几种:
-
基于 URI 的缓存键:使用图片的 URI 作为缓存键,确保不同 URI 的图片有不同的缓存项。
-
基于 URI 和参数的缓存键:除了 URI 之外,还考虑图片的请求参数,如裁剪尺寸、旋转角度等,确保不同参数的图片有不同的缓存项。
-
哈希缓存键:使用哈希算法对 URI 和参数进行哈希处理,生成唯一的哈希值作为缓存键,减少缓存键的长度,提高存储效率。
以下是一个基于 URI 和参数的缓存键生成示例:
java
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
// 缓存键生成器类
public class CacheKeyGenerator {
// 生成缓存键的方法
public static String generateCacheKey(URI uri, Map<String, String> params) {
StringBuilder keyBuilder = new StringBuilder();
// 添加 URI 到缓存键
keyBuilder.append(uri.toString());
if (params != null && !params.isEmpty()) {
// 对参数进行排序,确保相同参数的顺序生成相同的缓存键
java.util.TreeMap<String, String> sortedParams = new java.util.TreeMap<>(params);
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
keyBuilder.append("&");
keyBuilder.append(entry.getKey());
keyBuilder.append("=");
keyBuilder.append(entry.getValue());
}
}
// 对缓存键进行哈希处理
return hash(keyBuilder.toString());
}
// 哈希处理方法,这里简单使用 hashCode 方法
private static String hash(String input) {
return String.valueOf(input.hashCode());
}
public static void main(String[] args) {
URI uri = URI.create("https://example.com/image.jpg");
Map<String, String> params = new HashMap<>();
params.put("width", "200");
params.put("height", "300");
String cacheKey = generateCacheKey(uri, params);
System.out.println("Cache Key: " + cacheKey);
}
}
5.2.1 源码分析
- generateCacheKey 方法:首先将 URI 添加到缓存键中,然后对参数进行排序并添加到缓存键中。最后对缓存键进行哈希处理,生成最终的缓存键。
- hash 方法:这里简单使用
hashCode
方法对输入进行哈希处理,实际应用中可以使用更复杂的哈希算法,如 MD5、SHA-1 等。
5.3 缓存键的使用示例
在使用内存缓存和磁盘缓存时,需要使用缓存键来进行数据的存储和获取。以下是一个使用缓存键的示例:
java
// 创建内存缓存实例
LruMemoryCache<String, String> memoryCache = new LruMemoryCache<>(10);
// 生成缓存键
URI uri = URI.create("https://example.com/image.jpg");
Map<String, String> params = new HashMap<>();
params.put("width", "200");
params.put("height", "300");
String cacheKey = CacheKeyGenerator.generateCacheKey(uri, params);
// 向内存缓存中添加数据
memoryCache.put(cacheKey, "Image data");
// 从内存缓存中获取数据
String data = memoryCache.get(cacheKey);
if (data != null) {
System.out.println("Cached data: " + data);
}
六、缓存策略(Cache Strategy)
6.1 缓存策略的作用和分类
缓存策略定义了缓存的存储和清理规则,它在缓存模块中起着至关重要的作用。合理的缓存策略可以有效地提高缓存的命中率,减少内存和磁盘的使用,从而提升应用的性能。常见的缓存策略可以分为以下几类:
- 基于时间的缓存策略:根据缓存项的存储时间来决定是否清理缓存项,如设置缓存项的过期时间,超过过期时间的缓存项将被清理。
- 基于大小的缓存策略:根据缓存的大小来决定是否清理缓存项,如设置缓存的最大容量,当缓存大小超过最大容量时,优先清理最近最少使用的缓存项。
- 基于访问频率的缓存策略:根据缓存项的访问频率来决定是否清理缓存项,如优先清理访问频率较低的缓存项。
6.2 基于时间的缓存策略实现
以下是一个简单的基于时间的缓存策略实现示例:
java
import java.util.HashMap;
import java.util.Map;
// 基于时间的缓存类
public class TimeBasedCache<K, V> {
// 存储缓存项的映射
private final Map<K, CacheEntry<V>> cache;
// 缓存项的过期时间(毫秒)
private final long expirationTime;
// 构造函数,初始化过期时间
public TimeBasedCache(long expirationTime) {
this.cache = new HashMap<>();
this.expirationTime = expirationTime;
}
// 从缓存中获取数据
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry != null) {
// 检查缓存项是否过期
if (System.currentTimeMillis() - entry.timestamp < expirationTime) {
return entry.value;
} else {
// 缓存项已过期,移除缓存项
cache.remove(key);
}
}
return null;
}
// 向缓存中添加数据
public void put(K key, V value) {
// 创建新的缓存项
CacheEntry<V> entry = new CacheEntry<>(value, System.currentTimeMillis());
cache.put(key, entry);
}
// 缓存项信息类
private static class CacheEntry<V> {
final V value;
final long timestamp;
CacheEntry(V value, long timestamp) {
this.value = value;
this.timestamp = timestamp;
}
}
public static void main(String[] args) {
// 创建一个过期时间为 10 秒的缓存
TimeBasedCache<String, String> cache = new TimeBasedCache<>(10 * 1000);
// 向缓存中添加数据
cache.put("key1", "value1");
// 从缓存中获取数据
String value = cache.get("key1");
if
java
import java.util.HashMap;
import java.util.Map;
// 基于时间的缓存类
public class TimeBasedCache<K, V> {
// 存储缓存项的映射
private final Map<K, CacheEntry<V>> cache;
// 缓存项的过期时间(毫秒)
private final long expirationTime;
// 构造函数,初始化过期时间
public TimeBasedCache(long expirationTime) {
this.cache = new HashMap<>();
this.expirationTime = expirationTime;
}
// 从缓存中获取数据
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry != null) {
// 检查缓存项是否过期
if (System.currentTimeMillis() - entry.timestamp < expirationTime) {
return entry.value;
} else {
// 缓存项已过期,移除缓存项
cache.remove(key);
}
}
return null;
}
// 向缓存中添加数据
public void put(K key, V value) {
// 创建新的缓存项
CacheEntry<V> entry = new CacheEntry<>(value, System.currentTimeMillis());
cache.put(key, entry);
}
// 缓存项信息类
private static class CacheEntry<V> {
final V value;
final long timestamp;
CacheEntry(V value, long timestamp) {
this.value = value;
this.timestamp = timestamp;
}
}
public static void main(String[] args) {
// 创建一个过期时间为 10 秒的缓存
TimeBasedCache<String, String> cache = new TimeBasedCache<>(10 * 1000);
// 向缓存中添加数据
cache.put("key1", "value1");
// 从缓存中获取数据
String value = cache.get("key1");
if (value != null) {
System.out.println("Cached value: " + value);
}
try {
// 等待 11 秒,让缓存项过期
Thread.sleep(11 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次从缓存中获取数据
value = cache.get("key1");
if (value == null) {
System.out.println("Cache item has expired.");
}
}
}
6.2.1 源码分析
- 构造函数:初始化一个
HashMap
用于存储缓存项,并设置缓存项的过期时间。 - get 方法:从缓存中获取指定键对应的值。首先检查缓存项是否存在,如果存在则判断其是否过期。若未过期则返回值,若已过期则移除该缓存项并返回
null
。 - put 方法:向缓存中添加新的键值对。创建一个新的
CacheEntry
对象,记录当前时间戳,并将其存入HashMap
中。 - CacheEntry 类:用于存储缓存项的值和存储时间戳。
- main 方法:创建一个过期时间为 10 秒的缓存实例,添加一个缓存项,获取该缓存项,然后等待 11 秒使缓存项过期,再次获取该缓存项,验证过期机制。
6.3 基于大小的缓存策略实现
Fresco 框架中的 LruMemoryCache
就是基于大小的缓存策略的典型实现,这里再给出一个简化版的示例:
java
import java.util.LinkedHashMap;
import java.util.Map;
// 基于大小的缓存类
public class SizeBasedCache<K, V> {
// 缓存的最大容量
private final int maxSize;
// 存储缓存项的 LRU 链表
private final LinkedHashMap<K, V> cache;
// 构造函数,初始化最大容量
public SizeBasedCache(int maxSize) {
this.maxSize = maxSize;
// 初始化 LRU 链表,设置 accessOrder 为 true 以实现 LRU 算法
this.cache = new LinkedHashMap<K, V>(0, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当当前缓存大小超过最大容量时,移除最旧的缓存项
return size() > maxSize;
}
};
}
// 从缓存中获取数据
public V get(K key) {
return cache.get(key);
}
// 向缓存中添加数据
public void put(K key, V value) {
cache.put(key, value);
}
public static void main(String[] args) {
// 创建一个最大容量为 3 的缓存
SizeBasedCache<String, String> cache = new SizeBasedCache<>(3);
// 向缓存中添加数据
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
// 此时缓存已满
System.out.println("Cache size: " + cache.cache.size());
// 再添加一个新的缓存项,会触发 LRU 机制移除最旧的缓存项
cache.put("key4", "value4");
System.out.println("Cache size after adding key4: " + cache.cache.size());
// 检查最旧的缓存项是否已被移除
if (cache.get("key1") == null) {
System.out.println("Key1 has been removed from cache.");
}
}
}
6.3.1 源码分析
- 构造函数:初始化缓存的最大容量和
LinkedHashMap
。LinkedHashMap
的accessOrder
参数设置为true
,使其按照访问顺序排序,从而实现 LRU 算法。 - get 方法:从缓存中获取指定键对应的值。
- put 方法:向缓存中添加新的键值对。如果添加后缓存大小超过最大容量,
removeEldestEntry
方法会被调用,移除最旧的缓存项。 - main 方法:创建一个最大容量为 3 的缓存实例,添加 3 个缓存项使缓存已满,再添加一个新的缓存项,验证 LRU 机制是否移除了最旧的缓存项。
6.4 基于访问频率的缓存策略实现
java
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
// 基于访问频率的缓存类
public class FrequencyBasedCache<K, V> {
// 缓存的最大容量
private final int maxSize;
// 存储缓存项的映射
private final Map<K, CacheEntry<V>> cache;
// 按访问频率排序的优先队列
private final PriorityQueue<CacheEntry<V>> frequencyQueue;
// 构造函数,初始化最大容量
public FrequencyBasedCache(int maxSize) {
this.maxSize = maxSize;
this.cache = new HashMap<>();
this.frequencyQueue = new PriorityQueue<>((a, b) -> a.accessCount - b.accessCount);
}
// 从缓存中获取数据
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry != null) {
// 增加访问次数
entry.accessCount++;
// 从优先队列中移除该缓存项
frequencyQueue.remove(entry);
// 重新插入优先队列,以更新排序
frequencyQueue.add(entry);
return entry.value;
}
return null;
}
// 向缓存中添加数据
public void put(K key, V value) {
if (cache.size() >= maxSize) {
// 缓存已满,移除访问频率最低的缓存项
CacheEntry<V> leastFrequentEntry = frequencyQueue.poll();
if (leastFrequentEntry != null) {
cache.remove(leastFrequentEntry.key);
}
}
// 创建新的缓存项
CacheEntry<V> newEntry = new CacheEntry<>(key, value, 1);
cache.put(key, newEntry);
frequencyQueue.add(newEntry);
}
// 缓存项信息类
private static class CacheEntry<V> {
final K key;
final V value;
int accessCount;
CacheEntry(K key, V value, int accessCount) {
this.key = key;
this.value = value;
this.accessCount = accessCount;
}
}
public static void main(String[] args) {
// 创建一个最大容量为 3 的缓存
FrequencyBasedCache<String, String> cache = new FrequencyBasedCache<>(3);
// 向缓存中添加数据
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
// 访问 key1 两次,使其访问频率增加
cache.get("key1");
cache.get("key1");
// 再添加一个新的缓存项,会移除访问频率最低的缓存项
cache.put("key4", "value4");
// 检查访问频率最低的缓存项是否已被移除
if (cache.get("key2") == null) {
System.out.println("Key2 has been removed from cache.");
}
}
}
6.4.1 源码分析
- 构造函数:初始化缓存的最大容量、
HashMap
用于存储缓存项,以及一个优先队列PriorityQueue
用于按访问频率对缓存项进行排序。 - get 方法:从缓存中获取指定键对应的值。如果缓存项存在,增加其访问次数,从优先队列中移除该缓存项,再重新插入以更新排序。
- put 方法:向缓存中添加新的键值对。如果缓存已满,移除优先队列中访问频率最低的缓存项。创建新的缓存项并添加到
HashMap
和优先队列中。 - CacheEntry 类:用于存储缓存项的键、值和访问次数。
- main 方法:创建一个最大容量为 3 的缓存实例,添加 3 个缓存项,增加
key1
的访问频率,再添加一个新的缓存项,验证是否移除了访问频率最低的缓存项。
七、缓存模块的交互流程
7.1 图片请求时的缓存查找流程
当应用发起一个图片请求时,Fresco 框架的缓存模块会按照以下流程进行缓存查找:
java
// 图片请求处理类
public class ImageRequestProcessor {
private final LruMemoryCache<String, Image> memoryCache;
private final BufferedDiskCache diskCache;
public ImageRequestProcessor(LruMemoryCache<String, Image> memoryCache, BufferedDiskCache diskCache) {
this.memoryCache = memoryCache;
this.diskCache = diskCache;
}
public Image processRequest(String imageUrl) {
// 生成缓存键
String cacheKey = CacheKeyGenerator.generateCacheKey(imageUrl, null);
// 首先在内存缓存中查找
Image image = memoryCache.get(cacheKey);
if (image != null) {
System.out.println("Image found in memory cache.");
return image;
}
// 内存缓存中未找到,在磁盘缓存中查找
InputStream diskData = diskCache.get(cacheKey);
if (diskData != null) {
System.out.println("Image found in disk cache.");
// 从磁盘数据中解码图片
image = decodeImage(diskData);
// 将图片存入内存缓存
memoryCache.put(cacheKey, image);
return image;
}
// 磁盘缓存中也未找到,从网络下载图片
System.out.println("Image not found in cache, downloading from network...");
image = downloadImage(imageUrl);
if (image != null) {
// 将图片存入磁盘缓存
diskCache.put(cacheKey, encodeImage(image));
// 将图片存入内存缓存
memoryCache.put(cacheKey, image);
}
return image;
}
private Image decodeImage(InputStream data) {
// 解码图片的逻辑,这里简单返回 null 表示未实现
return null;
}
private InputStream encodeImage(Image image) {
// 编码图片的逻辑,这里简单返回 null 表示未实现
return null;
}
private Image downloadImage(String imageUrl) {
// 从网络下载图片的逻辑,这里简单返回 null 表示未实现
return null;
}
public static void main(String[] args) {
// 创建内存缓存和磁盘缓存实例
LruMemoryCache<String, Image> memoryCache = new LruMemoryCache<>(10);
File cacheDir = new File("/sdcard/my_cache");
BufferedDiskCache diskCache = new BufferedDiskCache(cacheDir, 1024 * 1024 * 10);
ImageRequestProcessor processor = new ImageRequestProcessor(memoryCache, diskCache);
// 处理图片请求
Image image = processor.processRequest("https://example.com/image.jpg");
if (image != null) {
System.out.println("Image loaded successfully.");
}
}
}
7.1.1 源码分析
-
构造函数:初始化内存缓存和磁盘缓存实例。
-
processRequest 方法:
- 生成图片的缓存键。
- 首先在内存缓存中查找图片,如果找到则直接返回。
- 若内存缓存中未找到,在磁盘缓存中查找。若找到,解码图片并将其存入内存缓存。
- 若磁盘缓存中也未找到,从网络下载图片,将其存入磁盘缓存和内存缓存。
-
decodeImage 方法:用于解码图片,这里未实现具体逻辑。
-
encodeImage 方法:用于编码图片,这里未实现具体逻辑。
-
downloadImage 方法:用于从网络下载图片,这里未实现具体逻辑。
-
main 方法:创建内存缓存、磁盘缓存和请求处理器实例,处理图片请求并输出结果。
7.2 缓存更新和清理流程
7.2.1 缓存更新
当图片数据发生变化时,需要更新缓存。以下是一个简单的缓存更新示例:
java
public class CacheUpdater {
private final LruMemoryCache<String, Image> memoryCache;
private final BufferedDiskCache diskCache;
public CacheUpdater(LruMemoryCache<String, Image> memoryCache, BufferedDiskCache diskCache) {
this.memoryCache = memoryCache;
this.diskCache = diskCache;
}
public void updateCache(String imageUrl, Image newImage) {
// 生成缓存键
String cacheKey = CacheKeyGenerator.generateCacheKey(imageUrl, null);
// 更新内存缓存
memoryCache.put(cacheKey, newImage);
// 更新磁盘缓存
diskCache.put(cacheKey, encodeImage(newImage));
}
private InputStream encodeImage(Image image) {
// 编码图片的逻辑,这里简单返回 null 表示未实现
return null;
}
public static void main(String[] args) {
// 创建内存缓存和磁盘缓存实例
LruMemoryCache<String, Image> memoryCache = new LruMemoryCache<>(10);
File cacheDir = new File("/sdcard/my_cache");
BufferedDiskCache diskCache = new BufferedDiskCache(cacheDir, 1024 * 1024 * 10);
CacheUpdater updater = new CacheUpdater(memoryCache, diskCache);
// 模拟新的图片数据
Image newImage = null;
updater.updateCache("https://example.com/image.jpg", newImage);
}
}
7.2.1.1 源码分析
-
构造函数:初始化内存缓存和磁盘缓存实例。
-
updateCache 方法:
- 生成图片的缓存键。
- 更新内存缓存中的图片数据。
- 更新磁盘缓存中的图片数据。
-
encodeImage 方法:用于编码图片,这里未实现具体逻辑。
-
main 方法:创建内存缓存、磁盘缓存和缓存更新器实例,模拟更新图片缓存。
7.2.2 缓存清理
缓存清理可以根据不同的策略进行,如定期清理、内存不足时清理等。以下是一个简单的定期清理示例:
java
import java.util.Timer;
import java.util.TimerTask;
public class CacheCleaner {
private final LruMemoryCache<String, Image> memoryCache;
private final BufferedDiskCache diskCache;
public CacheCleaner(LruMemoryCache<String, Image> memoryCache, BufferedDiskCache diskCache) {
this.memoryCache = memoryCache;
this.diskCache = diskCache;
}
public void startPeriodicCleaning(long interval) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 清理内存缓存
memoryCache.trimToSize(memoryCache.getMaxCacheSize() / 2);
// 清理磁盘缓存
diskCache.trimToSize(diskCache.getMaxCacheSize() / 2);
}
}, 0, interval);
}
public static void main(String[] args) {
// 创建内存缓存和磁盘缓存实例
LruMemoryCache<String, Image> memoryCache = new LruMemoryCache<>(10);
File cacheDir = new File("/sdcard/my_cache");
BufferedDiskCache diskCache = new BufferedDiskCache(cacheDir, 1024 * 1024 * 10);
CacheCleaner cleaner = new CacheCleaner(memoryCache, diskCache);
// 开始定期清理,每隔 1 小时清理一次
cleaner.startPeriodicCleaning(1000 * 60 * 60);
}
}
7.2.2.1 源码分析
- 构造函数:初始化内存缓存和磁盘缓存实例。
- startPeriodicCleaning 方法:使用
Timer
和TimerTask
实现定期清理。每隔指定的时间间隔,调用trimToSize
方法清理内存缓存和磁盘缓存,将缓存大小调整为最大容量的一半。 - main 方法:创建内存缓存、磁盘缓存和缓存清理器实例,开始定期清理,每隔 1 小时清理一次。
八、缓存模块的性能优化
8.1 缓存命中率优化
- 合理设置缓存大小:根据应用的使用场景和设备的内存、存储情况,合理设置内存缓存和磁盘缓存的大小。如果缓存太小,可能会导致频繁的缓存失效和重新加载;如果缓存太大,可能会占用过多的系统资源。
- 优化缓存键生成策略:确保缓存键的生成能够准确反映图片的唯一性,避免因缓存键冲突导致的缓存命中率下降。可以考虑使用更复杂的哈希算法和更多的请求参数来生成缓存键。
- 使用预加载机制:对于一些经常使用的图片,可以提前将其加载到缓存中,提高缓存命中率。例如,在应用启动时预加载一些常用的图片。
8.2 缓存读写性能优化
-
内存缓存优化:
- 使用高效的数据结构:如
LruMemoryCache
中使用的LinkedHashMap
,可以快速地进行插入、删除和查找操作。 - 减少内存拷贝:在存储和获取图片数据时,尽量减少内存拷贝的次数,提高内存使用效率。
- 使用高效的数据结构:如
-
磁盘缓存优化:
- 采用异步读写:使用异步 I/O 操作进行磁盘读写,避免阻塞主线程,提高应用的响应速度。
- 合理设置缓存文件的存储结构:如按图片类型、时间等进行分类存储,方便查找和管理。
8.3 缓存清理策略优化
- 智能清理:根据缓存项的使用频率、过期时间等因素,智能地选择要清理的缓存项。例如,优先清理访问频率低、过期时间长的缓存项。
- 增量清理:在进行缓存清理时,采用增量清理的方式,每次只清理一部分缓存项,避免一次性清理过多缓存项导致的性能波动。
九、缓存模块的扩展性分析
9.1 自定义缓存实现
开发者可以根据自己的需求自定义缓存实现,只需要实现 MemoryCache
或 DiskCache
接口即可。以下是一个简单的自定义内存缓存实现示例:
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.MemoryCache;
import com.facebook.imagepipeline.image.CloseableImage;
import java.util.HashMap;
import java.util.Map;
// 自定义内存缓存实现
public class CustomMemoryCache implements MemoryCache<CacheKey, CloseableReference<CloseableImage>> {
private final Map<CacheKey, CloseableReference<CloseableImage>> cache;
public CustomMemoryCache() {
this.cache = new HashMap<>();
}
@Override
public CloseableReference<CloseableImage> get(CacheKey key) {
return cache.get(key);
}
@Override
public CloseableReference<CloseableImage> cache(CacheKey key, CloseableReference<CloseableImage> value) {
cache.put(key, value);
return value;
}
@Override
public boolean contains(CacheKey key) {
return cache.containsKey(key);
}
@Override
public boolean remove(CacheKey key) {
return cache.remove(key) != null;
}
@Override
public int removeAll(RemoveCondition<CacheKey> condition) {
int removedCount = 0;
for (Map.Entry<CacheKey, CloseableReference<CloseableImage>> entry : cache.entrySet()) {
if (condition.shouldRemove(entry.getKey())) {
cache.remove(entry.getKey());
removedCount++;
}
}
return removedCount;
}
}
9.1.1 源码分析
- 构造函数:初始化一个
HashMap
用于存储缓存项。 - get 方法:从缓存中获取指定键对应的值。
- cache 方法:向缓存中添加新的键值对。
- contains 方法:检查缓存中是否包含指定的键。
- remove 方法:从缓存中移除指定的键值对。
- removeAll 方法:根据指定的条件移除缓存项。
9.2 自定义缓存策略
开发者也可以自定义缓存策略,只需要实现相应的缓存策略接口即可。以下是一个简单的自定义基于时间和大小的混合缓存策略实现示例:
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.MemoryCache;
import com.facebook.imagepipeline.image.CloseableImage;
import java.util.*;
// 自定义混合缓存策略
public class CustomCacheStrategy implements MemoryCache<CacheKey, CloseableReference<CloseableImage>> {
private final Map<CacheKey, CacheEntry<CloseableReference<CloseableImage>>> cache;
private final int maxSize;
private final long expirationTime;
public CustomCacheStrategy(int maxSize, long expirationTime) {
this.cache = new HashMap<>();
this.maxSize = maxSize;
this.expirationTime = expirationTime;
}
@Override
public CloseableReference<CloseableImage> get(CacheKey key) {
CacheEntry<CloseableReference<CloseableImage>> entry = cache.get(key);
if (entry != null) {
if (System.currentTimeMillis() - entry.timestamp < expirationTime) {
return entry.value;
} else {
cache.remove(key);
}
}
return null;
}
@Override
public CloseableReference<CloseableImage> cache(CacheKey key, CloseableReference<CloseableImage> value) {
if (cache.size() >= maxSize) {
// 按时间排序,移除最旧的缓存项
List<CacheEntry<CloseableReference<CloseableImage>>> entries = new ArrayList<>(cache.values());
entries.sort((a, b) -> Long.compare(a.timestamp, b.timestamp));
CacheEntry<CloseableReference<CloseableImage>> oldestEntry = entries.get(0);
cache.remove(oldestEntry.key);
}
CacheEntry<CloseableReference<CloseableImage>> newEntry = new CacheEntry<>(key, value, System.currentTimeMillis());
cache.put(key, newEntry);
return value;
}
@Override
public boolean contains(CacheKey key) {
CacheEntry<CloseableReference<CloseableImage>> entry = cache.get(key);
return entry != null && System.currentTimeMillis() - entry.timestamp < expirationTime;
}
@Override
public boolean remove(CacheKey key) {
return cache.remove(key) != null;
}
@Override
public int removeAll(RemoveCondition<CacheKey> condition) {
int removedCount = 0;
for (Map.Entry<CacheKey, CacheEntry<CloseableReference<CloseableImage>>> entry : cache.entrySet()) {
if (condition.shouldRemove(entry.getKey())) {
cache.remove(entry.getKey());
removedCount++;
}
}
return removedCount;
}
private static class CacheEntry<V> {
final CacheKey key;
final V value;
final long timestamp;
CacheEntry(CacheKey key, V value, long timestamp) {
this.key = key;
this.value = value;
this.timestamp = timestamp;
}
}
}
9.2.1 源码分析
-
构造函数:初始化缓存的最大容量和缓存项的过期时间。
-
get 方法:从缓存中获取指定键对应的值。检查缓存项是否过期,若过期则移除该缓存项。
-
cache 方法:向缓存中添加新的键值对。如果缓存已满,按时间排序移除最旧的缓存项。
-
contains 方法:检查缓存中是否包含指定的
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.MemoryCache;
import com.facebook.imagepipeline.image.CloseableImage;
import java.util.*;
// 自定义混合缓存策略
public class CustomCacheStrategy implements MemoryCache<CacheKey, CloseableReference<CloseableImage>> {
private final Map<CacheKey, CacheEntry<CloseableReference<CloseableImage>>> cache;
private final int maxSize;
private final long expirationTime;
public CustomCacheStrategy(int maxSize, long expirationTime) {
this.cache = new HashMap<>();
this.maxSize = maxSize;
this.expirationTime = expirationTime;
}
@Override
public CloseableReference<CloseableImage> get(CacheKey key) {
CacheEntry<CloseableReference<CloseableImage>> entry = cache.get(key);
if (entry != null) {
if (System.currentTimeMillis() - entry.timestamp < expirationTime) {
return entry.value;
} else {
cache.remove(key);
}
}
return null;
}
@Override
public CloseableReference<CloseableImage> cache(CacheKey key, CloseableReference<CloseableImage> value) {
if (cache.size() >= maxSize) {
// 按时间排序,移除最旧的缓存项
List<CacheEntry<CloseableReference<CloseableImage>>> entries = new ArrayList<>(cache.values());
entries.sort((a, b) -> Long.compare(a.timestamp, b.timestamp));
CacheEntry<CloseableReference<CloseableImage>> oldestEntry = entries.get(0);
cache.remove(oldestEntry.key);
}
CacheEntry<CloseableReference<CloseableImage>> newEntry = new CacheEntry<>(key, value, System.currentTimeMillis());
cache.put(key, newEntry);
return value;
}
@Override
public boolean contains(CacheKey key) {
CacheEntry<CloseableReference<CloseableImage>> entry = cache.get(key);
return entry != null && System.currentTimeMillis() - entry.timestamp < expirationTime;
}
@Override
public boolean remove(CacheKey key) {
return cache.remove(key) != null;
}
@Override
public int removeAll(RemoveCondition<CacheKey> condition) {
int removedCount = 0;
for (Map.Entry<CacheKey, CacheEntry<CloseableReference<CloseableImage>>> entry : cache.entrySet()) {
if (condition.shouldRemove(entry.getKey())) {
cache.remove(entry.getKey());
removedCount++;
}
}
return removedCount;
}
private static class CacheEntry<V> {
final CacheKey key;
final V value;
final long timestamp;
CacheEntry(CacheKey key, V value, long timestamp) {
this.key = key;
this.value = value;
this.timestamp = timestamp;
}
}
}
9.2.1 源码分析
-
contains 方法:
- 此方法用于检查缓存中是否包含指定的键。它首先通过
cache.get(key)
尝试获取对应的缓存项。 - 若缓存项存在,会进一步检查该缓存项是否过期。通过比较当前时间与缓存项存储时间戳的差值和设定的过期时间
expirationTime
,若未过期则返回true
,否则返回false
。
- 此方法用于检查缓存中是否包含指定的键。它首先通过
-
remove 方法:
- 该方法用于从缓存中移除指定键的缓存项。它调用
cache.remove(key)
尝试移除对应的键值对,若移除成功则返回true
,反之返回false
。
- 该方法用于从缓存中移除指定键的缓存项。它调用
-
removeAll 方法:
- 该方法可根据指定的条件移除缓存项。它遍历
cache
中的所有键值对,对于每个键值对,调用condition.shouldRemove(entry.getKey())
判断是否需要移除。 - 若需要移除,则调用
cache.remove(entry.getKey())
移除该键值对,并将移除计数removedCount
加 1。最后返回移除的缓存项数量。
- 该方法可根据指定的条件移除缓存项。它遍历
-
CacheEntry 类:
- 这是一个内部静态类,用于封装缓存项的信息。包含缓存键
key
、缓存值value
以及存储时间戳timestamp
。 - 其构造函数用于初始化这些信息,确保每个缓存项都能记录其存储时间,以便后续进行过期检查和排序操作。
- 这是一个内部静态类,用于封装缓存项的信息。包含缓存键
9.3 与其他组件的集成扩展性
Fresco 缓存模块具备良好的扩展性,能够与其他组件进行集成,以满足不同的业务需求。
9.3.1 与网络请求组件的集成
Fresco 可以与各种网络请求组件集成,当缓存中未找到所需图片时,通过网络请求组件从网络下载图片。以下是一个简单的与 OkHttp 集成的示例:
java
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import okhttp3.OkHttpClient;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
// 创建 OkHttp 客户端
OkHttpClient okHttpClient = new OkHttpClient();
// 使用 OkHttp 配置 ImagePipeline
ImagePipelineConfig config = OkHttpImagePipelineConfigFactory
.newBuilder(this, okHttpClient)
.build();
// 初始化 Fresco
Fresco.initialize(this, config);
// 创建图片请求
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(Uri.parse("https://example.com/image.jpg"))
.build();
// 获取 ImagePipeline 实例
ImagePipeline imagePipeline = Fresco.getImagePipeline();
// 获取数据源
DataSource<CloseableReference<CloseableImage>> dataSource =
imagePipeline.fetchDecodedImage(request, this);
// 订阅数据源
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(Bitmap bitmap) {
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
// 处理失败情况
}
}, CallerThreadExecutor.getInstance());
}
}
9.3.1.1 源码分析
- 创建 OkHttp 客户端:使用
OkHttpClient
创建一个 OkHttp 客户端实例,用于处理网络请求。 - 配置 ImagePipeline:通过
OkHttpImagePipelineConfigFactory
使用 OkHttp 客户端配置ImagePipelineConfig
,确保 Fresco 使用 OkHttp 进行网络请求。 - 初始化 Fresco:调用
Fresco.initialize
方法,传入配置好的ImagePipelineConfig
对 Fresco 进行初始化。 - 创建图片请求:使用
ImageRequestBuilder
创建一个图片请求,指定图片的来源 URL。 - 获取数据源:通过
ImagePipeline
的fetchDecodedImage
方法获取图片的数据源。 - 订阅数据源:使用
BaseBitmapDataSubscriber
订阅数据源,当图片加载成功时,将图片显示在ImageView
上;若加载失败,则处理相应的失败情况。
9.3.2 与数据加密组件的集成
为了保证缓存数据的安全性,可以将 Fresco 缓存模块与数据加密组件集成。以下是一个简单的示例,展示如何在存储和读取缓存数据时进行加密和解密:
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.DiskCache;
import com.facebook.imagepipeline.image.EncodedImage;
import java.io.*;
import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
// 加密磁盘缓存实现
public class EncryptedDiskCache implements DiskCache {
private final DiskCache delegate;
private final Cipher encryptCipher;
private final Cipher decryptCipher;
public EncryptedDiskCache(DiskCache delegate) throws Exception {
this.delegate = delegate;
// 生成加密密钥
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128, new SecureRandom());
SecretKey secretKey = keyGen.generateKey();
// 初始化加密和解密 Cipher
encryptCipher = Cipher.getInstance("AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey);
decryptCipher = Cipher.getInstance("AES");
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
}
@Override
public CloseableReference<EncodedImage> get(CacheKey key) {
CloseableReference<EncodedImage> result = delegate.get(key);
if (result != null) {
try {
EncodedImage encodedImage = result.get();
InputStream inputStream = encodedImage.getInputStream();
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, decryptCipher);
encodedImage.setInputStream(cipherInputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
@Override
public void put(CacheKey key, EncodedImage encodedImage) {
try {
InputStream inputStream = encodedImage.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(byteArrayOutputStream, encryptCipher);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
cipherOutputStream.write(buffer, 0, bytesRead);
}
cipherOutputStream.close();
ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
EncodedImage encryptedEncodedImage = EncodedImage.wrapStream(encryptedInputStream, encodedImage.getSize());
delegate.put(key, encryptedEncodedImage);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean containsSync(CacheKey key) {
return delegate.containsSync(key);
}
@Override
public void remove(CacheKey key) {
delegate.remove(key);
}
@Override
public void clearAll() {
delegate.clearAll();
}
}
9.3.2.1 源码分析
-
构造函数:
- 接收一个
DiskCache
实例作为委托对象,用于实际的缓存操作。 - 使用
KeyGenerator
生成一个 AES 加密密钥。 - 初始化加密和解密的
Cipher
对象,分别用于加密和解密缓存数据。
- 接收一个
-
get 方法:
- 调用委托对象的
get
方法获取缓存的图片数据。 - 若获取到数据,使用
CipherInputStream
对数据进行解密,并更新EncodedImage
的输入流。
- 调用委托对象的
-
put 方法:
- 从
EncodedImage
中获取输入流,使用CipherOutputStream
对数据进行加密。 - 将加密后的数据封装成新的
EncodedImage
对象,并调用委托对象的put
方法将其存入缓存。
- 从
-
containsSync 方法:直接调用委托对象的
containsSync
方法,检查缓存中是否包含指定的键。 -
remove 方法:直接调用委托对象的
remove
方法,移除指定键的缓存项。 -
clearAll 方法:直接调用委托对象的
clearAll
方法,清空所有缓存项。
十、缓存模块的错误处理与调试
10.1 错误处理机制
在缓存模块中,可能会遇到各种错误,如磁盘读写错误、内存不足等。Fresco 提供了相应的错误处理机制,确保在出现错误时能够进行恰当的处理。
10.1.1 磁盘缓存错误处理
在磁盘缓存操作中,可能会出现文件读写错误。以下是一个处理磁盘缓存写入错误的示例:
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.DiskCache;
import com.facebook.imagepipeline.image.EncodedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
// 增强的磁盘缓存实现,处理写入错误
public class EnhancedDiskCache implements DiskCache {
private final DiskCache delegate;
public EnhancedDiskCache(DiskCache delegate) {
this.delegate = delegate;
}
@Override
public CloseableReference<EncodedImage> get(CacheKey key) {
return delegate.get(key);
}
@Override
public void put(CacheKey key, EncodedImage encodedImage) {
try {
delegate.put(key, encodedImage);
} catch (Exception e) {
// 处理写入错误
handleWriteError(e);
}
}
private void handleWriteError(Exception e) {
// 记录错误日志
System.err.println("Disk cache write error: " + e.getMessage());
// 可以添加其他处理逻辑,如重试机制
}
@Override
public boolean containsSync(CacheKey key) {
return delegate.containsSync(key);
}
@Override
public void remove(CacheKey key) {
delegate.remove(key);
}
@Override
public void clearAll() {
delegate.clearAll();
}
}
10.1.1.1 源码分析
- 构造函数:接收一个
DiskCache
实例作为委托对象,用于实际的缓存操作。 - put 方法:调用委托对象的
put
方法将图片数据存入磁盘缓存。若出现异常,调用handleWriteError
方法处理写入错误。 - handleWriteError 方法:记录错误日志,打印错误信息。开发者可以在此方法中添加其他处理逻辑,如重试机制,以提高系统的健壮性。
- 其他方法:
get
、containsSync
、remove
和clearAll
方法直接调用委托对象的相应方法。
10.1.2 内存缓存错误处理
在内存缓存操作中,可能会出现内存不足的情况。以下是一个处理内存缓存溢出错误的示例:
java
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.cache.CacheKey;
import com.facebook.imagepipeline.cache.MemoryCache;
import com.facebook.imagepipeline.image.CloseableImage;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// 增强的内存缓存实现,处理内存溢出错误
public class EnhancedMemoryCache implements MemoryCache<CacheKey, CloseableReference<CloseableImage>> {
private final MemoryCache<CacheKey, CloseableReference<CloseableImage>> delegate;
private final Map<CacheKey, CloseableReference<CloseableImage>> tempCache;
public EnhancedMemoryCache(MemoryCache<CacheKey, CloseableReference<CloseableImage>> delegate) {
this.delegate = delegate;
this.tempCache = new ConcurrentHashMap<>();
}
@Override
public CloseableReference<CloseableImage> get(CacheKey key) {
CloseableReference<CloseableImage> result = delegate.get(key);
if (result == null) {
result = tempCache.get(key);
}
return result;
}
@Override
public CloseableReference<CloseableImage> cache(CacheKey key, CloseableReference<CloseableImage> value) {
try {
return delegate.cache(key, value);
} catch (OutOfMemoryError e) {
// 处理内存溢出错误
handleOutOfMemoryError(e, key, value);
return null;
}
}
private void handleOutOfMemoryError(OutOfMemoryError e, CacheKey key, CloseableReference<CloseableImage> value) {
// 记录错误日志
System.err.println("Memory cache out of memory error: " + e.getMessage());
// 将数据存入临时缓存
tempCache.put(key, value);
// 可以添加其他处理逻辑,如清理部分缓存
}
@Override
public boolean contains(CacheKey key) {
return delegate.contains(key) || tempCache.containsKey(key);
}
@Override
public boolean remove(CacheKey key) {
boolean result = delegate.remove(key);
result = result || tempCache.remove(key) != null;
return result;
}
@Override
public int removeAll(RemoveCondition<CacheKey> condition) {
int removedCount = delegate.removeAll(condition);
for (Map.Entry<CacheKey, CloseableReference<CloseableImage>> entry : tempCache.entrySet()) {
if (condition.shouldRemove(entry.getKey())) {
tempCache.remove(entry.getKey());
removedCount++;
}
}
return removedCount;
}
}
10.1.2.1 源码分析
- 构造函数:接收一个
MemoryCache
实例作为委托对象,用于实际的缓存操作。同时初始化一个临时缓存tempCache
,用于在内存溢出时存储数据。 - get 方法:先从委托对象的缓存中获取数据,若未找到则从临时缓存中获取。
- cache 方法:调用委托对象的
cache
方法将数据存入内存缓存。若出现OutOfMemoryError
异常,调用handleOutOfMemoryError
方法处理内存溢出错误。 - handleOutOfMemoryError 方法:记录错误日志,将数据存入临时缓存。开发者可以在此方法中添加其他处理逻辑,如清理部分缓存,以释放内存。
- contains 方法:检查委托对象的缓存或临时缓存中是否包含指定的键。
- remove 方法:从委托对象的缓存和临时缓存中移除指定的键值对。
- removeAll 方法:根据指定的条件从委托对象的缓存和临时缓存中移除缓存项。
10.2 调试技巧
在开发和调试过程中,以下技巧可以帮助开发者更好地理解和排查缓存模块的问题。
10.2.1 日志调试
Fresco 提供了详细的日志输出,开发者可以通过设置日志级别来查看不同详细程度的日志信息。以下是一个设置日志级别的示例:
java
import com.facebook.common.logging.FLog;
// 设置日志级别为 VERBOSE
FLog.setMinimumLoggingLevel(FLog.VERBOSE);
10.2.1.1 源码分析
FLog
是 Fresco 中用于日志记录的类,通过调用 setMinimumLoggingLevel
方法并传入 FLog.VERBOSE
,可以将日志级别设置为详细模式,从而查看最详细的日志信息,帮助开发者定位问题。
10.2.2 性能监测
开发者可以使用 Android Studio 的性能监测工具来监测缓存模块的性能。例如,使用 Memory Profiler 监测内存缓存的使用情况,使用 Disk Profiler 监测磁盘缓存的读写操作。
10.2.3 断点调试
在关键代码处设置断点,逐步执行代码,观察变量的值和程序的执行流程,有助于深入理解缓存模块的工作原理和排查问题。例如,在 get
和 put
方法中设置断点,观察缓存的读取和写入操作。
十一、缓存模块的未来发展趋势
11.1 对新存储技术的支持
随着存储技术的不断发展,如固态硬盘(SSD)、分布式存储等,Fresco 缓存模块可能会增加对这些新存储技术的支持,以提高缓存的读写性能和可靠性。
11.2 智能化缓存策略
未来的缓存策略可能会更加智能化,能够根据应用的使用场景、设备的性能和用户的行为习惯,动态调整缓存的大小、过期时间和清理策略,以提供更高效的缓存服务。
11.3 与新兴技术的集成
Fresco 缓存模块可能会与新兴技术进行集成,如人工智能、机器学习等。例如,利用机器学习算法预测用户可能请求的图片,提前将其缓存,以提高图片的加载速度。
11.4 安全性增强
随着数据安全和隐私问题的日益重要,Fresco 缓存模块可能会加强对缓存数据的加密和保护,确保用户的图片数据在缓存过程中的安全性。
十二、总结
本文对 Android Fresco 框架的缓存模块进行了深入的源码级别分析。首先介绍了缓存模块的概述,包括其作用、主要组件和架构。接着详细分析了内存缓存和磁盘缓存的实现类,包括 LruMemoryCache
和 BufferedDiskCache
,以及它们的工作原理和使用示例。然后探讨了缓存键的生成策略、缓存策略的实现方式,以及缓存模块的交互流程、性能优化和扩展性分析。最后介绍了缓存模块的错误处理与调试技巧,以及未来的发展趋势。
通过对 Fresco 缓存模块的深入理解,开发者可以更好地使用该框架,优化应用的图片加载性能,提高用户体验。同时,也可以根据自己的需求对缓存模块进行定制和扩展,以满足不同的业务场景。随着技术的不断发展,Fresco 缓存模块也将不断演进和完善,为 Android 开发者提供更强大的图片缓存功能。