fresco是android一款比较好的图片处理框架,特别是在5.0以下,效果很佳。
在5.0以下系统,Fresco将图片放到一个特别的内存区域ashmem中。这块内存我们通过android studio查看时不会显示,回收机制与java回收机制差不多。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得APP更加流畅,减少因图片内存占用而引发的OOM。
5.0以上系统,我们使用了Fresco,但是看到的效果是内存持续增长到200M,甚至300M,线上的OutOfMemoryError成百上千的。当时一度怀疑Fresco这个框架是不是支持的不够好。静下心来研究了一下fresco内部机制,最终解决了Fresco 5.0以上内存优化的问题。
也是网上广为流传的方式,通过BitmapFactory.Options的属性:
options.inPurgeable
下面通过实例测试
//图片1
ImageView iv = new ImageView(this);
iv.setLayoutParams(new ViewGroup.LayoutParams(500,300));
options = new BitmapFactory.Options();
options.inPurgeable = true;
iv1.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options));
//图片2
ImageView iv1 = new ImageView(this);
iv1.setLayoutParams(new ViewGroup.LayoutParams(500,300));
iv1.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options));
//图片3
ImageView iv2 = new ImageView(this);
iv2.setLayoutParams(new ViewGroup.LayoutParams(500,300));
iv2.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options));
3张图片,单独显示一张在21M左右,两两组合 内存在28M左右,3张图片一起显示在32M左右。使用了inPurgeable没啥效果。查看文档在api21已经废弃
@deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is ignored.
再则,如果选择把5.0以上bitmap都保存在ashmem中,就得抛弃fresco,自写一套图片框架,这个工程比较大,也没能力写的比fresco好。放弃了这条路。
memoryTrimmableRegistry它持有一个MemoryTrimmable的集合。Fresco的缓存就在它们中间。当你接受到一个系统的内存事件时,你可以调用MemoryTrimmable的对应方法来释放资源。
当我们继承memoryTrimmableRegistry设置一个监听后,发现只是在注册时候收到回调,缓存时候没有收到对应的回调,查看源码,在CountingMemoryCache中有下面一段代码:
public void trim(MemoryTrimType trimType) {
.......
final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
.......
}
最终调用了BitmapMemoryCacheTrimStrategy.getTrimRatio(trimType),这样就不会回调到我们自定义的MyMemoryTrimmableRegistry了,这样memoryTrimmableRegistry回调中手动释放内存行不通。
ImageCacheStatsTracker统计缓存的命中率,你可以实现ImageCacheStatsTracker, 在这个类中,每个缓存时间都有回调通知,基于这些事件,可以实现缓存的计数和统计。
当添加缓存是put收到回调,命中缓存是Hit回调,没有命中缓存miss回调
@Override
public void onMemoryCachePut() {
}
@Override
public void onMemoryCacheHit() {
}
@Override
public void onMemoryCacheMiss() {
}
不过这些回调不是我们注册的那个ImageCacheStatsTracker,而是fresco内部类中的回调,并且这些回调也没有参数,我们不能清楚的知道当前内存多少,是不是该清除内存了,所以这个方式也是行不通的。
eviction: 逐出,收回
既然被你标记为回收,这部分内存就是用处不大的,可以及时清除掉这部分内存,就不会出现内存持续增长了。
/** Checks the cache constraints to determine whether the new value can be cached or not. */
private synchronized boolean canCacheNewValue(V value) {
int newValueSize = mValueDescriptor.getSizeInBytes(value);
return (newValueSize <= mMemoryCacheParams.maxCacheEntrySize) &&
(getInUseCount() <= mMemoryCacheParams.maxCacheEntries - 1) &&
(getInUseSizeInBytes() <= mMemoryCacheParams.maxCacheSize - newValueSize);
}
/** Gets the number of the cached items that are used by at least one client. */
public synchronized int getInUseCount() {
return mCachedEntries.getCount() - mExclusiveEntries.getCount();
}
/** Gets the total size in bytes of the cached items that are used by at least one client. */
public synchronized int getInUseSizeInBytes() {
return mCachedEntries.getSizeInBytes() - mExclusiveEntries.getSizeInBytes();
}
从源码中看出,在缓存之前需要检查能否缓存canCacheNewValue,比较当前使用的缓存cacheSize与maxCacheSize;当前缓存数量cacheEntrys与maxCacheEntrys大小。只要不超过maxCacheSize且不超过maxCacheEntrys,就可以添加到缓存队列中。
上面都没啥问题,注意到cacheSize与cacheEntrys的计算方式
mCachedEntries.getCount() - mExclusiveEntries.getCount()
mCachedEntries.getSizeInBytes() - mExclusiveEntries.getSizeInBytes()
可以看到是先减去将要被回收的那部分bitmap的数量和size,问题就在这里,mExclusiveEntries是将要被回收的缓存,但是还没有被回收,如果这部分内存足够大时,又没有被Fresco计算在内,可能引起OOM。经过测试,这部分内存,经常保持在40-60M之间,这么大块内存没有被及时回收,不发生OOM才怪呢。所以我们只需要减小ExclusiveEntries的大小,就能及时的回收fresco内存了
Fresco默认DefaultBitmapMemoryCacheParamsSupplier中设置了EVICTION池为Integer.MAX_VALUE,我们只需仿照这个DefaultBitmapMemoryCacheParamsSupplier,把EVICTION池该成足够小,就可以了了
private static final int MAX_EVICTION_QUEUE_SIZE = Integer.MAX_VALUE;
private static final int MAX_EVICTION_QUEUE_ENTRIES = Integer.MAX_VALUE;
private static final int MAX_CACHE_ENTRY_SIZE = Integer.MAX_VALUE;
自定义Supplier 完整代码
这里设置EVICTION为5M,eviction entry数量为5条记录,单一entry大小为1M,设置为5是为了减少GC的次数,5M内存积累也需要一段时间,不会影响app使用体验。当然你可以设置为1M或者更小。
public class MyBitmapMemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> {
private static final int MAX_CACHE_ENTRIES = 56;
private static final int MAX_CACHE_ASHM_ENTRIES = 128;
private static final int MAX_CACHE_EVICTION_SIZE = 5;
private static final int MAX_CACHE_EVICTION_ENTRIES = 5;
private final ActivityManager mActivityManager;
public MyBitmapMemoryCacheParamsSupplier(ActivityManager activityManager) {
mActivityManager = activityManager;
}
@Override
public MemoryCacheParams get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new MemoryCacheParams(getMaxCacheSize(), MAX_CACHE_ENTRIES, MAX_CACHE_EVICTION_SIZE, MAX_CACHE_EVICTION_ENTRIES, 1);
} else {
return new MemoryCacheParams(
getMaxCacheSize(),
MAX_CACHE_ASHM_ENTRIES,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
Integer.MAX_VALUE);
}
}
private int getMaxCacheSize() {
final int maxMemory =
Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);
if (maxMemory < 32 * ByteConstants.MB) {
return 4 * ByteConstants.MB;
} else if (maxMemory < 64 * ByteConstants.MB) {
return 6 * ByteConstants.MB;
} else {
return maxMemory / 5;
}
}
}
在初始化Fresco的时候把MyBitmapMemoryCacheParamsSupplier设置到config中。
然后重写下application的 onTrimMemory,onLowMemory
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
FrescoUtils.TrimMemory(level);
}
@Override
public void onLowMemory() {
super.onLowMemory();
FrescoUtils.clearAllMemoryCaches();
}
当然防止单张图片过大导致的OOM,需要使用Resize属性
public void setImageURIListener(Uri uri, ControllerListener listener, boolean isSmall) {
if(uri == null){
return;
}
PipelineDraweeControllerBuilder builder = Fresco.getDraweeControllerBuilderSupplier().get()
.setCallerContext(null)
.setUri(uri)
.setOldController(getController());
builder.setControllerListener(listener);
ResizeOptions resizeOptions;
if (isSmall) {
resizeOptions = new ResizeOptions(Util.dip2px(mContext, 144), Util.dip2px(mContext, 144));
} else {
resizeOptions = new ResizeOptions(AppConfig.sScreenWidth, AppConfig.sScreenWidth / 2);
}
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
.setResizeOptions(resizeOptions).build();
builder.setImageRequest(request);
setController(builder.build());
}
Resize对jpg格式有效,对于png等其他格式的图片也支持这个属性,需要设置Downsample
ImagePipelineConfig.newBuilder(context)
.setDownsampleEnabled(true)
大功告成,再也不用担心5.0以上使用Fresco出现OOM了
效果:停留在一个页面,一段时间后出现明显的GC现象