You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView异步加载致显示异常,同步加载卡顿如何解决?

这个问题的核心在于RecyclerView的ViewHolder复用机制和异步任务的生命周期不匹配——当你的异步解码任务完成时,对应的ViewHolder可能已经被回收,重新绑定到了另一个列表项,这就导致了数据错乱、重复显示的问题。下面是几个针对性的解决方案,既能保持异步加载避免UI卡顿,又能彻底解决条目异常:

1. 给ViewHolder绑定任务标识,校验后再更新UI

在异步任务执行前,把当前的position绑定到ViewHolder的Tag上,当任务完成后,先检查ViewHolder当前的Tag是否和任务对应的position一致,只有一致时才更新UI,避免给复用后的ViewHolder设置错误的数据。

修改你的SetContentAsync方法:

private async Task SetContentAsync(PhotoViewHolder vh, int position) {
    // 给当前ViewHolder标记对应的position,用于后续校验
    vh.Tag = position;

    string songName = "";
    string artistName = "";
    byte[] albumArtData = null;

    // 关键:不要复用全局的MediaMetadataRetriever!每个任务创建新实例,避免多线程冲突
    using (var reader = new MediaMetadataRetriever()) {
        try {
            reader.SetDataSource(mp3Obj[position].Mp3Uri);
            songName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyTitle);
            artistName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyArtist);
            albumArtData = reader.GetEmbeddedPicture();
        } catch {
            ((Activity)ctx).RunOnUiThread(() => 
                Toast.MakeText(ctx, "ERROR 77s9", ToastLength.Short).Show()
            );
        }
    } // using块会自动释放reader资源,避免内存泄漏

    // 异步任务完成后,先校验ViewHolder是否还绑定当前position
    if ((int)vh.Tag != position) {
        return; // ViewHolder已被复用,直接跳过更新
    }

    // await后会自动回到UI线程,无需手动调用RunOnUiThread
    vh.SongName.SetTypeface(tf, TypefaceStyle.Normal);
    vh.AristName.SetTypeface(tf, TypefaceStyle.Normal);
    vh.SongName.Text = songName;
    vh.AristName.Text = artistName;

    try {
        if (albumArtData != null) {
            var requestOptions = new RequestOptions()
                .DiskCacheStrategy(DiskCacheStrategy.None)
                .CircleCrop()
                .CenterInside();

            Glide.With(ctx)
                 .Load(albumArtData)
                 .Apply(requestOptions)
                 .Into(vh.CoverArt);

            // 安全处理背景模糊逻辑(已在UI线程)
            ConvertBitmapToBackground(BitmapFactory.DecodeByteArray(albumArtData, 0, albumArtData.Length), vh, albumArtData);
        } else {
            vh.CoverArt.SetImageResource(Resource.Drawable.btn_musicalnote);
            vh.dr = null;
        }
    } catch {
        Toast.MakeText(ctx, "ERROR 034c", ToastLength.Short).Show();
    }
}

2. 利用Glide的内置能力,简化异步加载逻辑

你手动解码Bitmap的步骤其实是多余的——Glide本身就支持异步加载字节数组,并且已经内置了ViewHolder复用的处理逻辑。可以把元数据提取的逻辑异步化,然后直接交给Glide处理:

private async Task SetContentAsync(PhotoViewHolder vh, int position) {
    vh.Tag = position;
    string songName = "";
    string artistName = "";
    byte[] albumArtData = null;

    // 把耗时的元数据提取逻辑放在Task.Run里,不阻塞UI线程
    await Task.Run(() => {
        using (var reader = new MediaMetadataRetriever()) {
            try {
                reader.SetDataSource(mp3Obj[position].Mp3Uri);
                songName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyTitle);
                artistName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyArtist);
                albumArtData = reader.GetEmbeddedPicture();
            } catch {
                ((Activity)ctx).RunOnUiThread(() => 
                    Toast.MakeText(ctx, "ERROR 77s9", ToastLength.Short).Show()
                );
            }
        }
    });

    if ((int)vh.Tag != position) return;

    // 更新文本UI
    vh.SongName.Text = songName;
    vh.AristName.Text = artistName;

    // 交给Glide处理封面加载,自动处理复用问题
    var coverRequest = Glide.With(ctx);
    if (albumArtData != null) {
        coverRequest.Load(albumArtData)
                    .Apply(new RequestOptions()
                        .CircleCrop()
                        .DiskCacheStrategy(DiskCacheStrategy.None))
                    .Into(vh.CoverArt);
    } else {
        coverRequest.Load(Resource.Drawable.btn_musicalnote)
                    .Into(vh.CoverArt);
        vh.dr = null;
    }
}

3. 监听ViewHolder回收,取消未完成的异步任务

如果你的异步任务比较耗时,可以在Adapter中重写OnViewRecycled方法,取消ViewHolder对应的未完成任务,避免不必要的资源消耗和错误更新:

首先给ViewHolder添加取消令牌源:

public class PhotoViewHolder : RecyclerView.ViewHolder {
    public TextView SongName { get; set; }
    public TextView AristName { get; set; }
    public ImageView CoverArt { get; set; }
    public Drawable dr { get; set; }
    // 用于取消异步任务的令牌源
    public CancellationTokenSource Cts { get; set; }

    public PhotoViewHolder(View itemView) : base(itemView) {
        // 初始化控件
        SongName = itemView.FindViewById<TextView>(Resource.Id.songName);
        AristName = itemView.FindViewById<TextView>(Resource.Id.artistName);
        CoverArt = itemView.FindViewById<ImageView>(Resource.Id.coverArt);
    }
}

然后在Adapter中重写回收方法:

public override void OnViewRecycled(PhotoViewHolder holder) {
    base.OnViewRecycled(holder);
    // 取消未完成的异步任务
    holder.Cts?.Cancel();
    holder.Cts?.Dispose();
    holder.Cts = null;
    // 取消Glide的请求,避免加载到已回收的ViewHolder
    Glide.With(ctx).Clear(holder.CoverArt);
}

最后修改SetContentAsync方法,使用CancellationToken:

private async Task SetContentAsync(PhotoViewHolder vh, int position) {
    // 取消之前的任务(如果有)
    vh.Cts?.Cancel();
    vh.Cts = new CancellationTokenSource();
    var token = vh.Cts.Token;

    string songName = "";
    string artistName = "";
    byte[] albumArtData = null;

    try {
        await Task.Run(() => {
            // 检查令牌是否已取消,提前终止任务
            token.ThrowIfCancellationRequested();
            using (var reader = new MediaMetadataRetriever()) {
                reader.SetDataSource(mp3Obj[position].Mp3Uri);
                token.ThrowIfCancellationRequested();
                songName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyTitle);
                artistName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyArtist);
                albumArtData = reader.GetEmbeddedPicture();
            }
        }, token);
    } catch (OperationCanceledException) {
        // 任务被取消,直接返回
        return;
    } catch {
        ((Activity)ctx).RunOnUiThread(() => 
            Toast.MakeText(ctx, "ERROR 77s9", ToastLength.Short).Show()
        );
        return;
    }

    if ((int)vh.Tag != position || token.IsCancellationRequested) return;

    // 后续UI更新逻辑...
}

核心要点总结

  • 永远不要假设ViewHolder会一直绑定同一个position,必须通过校验确保异步任务完成时ViewHolder的有效性;
  • 避免复用全局的MediaMetadataRetriever,每个异步任务创建独立实例,防止多线程冲突;
  • 利用Glide等图片加载库的异步能力,它们已经处理了ViewHolder复用的问题;
  • 在ViewHolder回收时取消未完成的异步任务,节省系统资源。

内容的提问来源于stack exchange,提问作者innomotion media

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:04:45