RecyclerView:异步加载图片

23

我正在使用 RecyclerView 显示包含 imageView 的列表。为了使UI更加流畅,我使用一个 asyncTask 将存储在SD卡上的58dp缩略图加载到这些imageView中。

问题是,一旦一个 childView 进入视觉显示,就会重用另一个数据的旧图像,然后在 AsyncTask 完成时替换。可以通过在 onPreExecute 中将 imageView 的位图设置为null来停止洗牌。

是否有一种方法真正重用旧图像,还是每次新的 View 进入位置时都必须从sd卡加载图像?这使视图非常丑陋,因为要么首先出现错误的图像,要么图像是纯白色的。

7个回答

34

由于视图重用,您将获取已经具有内容的视图,如果您正在使用ViewHolder模式,这也是ListViews的问题。

这里有两个解决方案:一个是好的做法,另一个是糟糕的Hack:

  • 在好的做法中,您可以在bindViewHolder(VH holder,int position)的开头设置ImageView以不显示任何内容,使用setDrawable(null)或类似方法。

  • 在糟糕的Hack中,您将不会回收/重用视图,也不强制使用ViewHolder模式,并且每次都会重新填充它,但这仅允许在ListView和其他旧组件中使用。


那么,一旦视图离开屏幕,与其相关联的旧位图就会被丢弃? - Paul Woitaschek
2
当您读取它们或在删除它们之前,您可以将它们放入LruCache中,并稍后重新获取它们。 - MLProgrammer-CiM
初始化ImageView,还是要使用setDrawable(null) - Jared Burrows
1
尝试使用 setDrawable / setBitmap = null 都不起作用。 - user3402040
无法工作,即使更多的 setBitmap(null) 延迟了洗牌,但最终问题仍然存在。 - Emmanuel Mtali

8
你应该查看通用图像加载器。它具有内存缓存、磁盘缓存,并且异步加载图像,不会阻塞ui。你可以设置默认图像和/或无法获取图像等。它可以对图像进行采样,以减少位图的内存占用。我真的建议你在处理图像时使用它。
对于你的情况,请勿禁用可回收性,因为这是毫无意义的。图像必须被回收,因为如果没有正确采样,它们的位图可生成非常高的内存负载。
RecyclerViewAdapter中的示例用法:
@Override
public void onBindViewHolder(CustomViewHolder viewHolder, int position) {
    String imageUri = "";//local or remote image uri address
    //viewHolder.imgView: reference to your imageview
    //before you call the displayImage you have to 
    //initialize imageloader in anywhere in your code for once.   
    //(Generally done in the Application class extender.)
    ImageLoader.getInstance().displayImage(imageUri, viewHolder.imgView);
}

编辑: 如今,我认为Glide是我的主要图像加载和缓存库。 您可以这样使用它:

Glide.with(context)
    .load(imageUri)
    .placeholder(R.drawable.myplaceholder)
    .into(imageView);

为什么要使用Glide而不是Picasso?有什么优势吗? - mhdjazmati
1
Picasso更好 +1 - user3402040
3
这取决于要求... Glide支持动画GIF和缩略图(适用于列表视图)。 - user846316

5
在开始新请求之前,您应该取消旧请求,但是即使取消了请求,如果两个图像在相同的容器/视图持有者上同时加载(在快速滚动和小图像中很容易发生),仍然可能会显示错误的图像。
解决方案是:
  1. 在onBindViewHolder期间,在View Holder中存储一些唯一标识符(这是同步发生的,因此如果VH被回收,这将被覆盖)
  2. 然后异步加载图像(使用AsynchTask、RxJava等),并传递此唯一ID以供参考
  3. 最后,在图像加载方法中进行后处理(对于AsyncTasks的onPostExecute),检查在异步请求中传递的ID是否与View Holder中当前存在的ID相同。
使用RxJava从应用程序中后台加载图标的示例:
 public void loadIcon(final ImageView appIconView, final ApplicationInfo appInfo, final String uniqueAppID) {
    Single.fromCallable(() -> {
            return appIconView.getContext().getPackageManager().getApplicationIcon(appInfo);
        })
          .subscribeOn(Schedulers.computation())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe( drawable -> {
                 if (uniqueAppID.equals(mUniqueAppID)) { // Show always the correct app icon
                    appIconView.setImageDrawable(drawable);
                 }
             }
         );
}

mUniqueAppID是视图持有者的一个字段,在onBindViewHolder中进行更改。


如果ID不同,则ImageView的Drawable保持为空白,那么在这种情况下我们应该怎么做? - Duna
1
@Duna,没错,如果id不同,这意味着viewHolder已经被回收,而drawable不是该视图的正确drawable。使用正确id的异步加载最终应该会发生。如果视图仍然为空,则可能意味着异步图像加载(在id检查之前)失败了。 - Sebas LG
我理解了你的观点,但是假设我们有10个可见的项目,在每个项目上调用onBindView。每个图像的加载需要1秒钟,但是mUniqueAppID已经设置为最新的项目(1秒钟还没有过去)->那么只会显示最后一个图像。 - Duna
1
@Duna 我理解了你的误解,mUniqueAppID 是 ViewHolder 中的一个字段,与 ViewHolder 中引用的视图相同,因此只要视图没有被回收,每个 ViewHolder 就会有一个 mUniqueAppID - Sebas LG

1

在“onBindViewHolder”方法中,您必须取消旧请求:

try{
        ((SpecialOfferViewHolder)viewHolder).imageContainer.cancelRequest();
}catch(Exception e) {

}

请记得在ViewHolder中保存图像容器:
public void onResponse(ImageContainer response, boolean arg1) {
                ((SpecialOfferViewHolder)viewHolder).imageContainer=response;

}


0

我想补充一下好的做法:

在bindViewHolder(VH holder, int position)的开始,使用setDrawable(null)或类似方法将您的ImageView设置为不显示任何内容。

不是不显示任何内容,而是显示一个加载器图像,以便向用户提供反馈,表明该视图正在进行一些处理,并且很快就会看到结果。仅看到空白视图并不是好的做法,您需要向用户提供反馈。


0

你应该使用Picasso这是一个强大的Android图片下载和缓存库。 它易于使用且功能强大。使用此库,您可以从资源、资产、文件、内容提供程序异步或同步获取图像。


-2

MLProgrammer所说的是非常正确的。

幸运的是,解决方案很简单:停止回收利用。

holder.setIsRecyclable(false);

这个选择有其后果,因为执行我上面建议的操作会抑制RecyclerView的一个主要目的:回收。

  • 您的RecyclerView滚动速度会变慢(每次都必须创建新的Holder,并从资源中再次膨胀);
  • 您的内存使用量会更大。

4
如果你在提出这个解决方案,至少要提到它的后果,因为这是一个不好的做法。 - MLProgrammer-CiM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接