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
相关产品推荐
相关产品推荐

