android图片缓存(包含ReusableBitmapDrawable和BitmapPool)
现在做的项目中,有用到一个开源的2D地图框架osmdroid,但是在项目中,使用还是有一些问题,例如,多个地图实例,会有独自的图片缓存,Activity onPause时,并不会释放图片缓存,如果多级界面都有地图的话,可能会造成很多手机内存溢出(按照每个瓦片256*256,屏幕1280*720来算,显示一个屏幕的地图,至少要在内存保存15张图片,占用内存256*256*4*15=3.75M),所以还是得针对项目做一定的修改调整。今天将项目中使用到的图片缓存做一个整理。
首先说下整体的图片加载流程:
1、内存中加载。通过LruCache保存,LruCache中移除的图片,如果没有引用使用,会缓存到BitmapPool以供重用,避免频繁申请释放内存,这样滑动时加载图片会更平滑;
2、从文件加载。内存中没有,会从瓦片缓存目录中加载,加载成功保存到内存并通知地图刷新显示;
3、从网络加载。如果本地文件也没有,会从网络下载,然后保存到瓦片缓存目录,以及内存中,然后再通知地图刷新显示。
接下来说一下Bitmap相关的几个重要的参数:
BitmapFactory.Options.inBitmap:
If set, decode methods that take the Options object will attempt to reuse this bitmap when loading content. If the decode operation cannot use this bitmap, the decode method will return null and will throw an IllegalArgumentException. The current implementation necessitates that the reused bitmap be of the same size as the source content and in jpeg or png format (whether as a resource or as a stream). The configuration of the reused bitmap will override the setting of inPreferredConfig, if set.
You should still always use the returned Bitmap of the decode method and not assume that reusing the bitmap worked, due to the constraints outlined above and failure situations that can occur. Checking whether the return value matches the value of the inBitmap set in the Options structure is a way to see if the bitmap was reused, but in all cases you should use the returned Bitmap to make sure that you are using the bitmap that was used as the decode destination.
从Android 3.0(API level 11)开始,引入了BitmapFactory.Options.inBitmap字段,如果这个属性被设置了,拥有这个Options对象的方法在解析图片的时候会尝试复用一张已存在的图片。这意味着图片缓存被复用了,这意味着更流畅的用户体验以及更好的内存分配和回收。然而,要使用inBitmap有这一定的限制:
1、在Android 4.4(API level 19)之前,只有相同大小的Btiamp才会被复用;
2、必须配合inMutable=true,inSampleSize=1一起使用;
3、一定要使用解码方法BitmapFactory.decodeStream返回的Bitmap,否则重用可能会失败。
具体使用:
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
bitmapOptions.inBitmap = obtainSizedBitmapFromPool();
bitmapOptions.inSampleSize = 1;
bitmapOptions.inMutable = true;
}
Bitmap bitmap = BitmapFactory.decodeStream(aFileInputStream, null, bitmapOptions);
Drawable drawable = new ReusableBitmapDrawable(bitmap);
如果是在Android 2.3.3 (API level 10),以及更低的版本中,推荐使用Bitmap.recycle方法加快Bitmap内存的回收。
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
//BitmapOptions.inBitmap无效,不缓存图片
if (b != null && !b.isRecycled()){
b.recycle();
}
return;
}
然后介绍下使用到的几个比较重要的类:
ReusableBitmapDrawable:主要是记录Bitmap的引用数,在从内存中移除时判断是否可以recycle回收释放内存或添加到BitmapPool,也可避免"trying to use a recycled bitmap"得异常。
代码:
public class ReusableBitmapDrawable extends BitmapDrawable {
private boolean mBitmapRecycled = false;
private int mUsageRefCount = 0;
public ReusableBitmapDrawable(Bitmap pBitmap) {
super(pBitmap);
}
/**
* 开始使用图片,引用数+1,只要有地方引用图片就不能回收
* 之后还是需要通过isBitmapValid()确保图片有效能用
*/
public void beginUsingDrawable() {
synchronized (this) {
mUsageRefCount++;
}
}
/**
* 某个地方使用图片结束,引用数-1
*/
public void finishUsingDrawable() {
synchronized (this) {
mUsageRefCount--;
if (mUsageRefCount < 0)
throw new IllegalStateException("Unbalanced endUsingDrawable() called.");
}
}
/**
* 如果没有任何引用了,返回图片以便recycle释放内存或者加入BitmapPool以供复用
*/
public Bitmap tryRecycle() {
synchronized (this) {
if (mUsageRefCount == 0) {
mBitmapRecycled = true;
return getBitmap();
}
}
return null;
}
/**
* 判定是否已被回收或者加入缓存池
*/
public boolean isBitmapValid() {
synchronized (this) {
return !mBitmapRecycled;
}
}
}
使用示例:
Drawable currentMapTile = ...... boolean isReusable = currentMapTile != null
&& currentMapTile instanceof ReusableBitmapDrawable;
final ReusableBitmapDrawable reusableBitmapDrawable =
isReusable ? (ReusableBitmapDrawable) currentMapTile : null;
if (isReusable) {
reusableBitmapDrawable.beginUsingDrawable();
}
try {
if (isReusable && !((ReusableBitmapDrawable) currentMapTile).isBitmapValid()) {
currentMapTile = getLoadingTile(); //已经回收或者缓存,显示默认的加载图片
isReusable = false;
}
onTileReadyToDraw(pCanvas, currentMapTile, mTileRect); //绘制瓦片
} finally {
if (isReusable)
reusableBitmapDrawable.finishUsingDrawable();
}
当从内存中移除时:
public void returnDrawableToPool(ReusableBitmapDrawable drawable) {
Bitmap b = drawable.tryRecycle();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
//BitmapOptions.inBitmap无效,不缓存图片
if (b != null && !b.isRecycled()){
b.recycle();
}
return;
}
if (b != null && b.isMutable()){
synchronized (mPool) {
mPool.addLast(b);
}
}
}
LruCache:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
/**
* Sets the size of the cache.
*
* @param maxSize The new maximum size.
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
*/
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
*
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false
* if the removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put}. Otherwise it was caused by
* an eviction or a {@link #remove}.
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
*
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* <p>If a value for {@code key} exists in the cache when this method
* returns, the created value will be released with {@link #entryRemoved}
* and discarded. This can occur when multiple threads request the same key
* at the same time (causing multiple values to be created), or when one
* thread calls {@link #put} while another is creating a value for the same
* key.
*/
protected V create(K key) {
return null;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
* <p>An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return maxSize;
}
/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return evictionCount;
}
/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed.
*/
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(map);
}
@Override public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}
}
BitmapPool: 缓存Bitmap以便重用,避免频繁申请回收内存,使图片加载更加平滑。
public class BitmapPool {
final LinkedList<Bitmap> mPool = new LinkedList<Bitmap>();
private static BitmapPool sInstance;
public static BitmapPool getInstance() {
if (sInstance == null)
sInstance = new BitmapPool();
return sInstance;
}
public void returnDrawableToPool(ReusableBitmapDrawable drawable) {
Bitmap b = drawable.tryRecycle();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
//BitmapOptions.inBitmap无效,不缓存图片
if (b != null && !b.isRecycled()){
b.recycle();
}
return;
}
if (b != null && b.isMutable()){
synchronized (mPool) {
mPool.addLast(b);
}
}
}
public void applyReusableOptions(final BitmapFactory.Options aBitmapOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
aBitmapOptions.inBitmap = obtainBitmapFromPool();
aBitmapOptions.inSampleSize = 1;
aBitmapOptions.inMutable = true;
}
}
public Bitmap obtainBitmapFromPool() {
synchronized (mPool) {
if (mPool.isEmpty()) {
return null;
} else {
final Bitmap bitmap = mPool.removeFirst();
if (bitmap.isRecycled()) {
return obtainBitmapFromPool(); // recurse
} else {
return bitmap;
}
}
}
}
public Bitmap obtainSizedBitmapFromPool(final int aWidth, final int aHeight) {
synchronized (mPool) {
if (mPool.isEmpty()) {
return null;
} else {
for (final Bitmap bitmap : mPool) {
if (bitmap.isRecycled()) {
mPool.remove(bitmap);
return obtainSizedBitmapFromPool(aWidth, aHeight); // recurse to prevent ConcurrentModificationException
} else if (bitmap.getWidth() == aWidth && bitmap.getHeight() == aHeight) {
mPool.remove(bitmap);
return bitmap;
}
}
}
}
return null;
}
public void clearBitmapPool() {
synchronized (sInstance.mPool) {
while (!sInstance.mPool.isEmpty()) {
Bitmap bitmap = sInstance.mPool.remove();
bitmap.recycle();
}
}
}
}
如果文章中又写的有问题的地方,欢迎反馈。
android图片缓存(包含ReusableBitmapDrawable和BitmapPool)的更多相关文章
- Android图片缓存之Glide进阶
前言: 前面学习了Glide的简单使用(Android图片缓存之初识Glide),今天来学习一下Glide稍微复杂一点的使用. 图片缓存相关博客地址: Android图片缓存之Bitmap详解 And ...
- Android图片缓存之初识Glide
前言: 前面总结学习了图片的使用以及Lru算法,今天来学习一下比较优秀的图片缓存开源框架.技术本身就要不断的更迭,从最初的自己使用SoftReference实现自己的图片缓存,到后来做电商项目自己的实 ...
- 安卓高级 Android图片缓存之初识Glide
前言: 前面总结学习了图片的使用以及Lru算法,今天来学习一下比较优秀的图片缓存开源框架.技术本身就要不断的更迭,从最初的自己使用SoftReference实现自己的图片缓存,到后来做电商项目自己的实 ...
- Android图片缓存之Lru算法
前言: 上篇我们总结了Bitmap的处理,同时对比了各种处理的效率以及对内存占用大小.我们得知一个应用如果使用大量图片就会导致OOM(out of memory),那该如何处理才能近可能的降低oom发 ...
- Android图片缓存之Bitmap详解
前言: 最近准备研究一下图片缓存框架,基于这个想法觉得还是先了解有关图片缓存的基础知识,今天重点学习一下Bitmap.BitmapFactory这两个类. 图片缓存相关博客地址: Android图片缓 ...
- Android图片缓存框架Glide
Android图片缓存框架Glide Glide是Google提供的一个组件.它具有获取.解码和展示视频剧照.图片.动画等功能.它提供了灵活的API,帮助开发者将Glide应用在几乎任何网络协议栈中. ...
- Android图片缓存之Glide进阶(四)
前言: 前面学习了Glide的简单使用(http://www.cnblogs.com/whoislcj/p/5558168.html),今天来学习一下Glide稍微复杂一点的使用. GlideModu ...
- Android图片缓存之初识Glide(三)
前言: 前面总结学习了图片的使用以及Lru算法,今天来学习一下比较优秀的图片缓存开源框架.技术本身就要不断的更迭,从最初的自己使用SoftReference实现自己的图片缓存,到后来做电商项目自己的实 ...
- Android图片缓存之Bitmap详解(一)
前言: 最近准备研究一下图片缓存框架,基于这个想法觉得还是先了解有关图片缓存的基础知识,今天重点学习一下Bitmap.BitmapFactory这两个类. Bitmap: Bitmap是Android ...
随机推荐
- iOS-UIButton-设置button标题和图片位置
一.效果图 1.Button被点击之前 2.Button被点击之后 二.代码 - (void)createBtn3 { UIImage * buttonImage = [UIImage imageNa ...
- iOS-沙盒路径总结、文件管理NSFileManager总结
// // ViewController.m // 沙盒操作 // // Created by mncong on 15/11/26. // Copyright © 2015年 mancong ...
- Microsoft OneScript 团队发布的最新一版在 SQL Server Management Studio 中运行的脚本,可以帮助我们获取更详细的版本信息。
该脚本有以下几点: 1. SQL Server 的主要版本.服务级别和版本类别 2. 已安装SP包.累计更新CU,历史更新的QFE\ GDR 3. 推荐当前SP包可以安装最新的CU,并给到相关资源地地 ...
- Websocket协议的学习、调研和实现
本文章同时发在 cpper.info. 1. websocket是什么 Websocket是html5提出的一个协议规范,参考rfc6455. websocket约定了一个通信的规范,通过一个握手的机 ...
- 一个关于explain出来为all的说明及优化
explain sql语句一个语句,得到如下结果,为什么已经创建了t_bill_invests.bid_id的索引,但却没有显示using index,而是显示all扫描方式呢,原来这还与select ...
- SpringMVC——类型转换和格式化、数据校验、客户端显示错误消息
在介绍类型转换和格式化之前,我首先来介绍 <mvc:annotation-driven />. 需要导入的 schema: xmlns:mvc="http://www.sprin ...
- czxt
实验三 进程调度模拟程序 1. 目的和要求 1.1. 实验目的 用高级语言完成一个进程调度程序,以加深对进程的概念及进程调度算法的理解. 1.2. 实验要 ...
- 以对象的方式来访问xml数据表(一)
所有实例代码都是以C#演示—— 在将如何以对象的方式来访问xml数据表之前,我们先来谈谈如何用xml文件作为数据库吧! 平时在开发一些小的应用的时候,需要一个数据库,这个时候虽然可以用SQL serv ...
- Web基础开发最核心要解决的问题
Web基础开发要解决的问题,往往也就是那些框架出现的目的 - 要解决问题. 1. 便捷的Db操作: 2. 高效的表单处理: 3. 灵活的Url路由: 4. 合理的代码组织结构: 5. 架构延伸 缓存. ...
- Docker源码编译
官方建议docker源码编译在docker容器内进行,因为官方提供的容器内已经继承了编译需要的环境,如果非要自己搭建编译环境也不是不可以,就是稍微有些繁琐.以下以1.8.2版本为例. 1.pull d ...