起因
由于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
BaseBitmapDataSubscriber
and then set the bitmap to anImageView
as the bitmap could be recycled any time afteronNewResultImpl()
completes.
We strongly recommend using aDraweeView
instead 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 theCloseableReference
until 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