起因
由于RecyclerView复用控件,所以ViewHolder中的同一个ImageView会被循环利用,来显示列表中的不同图片,这就涉及到了bitmap内存控制的问题。
ImageView当前显示着bitmap1,随着向下滚动,ImageView可能显示了bitmap2,接着向上滚动,可能又需要显示bitmap1,这时bitmap1是否已经被recycled?
如果只是直接使用Fresco自带的控件DraweeView来渲染图片的话,没有任何问题,因为DraweeView做好了内存控制,但是如果一定要使用Fresco来向ImageView中填充图片,又要保证内存控制,该怎么做呢?
通过Fresco得到Drawable,向ImageView中填充
final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(string))
.build();
final DataSource<CloseableReference<CloseableImage>> dataSource =
Fresco.getImagePipeline().fetchDecodedImage(request, null);
final String finalString = string;
dataSource.subscribe(new BaseDataSubscriber<CloseableReference<CloseableImage>>() {
@Override
protected void onNewResultImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
CloseableReference<CloseableImage> result = dataSource.getResult();
if (result == null) {
onFailureImpl(dataSource);
return;
}
fillImage(result, imageView);
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
}
}, UiThreadImmediateExecutorService.getInstance());
private void fillImage(CloseableReference<CloseableImage> imageCloseableReference, ImageView imageView) {
try {
Drawable drawable = BoxingFrescoLoader.createDrawableFromFetchedResult(imageView.getContext(), imageCloseableReference.get());
if (drawable == null) {
return;
}
if (drawable instanceof AnimatedDrawable2) {
imageView.setImageDrawable(drawable);
((AnimatedDrawable2) drawable).start();
} else {
imageView.setImageDrawable(drawable);
}
} catch (UnsupportedOperationException e) {
}
}
public static Drawable createDrawableFromFetchedResult(Context context, CloseableImage image) {
if (image instanceof CloseableStaticBitmap) {
CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) image;
BitmapDrawable bitmapDrawable = createBitmapDrawable(context, closeableStaticBitmap.getUnderlyingBitmap());
return (closeableStaticBitmap.getRotationAngle() != 0 && closeableStaticBitmap.getRotationAngle() != -1 ? new OrientedDrawable(bitmapDrawable, closeableStaticBitmap.getRotationAngle()) : bitmapDrawable);
} else if (image instanceof CloseableAnimatedImage) {
DrawableFactory animatedDrawableFactory = Fresco.getImagePipelineFactory().getAnimatedDrawableFactory(context);
if (animatedDrawableFactory != null) {
AnimatedDrawable2 animatedDrawable = (AnimatedDrawable2) animatedDrawableFactory.createDrawable(image);
if (animatedDrawable != null) {
return animatedDrawable;
}
}
}
throw new UnsupportedOperationException("Unrecognized image class: " + image);
}
private static BitmapDrawable createBitmapDrawable(Context context, Bitmap bitmap) {
BitmapDrawable drawable;
if (context != null) {
drawable = new BitmapDrawable(context.getResources(), bitmap);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& drawable.canApplyTheme()) {
drawable.applyTheme(context.getTheme());
}
} else {
drawable = new BitmapDrawable(null, bitmap);
}
return drawable;
}
问题
如果只是像上述这样做,在非列表场景下,不会有问题,但是如果在RecyclerView中使用,就容易出现如下问题:
Canvas: trying to use a recycled bitmap
出现这个错误的原因如下所述(https://github.com/facebook/fresco/issues/1550)
You shouldn’t use a
BaseBitmapDataSubscriberand then set the bitmap to anImageViewas the bitmap could be recycled any time afteronNewResultImpl()completes.
We strongly recommend using aDraweeViewinstead of ImageView as it handles the lifecycle of the bitmap. If you must use an ImageView, you will have to use aBaseDataSubscriber<CloseableReference<CloseableImage>>and keep theCloseableReferenceuntil the image is no longer needed and then finally close it (for example, inonDestroy().
Take a look under “To get decoded image…” in this page of the docs.
解决方案
所以我们需要持有CloseableReference的引用,直到需要销毁为止
1. 定义一个FrescoCloseableHolder,用于持有和销毁CloseableReference的引用,并且作为缓存
public class FrescoCloseableHolder {
private final Map<String, CloseableReference<CloseableImage>> cache = new HashMap<>();
public void add(String path, CloseableReference<CloseableImage> closeableReference) {
if (closeableReference == null || cache.containsKey(path)) {
return;
}
cache.put(path, closeableReference);
}
public void destroy() {
Iterator<String> iterator = cache.keySet().iterator();
while (iterator.hasNext()) {
CloseableReference closeableReference = cache.get(iterator.next());
iterator.remove();
if (closeableReference != null) {
CloseableReference.closeSafely(closeableReference);
}
}
}
public CloseableReference<CloseableImage> getCloseable(String path) {
return cache.get(path);
}
}
2. 在Activity中创建FrescoCloseableHolder对象
private final FrescoCloseableHolder frescoCloseableHolder = new FrescoCloseableHolder();
3. 请求图像时,先从FrescoCloseableHolder的缓存中取出CloseableReference用于渲染,如果缓存中没有,则需要在渲染后将CloseableReference放到FrescoCloseableHolder的缓存中:
CloseableReference<CloseableImage> result = frescoCloseableHolder.getCloseable(string);
if (result != null) {
fillImage(result, imageView);
} else {
final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(string))
.build();
final DataSource<CloseableReference<CloseableImage>> dataSource =
Fresco.getImagePipeline().fetchDecodedImage(request, null);
final String finalString = string;
dataSource.subscribe(new BaseDataSubscriber<CloseableReference<CloseableImage>>() {
@Override
protected void onNewResultImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
CloseableReference<CloseableImage> result = dataSource.getResult();
if (result == null) {
onFailureImpl(dataSource);
return;
}
fillImage(result, imageView);
frescoCloseableHolder.add(finalString, result);
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
}
}, UiThreadImmediateExecutorService.getInstance());
}
4. 在Activity的onDestroy中销毁FrescoCloseableHolder中的CloseableReference:
@Override
protected void onDestroy() {
super.onDestroy();
frescoCloseableHolder.destroy();
}
拓展
Bilibili开源的Boxing图片选择器由于支持集成uCrop并且支持使用各种图片加载框架,所以在项目中使用。但是如果使用boxing-impl并且使用Fresco作为图片加载框架,在选择图片时,如果往回滑动,将会遇到如上问题(https://github.com/bilibili/boxing/issues/21)
解决方案
1. 首先需要把boxing-impl的源代码下载下来导入工程并引用
2. 在BoxingFrescoLoader中创建由Context的hashCode索引FrescoCloseableHolder的Map:
private static final Map<Integer, FrescoCloseableHolder> frescoCloseableHolderMap = new HashMap<>();
3. 修改FrescoCloseableHolder的displayThumbnail函数,请求图像时,先从frescoCloseableHolderMap中根据Context的hashCode取出FrescoCloseableHolder,再从其缓存中取出CloseableReference用于渲染,如果没有,则需要在渲染后将保留CloseableReference到缓存中:
@Override
public void displayThumbnail(@NonNull final ImageView img, @NonNull final String absPath, int width, int height) {
String finalAbsPath = "file://" + absPath;
int hash = img.getContext().hashCode();
FrescoCloseableHolder holder = frescoCloseableHolderMap.get(hash);
if (holder != null) {
CloseableReference<CloseableImage> result = holder.getCloseable(finalAbsPath);
if (result != null) {
Drawable drawable = createDrawableFromFetchedResult(img.getContext(), result.get());
img.setImageDrawable(drawable);
return;
}
}
ImageRequestBuilder requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(finalAbsPath));
requestBuilder.setResizeOptions(new ResizeOptions(width, height));
ImageRequest request = requestBuilder.build();
final DataSource<CloseableReference<CloseableImage>> dataSource =
Fresco.getImagePipeline().fetchDecodedImage(request, null);
dataSource.subscribe(new BaseDataSubscriber<CloseableReference<CloseableImage>>() {
@Override
protected void onNewResultImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
String path = (String) img.getTag(R.string.boxing_app_name);
if (path == null || absPath.equals(path)) {
if (dataSource.getResult() == null) {
onFailureImpl(dataSource);
return;
}
CloseableReference<CloseableImage> result = dataSource.getResult();
Drawable drawable = createDrawableFromFetchedResult(img.getContext(), result.get());
img.setImageDrawable(drawable);
int hash = img.getContext().hashCode();
synchronized (frescoCloseableHolderMap) {
FrescoCloseableHolder holder = frescoCloseableHolderMap.get(hash);
if (holder != null) {
holder.add(finalAbsPath, result);
} else {
holder = new FrescoCloseableHolder();
holder.add(finalAbsPath, result);
frescoCloseableHolderMap.put(hash, holder);
}
}
}
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
img.setImageResource(R.drawable.ic_boxing_broken_image);
}
}, UiThreadImmediateExecutorService.getInstance());
}
4. 在BoxingFrescoLoader中创建FrescoCloseableHolder销毁函数
public static void notifyContextDestroy(Context context) {
if (context == null) {
return;
}
int hash = context.hashCode();
synchronized (frescoCloseableHolderMap) {
FrescoCloseableHolder holder = frescoCloseableHolderMap.get(hash);
if (holder != null) {
holder.destroy();
frescoCloseableHolderMap.remove(hash);
}
}
}
5. 在BoxingViewFragment和BoxingBottomSheetFragment的onDestroy中调用销毁函数
@Override
public void onDestroy() {
super.onDestroy();
BoxingFrescoLoader.notifyContextDestroy(getActivity());
}
参考文献
https://github.com/bilibili/boxing/issues/21
http://ju.outofmemory.cn/entry/301143
https://github.com/razerdp/FriendCircle/issues/52
https://github.com/facebook/fresco/issues/648
https://github.com/facebook/fresco/issues/1726
https://github.com/facebook/fresco/issues/709
https://github.com/facebook/fresco/issues/717