通过Fresco渲染图片到RecyclerView中的ImageView中

起因

由于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 an ImageView as the bitmap could be recycled any time after onNewResultImpl() completes.
We strongly recommend using a DraweeView instead of ImageView as it handles the lifecycle of the bitmap. If you must use an ImageView, you will have to use a BaseDataSubscriber<CloseableReference<CloseableImage>> and keep the CloseableReference until the image is no longer needed and then finally close it (for example, in onDestroy().
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. 在ActivityonDestroy中销毁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中创建由ContexthashCode索引FrescoCloseableHolderMap

private static final Map<Integer, FrescoCloseableHolder> frescoCloseableHolderMap = new HashMap<>();

3. 修改FrescoCloseableHolderdisplayThumbnail函数,请求图像时,先从frescoCloseableHolderMap中根据ContexthashCode取出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. 在BoxingViewFragmentBoxingBottomSheetFragmentonDestroy中调用销毁函数

@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

https://github.com/facebook/fresco/blob/0f3d52318631f2125e080d2a19f6fa13a31efb31/imagepipeline/src/main/java/com/facebook/imagepipeline/datasource/BaseBitmapDataSubscriber.java#L71

https://github.com/facebook/fresco/issues/1550

https://github.com/facebook/fresco/issues/1532

Share

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注