大家好,又见面了,我是你们的朋友全栈君。
文章目录
在 Android 应用中不可避免地要显示很多图片,如果不做处理,不管图片是否显示过,每次启动时都需要从网络拉取,这就极大影响了图片加载速度和浪费用户流量,并且整个应用中的图片内存无法控制在一个总的范围内。因此,图片缓存在一个图片加载模块中很重要并且不可缺少。
目前比较流行的图片框架,如 Glide、Fresco等,都使用了“内存-本地-网络”三级缓存策略。首先应用程序访问网络拉取图片,分别将加载的图片保存在本地存储和内存中,当程序再一次需要加载图片时,先判断内存中是否有缓存,有则直接从内存中拉取,否则查看本地缓存目录中是否有缓存,本地缓存目录中如果存在缓存,则从本地缓存卡中拉取,否则从网络加载图片。三级缓存的实现逻辑如图所示。
内存缓存
使用软引用或弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但现在不建议开发者使用这种方案。从 API 9(Android 2.3)开始,Android 系统垃圾回收器更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠,并且从Android 3.0(API Level 11)开始,图片的数据无法用一种可预见的方式将其释放,这就存在潜在的内存溢出风险。
使用 LruCache 来实现图片内存管理是一种可靠的方式,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除
接下来使用 LruCache 实现一个图片的内存缓存
public class MemoryCache {
private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
private LruCache<String, Bitmap> mMemoryCache;
private final String TAG = "MemoryCache";
public MemoryCache(float sizePer) {
init(sizePer);
}
/**
* 初始化 LruCache,
*
* @param sizePer 参数 sizePer 是设置图片内存池的 size,因为不同的 ROM,会根据硬
* 件配置给应用下发不同的最大堆大小,所以使用一个百分比作为参数,一般的应用分配 1/8
* 的堆大小就能满足,图片较大的应用可以适当调整大小。
*/
private void init(float sizePer) {
int cacheSize = DEFAULT_MEM_CACHE_SIZE;
if (sizePer > 0) {
cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
}
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
/**
* sizeOf(String key,Bitmap value),这个方法是必须重写的
* @return 返回加入的缓存对象大小,这里是返回一个新的 Bitmap 对象内存大小。
*/
@Override
protected int sizeOf(String key, Bitmap value) {
final int size = getBitmapSize(value) / 1024;
return size == 0 ? 1 : size;
}
/**
* 重写 entryRemoved(boolean evicted,String key,Bitmap oldValue,Bitmap newValue)
* 这个方法在把最近最少使用的对象在缓存值达到预设定值之前从内存中移除时会回调,其中参数 oldValue 是即将要从
* LruCache 中移除的对象,这里重写是因为后面会用到。
*/
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
}
/**
* 获取bitmap大小
*/
public int getBitmapSize(Bitmap bitmap) {
return bitmap.getAllocationByteCount();
}
/**
* 通过 url 获取内存中的 Bitmap 对象,如果没有,则返回 null,需要进行下一步的加载图
* 片逻辑。
*/
public Bitmap getBitmap(String url) {
Bitmap bitmap = null;
if (mMemoryCache != null) {
bitmap = mMemoryCache.get(url);
}
return bitmap;
}
/**
* 增加一个新的 Bitmap 到内存池中,url 为 KEY。
*/
public void addBitmapToCache(String url, Bitmap bitmap) {
if (url == null || bitmap == null) {
return;
}
mMemoryCache.put(url, bitmap);
}
/**
* 清空缓存
*/
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
}
这样就实现了一个简单的图片缓存管理类,在对图片解码后,把图片放到内存中,代码如下:
if (bitmap !=null){
memoryCache.addBitmapToCache(url , bitmap);
}
在加载图片前,先尝试从 MemoryCache 中获取,实现如下代码:
Bitmap bitmap = memoryCache.getBitmap(url);
if (bitmap !=null){
// 设置展示到ImageView
}else{
// 加载Bitmap
}
这样,当解码出一组新图片时,都会缓存到 LruCache 中,在下次获取图片时,首先从LruCache 中拉取,但 LruCache 的大小是有一定限制的,从前面的内容可以看到,LruCache的大小是系统分配给应用的最大堆内存的百分比,具体如下所示:
cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024)
这个百分比多少合适呢,可以从以下几个点来考虑。
- 应用中内存的占用情况,除了图片以外,是否还有大内存的数据需要缓存到内存。
- 在应用中大部分情况要同时显示多少张图片,优先保证最大图片的显示数量的缓存支持。
- Bitmap 的规格,计算出一张图片占用的内存大小。
- 图片访问的频率。
缓存的大小需要设计好,一个过小的缓存不但没有任何好处,还会引起额外的开销,一个过大的缓存可能使 java.lang.OutOfMemory 异常的概率增加,并且应用剩余部分只留下很小的内存,导致其他数据缓存空间变小。
在应用中,如果有一些图片的访问频率要比其他的大一些,或者必须一直显示出来,就需要一直保持在内存中,这种情况可以使用多个 LruCache 对象来管理多组Bitmap,对 Bitmap 进行分级,不同级别的 Bitmap 放到不同的 LruCahe 中。
Bitmap内存复用
Bitmap 内存复用,从 Android 3.0 版本开始支持 Bitmap 内存复用,也就是 BitmapFactory.Options.inBitmap 属性,如果这个属性被设置有效的目标复用对象,decode 方法就在加载内容时重用已经存在的 Bitmap。这意味着 Bitmap 的内存被重新利用,这可以减少内存的分配与回收,提高图片的性能。在 MemoryCache 的基础上增加内存复用的功能。使用 mReusableBitmaps 存放可重用 Bitmap 的软引用的集合,代码如下:
private Set<SoftReference<Bitmap>> mReuseableBitmaps;
private void init(float sizePer) {
int cacheSize = DEFAULT_MEM_CACHE_SIZE;
if (sizePer > 0) {
cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
}
mReuseableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
/**
* sizeOf(String key,Bitmap value),这个方法是必须重写的
* @return 返回加入的缓存对象大小,这里是返回一个新的 Bitmap 对象内存大小。
*/
@Override
protected int sizeOf(String key, Bitmap value) {
final int size = getBitmapSize(value) / 1024;
return size == 0 ? 1 : size;
}
/**
* 重写 entryRemoved(boolean evicted,String key,Bitmap oldValue,Bitmap newValue)
* 这个方法在把最近最少使用的对象在缓存值达到预设定值之前从内存中移除时会回调,其中参数 oldValue 是即将要从
* LruCache 中移除的对象,这里重写是因为后面会用到。
*/
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
// 添加
mReuseableBitmaps.add(new SoftReference<Bitmap>(oldValue));
}
};
}
在需要分配一个新的 Bitmap 对象时,首先检查是否有可内存复用的 Bitmap 对象。代码如下:
public static Bitmap decodeSampledBitmapFromStream(InputStream in, BitmapFactory.Options options, ImageCache imageCache) {
addInBitmapOptions(options, imageCache);
return BitmapFactory.decodeStream(in, null, options);
}
public static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}
}
从代码中可以看到addInBitmapOptions 方 法 在 可 重 用 的 集 合 中 , 通 过cache.getBitmap-FromReusableSet(options)方法查找一个合适的 bitmap 赋值给 inBitmap。getBitmapFrom-ReusableSet 方法如下
public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReuseableBitmaps != null && !mReuseableBitmaps.isEmpty()) {
final Iterator<SoftReference<Bitmap>> iterator = mReuseableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
if (canUseForInBitmap(item, options)) {
bitmap = item;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}
}
return bitmap;
}
getBitmapFromReusableSet 方法从软引用集合 mReusableBitmaps 中查找规格可利用的Bitmap 作为内存复用对象,因为使用 inBitmap 有一些限制,在 Android 4.4 之前,只支持同等大小的位图。因此通过 canUseForInBitmap(item,options)方法来判断该 Bitmap 是否可以复用,代码如下:只要满足条件,就实现内存复用。
private boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options options) {
int width = options.outWidth / options.inSampleSize;
int height = options.outHeight / options.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
磁盘缓存
内存缓存在访问最近浏览过的 Bitmap 是很有效的,能明显提高显示的速度,然而并不是所有的图片在这个缓存中都是有效的。最典型的场景就是 GridView 等一类带有大数据的组件,可以很快填满内存缓存,一旦用户继续浏览,应用就不得不再次处理每一张图片。
如果增加一级磁盘缓存,在磁盘缓存中保存加载过的图片,在图片在内存缓存中失效的地方从本地读取图片数据,再解码,就可以减少图片从网络加载所需的时间。当然,从磁盘上获取这些图片要比从内存中加载慢,并且由于磁盘读取时间是不可预知的,所以图片的解码和文件读取都应该在后台进程中完成。
DiskLruCache 是 Android 提供的一个管理磁盘缓存的类。该类可用于在程序中把从网络加载的数据保存到磁盘上作为缓存数据,DiskLruCache 并不是 Google 官方提供的,是获得官方认可的一个开源工具类
引入DiskLRUCache库如下
implementation 'com.jakewharton:disklrucache:2.0.2'
- 初始化DiskLruCache
DiskLruCache 初始化是调用它的 open()静态方法,如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
directory 是缓存图片数据的文件夹,一般建议缓存到 SD 卡上,SD 卡有足够的空间。appVersion 是应用/引擎版本号,根据应用的业务需求来维护,如果 appVersion 发生变化,会自动删除前一个版本的数据。valueCount 是指 Key 与 Value 的对应关系,一般情况下是 1 对 1 关系,这里就写 1。maxSize 是缓存图片的最大缓存数据大小,根据业务自身需求设置。
初始化 DiskLruCache 的代码如下:
private int appVersion = 1;
private DiskLruCache mDiskLruCache;
public void init(final long cacheSize, final File cacheFile) {
new Thread() {
@Override
public void run() {
synchronized (mDiskCacheLock) {
if (!cacheFile.exists()) {
cacheFile.mkdir();
}
try {
mDiskLruCache = DiskLruCache.open(cacheFile, appVersion, 1, cacheSize);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
}
因为 open 方法中有 IO 操作,所以建议在异步线程中初始化,但如果在初始化前就要操作写或者读操作会导致失败,所以在初始化过程中加锁,避免同步问题。
- 写入 DiskLruCache
DiskLruCache.Editor 类实现写入的操作。通过调用 DiskLruCache 的 edit()方法来获取实例,写入操作流程如下:
- 获取 Editor 实例
Editor 实例需要传入一个 Key 来获取参数,Key 必须与图片 URL 有唯一对应关系,但由于 URL 中的字符可能会带文件名不支持的字符类型,所以取 URL 的 MD5 值作为文件名,实现 Key 与图片的对应关系,通过 URL 获取 MD5 值的代码如下:
private String hashKeyForDisk(String key) {
String cacheKey = null;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = byteToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return cacheKey;
}
private String byteToHexString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte aByte : bytes) {
String hex = Integer.toHexString(0xFF & aByte);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
- 写入需要保存的数据
有了 DiskLruCache.Editor 的实例之后,可以调用它的 newOutputStream()方法来创建一个输出流,把需要保存的数据写入 OutputStream。在写入操作执行完之后,再调用 commit()方法提交才能使写入生效,调用 abort()方法的话则表示放弃此次写入。
public void saveToDisk(String imageUrl, InputStream inputStream) {
synchronized (mDiskCacheLock) {
String key = hashKeyForDisk(imageUrl);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (inputStream != null && editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(inputStream, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public boolean downloadUrlToStream(InputStream inputStream, OutputStream outputStream) {
// 下载
return false;
}
- 读取图片缓存
public Bitmap getBitmapFromDiskCache(String url ,BitmapConfig bitmapConfig){
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null){
String key = hashKeyForDisk(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null){
return null;
}
InputStream inputStream = snapshot.getInputStream(0);
if ( inputStream != null){
// 根据 Option 读取 Bitmap 返回
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
但需要注意的是,读取并解码成Bitmap数据和保存图片数据都是有一定耗时的IO操作。所以一定要记住不能放在主线程中调用这些方法。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/143371.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...