我觉得内存管理是三方图库最重要的点, 而且该知识点能够应用到项目里, 所以着重看了一下fresco是如何回收内存的。
fresco内存释放分为2种方式:
1、按照LruCach的方式释放引用计数为0对象, fresco内部逻辑实现;
2、应用退到后台、手机低内存等场景下主动释放fresco的内存, 包括引用计数不为0的对象, 需要传事件给fresco。 参考: https://github.com/facebook/fresco/issues/1457
ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearMemoryCaches(); //内存,包括已解码和未解码
imagePipeline.clearDiskCaches(); //删除文件
// combines above two lines
imagePipeline.clearCaches();
上图是fresco官方提供的架构图, 按照三级缓存。 即已解码的图片,未解码的图片和文件缓存。Android5.0以前bitmap是缓存在ashmem里, Android5.0及以上保存在java堆里。 为了释放bitmap, fresco定义了引用计数类SharedPrefrence。
public class SharedReference {
// Keeps references to all live objects so finalization of those Objects always happens after
// SharedReference first disposes of it. Note, this does not prevent CloseableReference's from
// being finalized when the reference is no longer reachable.
@GuardedBy("itself")
private static final Map
在Android5.0后Fresco又封装了CloseableReference类实现了Cloneable、Closeable接口,它在调用.clone()方法时同时会调用addReference()来增加一个引用计数,在调用.close()方法时同时会调用deleteReference()来删除一个引用计数。 fresco对CloseableReference的注释:This class allows reference-counting semantics in a Java-friendlier way. A single object can have any number of CloseableReferences pointing to it. When all of these have been closed, the object either has its {@link Closeable#close} method called, if it implements {@link Closeable}, or its designated {@link ResourceReleaser#release}。 翻译过来就是CloseReference类提供了一种友好的引用计数方式,多个CloseReference对象可以指向同一个对象, 当所有的CloseReference都关闭时则调用Closeable接口的close方法(如果实现了Closeable接口),或者ResourceReleaser接口的release方法。
1、在赋值CloseableReference给新对象的时候,调用.clone()进行赋值, 引用计数加1。
2、在超出作用域范围的时候,必须调用.close(),通常会在finally代码块中调用, 引用计数减1。
/** * Decrement the reference count for the shared reference. If the reference count drops to zero, * then dispose of the referenced value */ public void deleteReference() { if (decreaseRefCount() == 0) { T deleted; synchronized (this) { deleted = mValue; mValue = null; } mResourceReleaser.release(deleted); removeLiveReference(deleted); } }当引用计数等于0时, 调用release方法释放内存。 Fresco代码里随处可见CloseReference的身影, 这个类是可以抽出来用作我们的内存管理类。
前面提到了已解码的mBitmapMemoryCache和未解码的mEncodedMemoryCache图片缓存, 实际上都是用CountingMemoryCache类实例化(InstrumentedMemoryCache的基类)的对象。 其内部成员变量有2个重要的对象,即mCacheEntries和mExclusiveEntries。 正在使用的数据放在mCachedEntries里, 等待复用(待回收)的数据放在mExclusiveEntries。 而mCachedEntries和mExclusiveEntries都是CountingLruMap的对象,类似于LruCache, 即从最远使用开始移除。
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.cache;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import android.os.SystemClock;
import android.util.Log;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Supplier;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.memory.MemoryTrimType;
import com.facebook.common.memory.MemoryTrimmable;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.references.ResourceReleaser;
import com.android.internal.util.Predicate;
/**
* Layer of memory cache stack responsible for managing eviction of the the cached items.
*
* This layer is responsible for LRU eviction strategy and for maintaining the size boundaries
* of the cached items.
*
*
Only the exclusively owned elements, i.e. the elements not referenced by any client, can be
* evicted.
*
* @param the key type
* @param the value type
*/
@ThreadSafe
public class CountingMemoryCache implements MemoryCache, MemoryTrimmable {
/**
* Interface used to specify the trimming strategy for the cache.
*/
public interface CacheTrimStrategy {
double getTrimRatio(MemoryTrimType trimType);
}
/**
* Interface used to observe the state changes of an entry.
*/
public interface EntryStateObserver {
/**
* Called when the exclusivity status of the entry changes.
*
* The item can be reused if it is exclusively owned by the cache.
*/
void onExclusivityChanged(K key, boolean isExclusive);
}
/**
* The internal representation of a key-value pair stored by the cache.
*/
@VisibleForTesting
static class Entry {
public final K key;
public final CloseableReference valueRef;
// The number of clients that reference the value.
public int clientCount;
// Whether or not this entry is tracked by this cache. Orphans are not tracked by the cache and
// as soon as the last client of an orphaned entry closes their reference, the entry's copy is
// closed too.
public boolean isOrphan;
@Nullable public final EntryStateObserver observer;
private Entry(K key, CloseableReference valueRef, @Nullable EntryStateObserver observer) {
this.key = Preconditions.checkNotNull(key);
this.valueRef = Preconditions.checkNotNull(CloseableReference.cloneOrNull(valueRef));
this.clientCount = 0;
this.isOrphan = false;
this.observer = observer;
}
/** Creates a new entry with the usage count of 0. */
@VisibleForTesting
static Entry of(
final K key,
final CloseableReference valueRef,
final @Nullable EntryStateObserver observer) {
return new Entry<>(key, valueRef, observer);
}
}
// How often the cache checks for a new cache configuration.
@VisibleForTesting
static final long PARAMS_INTERCHECK_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5);
// Contains the items that are not being used by any client and are hence viable for eviction.
@GuardedBy("this")
@VisibleForTesting
final CountingLruMap> mExclusiveEntries;
// Contains all the cached items including the exclusively owned ones.
@GuardedBy("this")
@VisibleForTesting
final CountingLruMap> mCachedEntries;
private final ValueDescriptor mValueDescriptor;
private final CacheTrimStrategy mCacheTrimStrategy;
// Cache size constraints.
private final Supplier mMemoryCacheParamsSupplier;
@GuardedBy("this")
protected MemoryCacheParams mMemoryCacheParams;
@GuardedBy("this")
private long mLastCacheParamsCheck;
public CountingMemoryCache(
ValueDescriptor valueDescriptor,
CacheTrimStrategy cacheTrimStrategy,
Supplier memoryCacheParamsSupplier) {
mValueDescriptor = valueDescriptor;
mExclusiveEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor));
mCachedEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor));
mCacheTrimStrategy = cacheTrimStrategy;
mMemoryCacheParamsSupplier = memoryCacheParamsSupplier;
mMemoryCacheParams = mMemoryCacheParamsSupplier.get();
mLastCacheParamsCheck = SystemClock.uptimeMillis();
}
private ValueDescriptor> wrapValueDescriptor(
final ValueDescriptor evictableValueDescriptor) {
return new ValueDescriptor>() {
@Override
public int getSizeInBytes(Entry entry) {
return evictableValueDescriptor.getSizeInBytes(entry.valueRef.get());
}
};
}
/**
* Caches the given key-value pair.
*
* Important: the client should use the returned reference instead of the original one.
* It is the caller's responsibility to close the returned reference once not needed anymore.
*
* @return the new reference to be used, null if the value cannot be cached
*/
public CloseableReference cache(final K key, final CloseableReference valueRef) {
return cache(key, valueRef, null);
}
/**
* Caches the given key-value pair.
*
* Important: the client should use the returned reference instead of the original one.
* It is the caller's responsibility to close the returned reference once not needed anymore.
*
* @return the new reference to be used, null if the value cannot be cached
*/
public CloseableReference cache(
final K key,
final CloseableReference valueRef,
final EntryStateObserver observer) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(valueRef);
maybeUpdateCacheParams();
Entry oldExclusive;
CloseableReference oldRefToClose = null;
CloseableReference clientRef = null;
synchronized (this) {
// remove the old item (if any) as it is stale now
oldExclusive = mExclusiveEntries.remove(key);
Entry oldEntry = mCachedEntries.remove(key);
if (oldEntry != null) {
makeOrphan(oldEntry);
oldRefToClose = referenceToClose(oldEntry);
}
if (canCacheNewValue(valueRef.get())) {
Entry newEntry = Entry.of(key, valueRef, observer);
mCachedEntries.put(key, newEntry);
clientRef = newClientReference(newEntry);
}
}
CloseableReference.closeSafely(oldRefToClose);
maybeNotifyExclusiveEntryRemoval(oldExclusive);
maybeEvictEntries();
return clientRef;
}
/** 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 item with the given key, or null if there is no such item.
*
* It is the caller's responsibility to close the returned reference once not needed anymore.
*/
@Nullable
public CloseableReference get(final K key) {
Preconditions.checkNotNull(key);
Entry oldExclusive;
CloseableReference clientRef = null;
synchronized (this) {
oldExclusive = mExclusiveEntries.remove(key);
Entry entry = mCachedEntries.get(key);
if (entry != null) {
clientRef = newClientReference(entry);
}
}
maybeNotifyExclusiveEntryRemoval(oldExclusive);
maybeUpdateCacheParams();
maybeEvictEntries();
return clientRef;
}
/** Creates a new reference for the client. */
private synchronized CloseableReference newClientReference(final Entry entry) {
increaseClientCount(entry);
return CloseableReference.of(
entry.valueRef.get(),
new ResourceReleaser() {
@Override
public void release(V unused) {
releaseClientReference(entry);
}
});
}
/** Called when the client closes its reference. */
private void releaseClientReference(final Entry entry) {
Preconditions.checkNotNull(entry);
boolean isExclusiveAdded;
CloseableReference oldRefToClose;
synchronized (this) {
decreaseClientCount(entry);
isExclusiveAdded = maybeAddToExclusives(entry);
oldRefToClose = referenceToClose(entry);
}
CloseableReference.closeSafely(oldRefToClose);
maybeNotifyExclusiveEntryInsertion(isExclusiveAdded ? entry : null);
maybeUpdateCacheParams();
maybeEvictEntries();
}
/** Adds the entry to the exclusively owned queue if it is viable for eviction. */
private synchronized boolean maybeAddToExclusives(Entry entry) {
if (!entry.isOrphan && entry.clientCount == 0) {
mExclusiveEntries.put(entry.key, entry);
return true;
}
return false;
}
/**
* Gets the value with the given key to be reused, or null if there is no such value.
*
* The item can be reused only if it is exclusively owned by the cache.
*/
@Nullable
public CloseableReference reuse(K key) {
Preconditions.checkNotNull(key);
CloseableReference clientRef = null;
boolean removed = false;
Entry oldExclusive = null;
synchronized (this) {
oldExclusive = mExclusiveEntries.remove(key);
if (oldExclusive != null) {
Entry entry = mCachedEntries.remove(key);
Preconditions.checkNotNull(entry);
Preconditions.checkState(entry.clientCount == 0);
// optimization: instead of cloning and then closing the original reference,
// we just do a move
clientRef = entry.valueRef;
removed = true;
}
}
if (removed) {
maybeNotifyExclusiveEntryRemoval(oldExclusive);
}
return clientRef;
}
/**
* Removes all the items from the cache whose key matches the specified predicate.
*
* @param predicate returns true if an item with the given key should be removed
* @return number of the items removed from the cache
*/
public int removeAll(Predicate predicate) {
ArrayList> oldExclusives;
ArrayList> oldEntries;
synchronized (this) {
oldExclusives = mExclusiveEntries.removeAll(predicate);
oldEntries = mCachedEntries.removeAll(predicate);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldExclusives);
maybeUpdateCacheParams();
maybeEvictEntries();
return oldEntries.size();
}
/** Removes all the items from the cache. */
public void clear() {
ArrayList> oldExclusives;
ArrayList> oldEntries;
synchronized (this) {
oldExclusives = mExclusiveEntries.clear();
oldEntries = mCachedEntries.clear();
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldExclusives);
maybeUpdateCacheParams();
}
/**
* Check if any items from the cache whose key matches the specified predicate.
*
* @param predicate returns true if an item with the given key matches
* @return true is any items matches from the cache
*/
@Override
public synchronized boolean contains(Predicate predicate) {
return !mCachedEntries.getMatchingEntries(predicate).isEmpty();
}
/** Trims the cache according to the specified trimming strategy and the given trim type. */
@Override
public void trim(MemoryTrimType trimType) {
ArrayList> oldEntries;
final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
synchronized (this) {
int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));
int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldEntries);
maybeUpdateCacheParams();
maybeEvictEntries();
}
/**
* Updates the cache params (constraints) if enough time has passed since the last update.
*/
private synchronized void maybeUpdateCacheParams() {
if (mLastCacheParamsCheck + PARAMS_INTERCHECK_INTERVAL_MS > SystemClock.uptimeMillis()) {
return;
}
mLastCacheParamsCheck = SystemClock.uptimeMillis();
mMemoryCacheParams = mMemoryCacheParamsSupplier.get();
}
/**
* Removes the exclusively owned items until the cache constraints are met.
*
* This method invokes the external {@link CloseableReference#close} method,
* so it must not be called while holding the this
lock.
*/
private void maybeEvictEntries() {
ArrayList> oldEntries;
synchronized (this) {
int maxCount = Math.min(
mMemoryCacheParams.maxEvictionQueueEntries,
mMemoryCacheParams.maxCacheEntries - getInUseCount());
int maxSize = Math.min(
mMemoryCacheParams.maxEvictionQueueSize,
mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
if (oldEntries != null) {
Log.d("gaorui", "oldEntries no memory");
}
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldEntries);
}
/**
* Removes the exclusively owned items until there is at most count
of them
* and they occupy no more than size
bytes.
*
* This method returns the removed items instead of actually closing them, so it is safe to
* be called while holding the this
lock.
*/
@Nullable
private synchronized ArrayList> trimExclusivelyOwnedEntries(int count, int size) {
count = Math.max(count, 0);
size = Math.max(size, 0);
// fast path without array allocation if no eviction is necessary
if (mExclusiveEntries.getCount() <= count && mExclusiveEntries.getSizeInBytes() <= size) {
return null;
}
ArrayList> oldEntries = new ArrayList<>();
while (mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) {
K key = mExclusiveEntries.getFirstKey();
mExclusiveEntries.remove(key);
oldEntries.add(mCachedEntries.remove(key));
}
return oldEntries;
}
/**
* Notifies the client that the cache no longer tracks the given items.
*
* This method invokes the external {@link CloseableReference#close} method,
* so it must not be called while holding the this
lock.
*/
private void maybeClose(@Nullable ArrayList> oldEntries) {
if (oldEntries != null) {
for (Entry oldEntry : oldEntries) {
CloseableReference.closeSafely(referenceToClose(oldEntry));
}
}
}
private void maybeNotifyExclusiveEntryRemoval(@Nullable ArrayList> entries) {
if (entries != null) {
for (Entry entry : entries) {
maybeNotifyExclusiveEntryRemoval(entry);
}
}
}
private static void maybeNotifyExclusiveEntryRemoval(@Nullable Entry entry) {
if (entry != null && entry.observer != null) {
entry.observer.onExclusivityChanged(entry.key, false);
}
}
private static void maybeNotifyExclusiveEntryInsertion(@Nullable Entry entry) {
if (entry != null && entry.observer != null) {
entry.observer.onExclusivityChanged(entry.key, true);
}
}
/** Marks the given entries as orphans. */
private synchronized void makeOrphans(@Nullable ArrayList> oldEntries) {
if (oldEntries != null) {
for (Entry oldEntry : oldEntries) {
makeOrphan(oldEntry);
}
}
}
/** Marks the entry as orphan. */
private synchronized void makeOrphan(Entry entry) {
Preconditions.checkNotNull(entry);
Preconditions.checkState(!entry.isOrphan);
entry.isOrphan = true;
}
/** Increases the entry's client count. */
private synchronized void increaseClientCount(Entry entry) {
Preconditions.checkNotNull(entry);
Preconditions.checkState(!entry.isOrphan);
entry.clientCount++;
}
/** Decreases the entry's client count. */
private synchronized void decreaseClientCount(Entry entry) {
Preconditions.checkNotNull(entry);
Preconditions.checkState(entry.clientCount > 0);
entry.clientCount--;
}
/** Returns the value reference of the entry if it should be closed, null otherwise. */
@Nullable
private synchronized CloseableReference referenceToClose(Entry entry) {
Preconditions.checkNotNull(entry);
return (entry.isOrphan && entry.clientCount == 0) ? entry.valueRef : null;
}
/** Gets the total number of all currently cached items. */
public synchronized int getCount() {
return mCachedEntries.getCount();
}
/** Gets the total size in bytes of all currently cached items. */
public synchronized int getSizeInBytes() {
return mCachedEntries.getSizeInBytes();
}
/** 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();
}
/** Gets the number of the exclusively owned items. */
public synchronized int getEvictionQueueCount() {
return mExclusiveEntries.getCount();
}
/** Gets the total size in bytes of the exclusively owned items. */
public synchronized int getEvictionQueueSizeInBytes() {
return mExclusiveEntries.getSizeInBytes();
}
}
fresco定义了MemoryTrimmable和DiskTrimmable接口类, 用于在app内存不足时回收内存。fresco已经帮实现了函数体, 只要在适当的地方调用一下就可以了。 但奇怪的是搜索fresco源码找不到在哪里调用trim方法!!! 网上有人说用fresco遇到了OOM的问题, 我觉得跟没调用trim方法有关。
从github的issue看出大部分OOM问题的解决方式是setDownSampleEnabled(true)或android:largeHeap="true"。 当手机内存低时应该释放一些资源, 包括引用计数不为0的图片, 即最终执行CountingMemoryCache类的trim方法。
我提了issue: https://github.com/facebook/fresco/issues/1455 和 https://github.com/facebook/fresco/issues/1457 , 关键在于如何拿到图片缓存对象实例。
issue里已经有我的解决方案了, 为了帮助英文不熟的同学,在这里再翻译一下
1. 声明一个静态变量, 用于缓存内存对象。
private static ArrayList sMemoryTrimmable;
public static ArrayList getMemoryTrimmable() {
return sMemoryTrimmable;
}
2. 在调用setMemoryTrimmableRegistry函数时, 在registerMemoryTrimmable里将MemoryTrimmable对象保存到静态对象里。
public static ImagePipelineConfig getImagePipelineConfig(Context context) {
if (sImagePipelineConfig == null) {
....
sMemoryTrimmable = new ArrayList<>();
configBuilder.setMemoryTrimmableRegistry(new MemoryTrimmableRegistry() {
@Override
public void registerMemoryTrimmable(MemoryTrimmable trimmable) {
sMemoryTrimmable.add(trimmable);
Log.d("brycegao", "registerMemoryTrimmable size: " + sMemoryTrimmable.size());
}
......
3. 覆盖Application的onTrimMemory方法或者Activity的onTrimMemory的方法, 遍历静态变量并调用trim方法。 注意参数不同, fresco释放的内存大小不同!
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
ArrayList array = ImagePipelineConfigFactory.getMemoryTrimmable();
if (array != null) {
for (MemoryTrimmable trimmable:array) {
//just demo, it should be proper params according to level.
trimmable.trim(MemoryTrimType.OnSystemLowMemoryWhileAppInBackground);
}
}
}
09-07 09:57:15.088 2235-2235/com.facebook.samples.comparison D/brycegao: registerMemoryTrimmable size: 4
实际测试验证, fresco默认有4个MemoryTrimmable对象。
CountingMemoryCache.java:
public void trim(MemoryTrimType trimType) {
ArrayList> oldEntries;
final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
synchronized (this) {
int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));
int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize);
makeOrphans(oldEntries);
}
....
}
BasePool.java :
public void trim(MemoryTrimType memoryTrimType) { trimToNothing(); }SharedByteArray.java:
public void trim(MemoryTrimType trimType) { if (!mSemaphore.tryAcquire()) { return; } try { mByteArraySoftRef.clear(); } finally { mSemaphore.release(); } }