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 ...
随机推荐
- Linux磁盘操作命令
查看本地磁盘使用情况:df或者df -l单位为k 容量便于查看,以1024单位换算为M或者G等:df -h或者df -lh 以1000为单位换算:df -H 显示文件系统类型:df -T 显示指定文件 ...
- SQL Server 2016里TempDb的提升
几个星期前,SQL Server 2016的最新CTP版本已经发布了:CTP 2.4(目前已经是CTP 3.0).这个预览版相比以前的CTP包含了很多不同的提升.在这篇文章里我会谈下对于SQL Ser ...
- 【Beta阶段】团队源代码管理
0. 快速上手与理解 如果你的团队来了一个新队员,有一台全新的机器,你们是否有一个文档,只要设置了相应的权限,她就可以根据文档,从头开始搭建环境,并成功地把最新.最稳定版本的软件编译出来,并运行必要的 ...
- 重构第11天 使用策略代替Switch(Switch to Strategy)
理解:策略就是平常设计模式中所说的策略模式.因为当你有一个庞大的switch方法的时候,每一次新加一个条件,都要去修改这个方法,这样耦合性太高,不易维护也不易扩展.这样我们就可以使用策略的设计模式,使 ...
- sql 去重
;WITH CETAS (SELECT *, ROW_NUMBER() OVER (PARTITION BY SearchTask_PKID ORDER BY SearchTask_PKID) Row ...
- JPA(7) spring-data-jpa
对于不是使用spring管理的项目,我们就自己创建对象使用:大概的思路就是①创建dao接口②实现该接口,并且编写逻辑: Dao: public interface StudentDao { publi ...
- (旧)子数涵数·Flash——影片剪辑的事件操作
一.综述 1.概念:影片剪辑的事件操作,就是onClipEvent命令,就如同在按钮上使用的on命令. 2.方法:onClipEnvent(参数){命令} 3.参数:onClipEnvent有许多的参 ...
- PHP学习笔记:删除与销毁session
删除某个session值可以使用PHP的unset函数,删除后就会从全局变量$_SESSION中去除,无法访问. session_start(); $_SESSION['name'] = 'jobs' ...
- HTML5离线缓存Manifest
web app不比PC,有性能和流量方面的考虑,离线应用越来越重要,虽然浏览器有缓存机制,但是时常不靠谱,更何况普通情况下html文件是没法缓存的,断网之后一切over. 什么是manifest? 简 ...
- H5前端面试题及答案(1)
前几天去面试了一家公司,整下改公司的面试题. 1.新的 HTML5 文档类型和字符集是? HTML5 文档类型很简单: <!doctype html> HTML5 使用 UTF-8 编码示 ...