如何避免C#中Task.Run后台线程生成的Android Bitmap被自动释放?
Android Bitmap后台加载时被释放问题的排查与解决
问题背景
我在C#中封装了NativeBitmap类来包装Android的Bitmap:
public class NativeBitmap { public Android.Graphics.Bitmap bitmap; public int rawHeight = 0; public int rawWidth = 0; }
通过getNativeBitmapFromDisk方法从磁盘加载Bitmap到该类:
private async Task<NativeBitmap> getNativeBitmapFromDisk(string localPhotoPathAndName) { NativeBitmap nativeBitmap = new(); using (FileStream fs = File.Open(localPhotoPathAndName, FileMode.Open, FileAccess.Read, FileShare.Read)) { nativeBitmap.bitmap = Android.Graphics.BitmapFactory.DecodeStream(fs); nativeBitmap.rawWidth = nativeBitmap.bitmap.Width; nativeBitmap.rawHeight = nativeBitmap.bitmap.Height; } return nativeBitmap; }
该方法在主线程运行时正常,但通过Task.Run放到后台线程运行时不稳定,频繁抛出异常:
// 主线程方式(正常): photoNativeBitmap[0] = await getNativeBitmapFromDisk(localPhotoPathAndName); // 后台线程方式(不稳定): photoNativeBitmap[0] = await Task.Run(() => getNativeBitmapFromDisk(localPhotoPathAndName)); try { Debug.WriteLine("GOT ANDROID PHOTO, WIDTH: " + photoNativeBitmap[0].bitmap.Width); } catch (Exception e) { Debug.WriteLine("EXCEPTION ON TASK RUN: " + e.Message); }
异常信息:
Cannot access a disposed object. Object name: 'Android.Graphics.Bitmap'
疑问:这是预期行为还是C#/.NET的bug?如何通过Task.Run实现后台加载同时避免Bitmap被垃圾回收?
原因分析
这不是.NET的bug,是Android Bitmap的线程关联性和垃圾回收机制导致的预期行为:
- Android的
Bitmap对象(尤其是基于Skia的实现)和创建它的线程存在隐性关联,后台线程创建的Bitmap如果没有被主线程及时持有强引用,容易被GC标记回收。 - 后台线程执行完毕后,局部变量的生命周期结束,若主线程对Bitmap的引用未及时建立,GC可能提前回收该Bitmap资源。
解决方案
方案1:确保主线程立即持有强引用
在后台任务完成后,主线程立即将Bitmap对象的引用固定,避免GC回收:
// 后台线程加载后,强制回到主线程持有引用 photoNativeBitmap[0] = await Task.Run(() => getNativeBitmapFromDisk(localPhotoPathAndName)).ConfigureAwait(true); // 立即访问Bitmap属性,触发引用绑定,防止GC提前回收 if (photoNativeBitmap[0].bitmap != null) { var _ = photoNativeBitmap[0].bitmap.Width; var __ = photoNativeBitmap[0].bitmap.Height; }
ConfigureAwait(true)确保后续代码回到主线程执行,主线程的强引用会阻止GC回收Bitmap。
方案2:手动管理Bitmap生命周期
修改NativeBitmap类实现IDisposable接口,通过显式引用计数避免Bitmap被意外回收:
public class NativeBitmap : IDisposable { public Android.Graphics.Bitmap bitmap; public int rawHeight = 0; public int rawWidth = 0; private bool _disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; // 释放非托管Bitmap资源 bitmap?.Recycle(); bitmap = null; _disposed = true; } ~NativeBitmap() { Dispose(false); } }
使用时在主线程中保持对NativeBitmap对象的强引用,直到不再需要时手动释放:
// 后台加载后,主线程持有NativeBitmap强引用 var nativeBmp = await Task.Run(() => getNativeBitmapFromDisk(localPhotoPathAndName)).ConfigureAwait(true); photoNativeBitmap[0] = nativeBmp; Debug.WriteLine("GOT ANDROID PHOTO, WIDTH: " + nativeBmp.bitmap.Width); // 不再使用时手动释放 // photoNativeBitmap[0]?.Dispose();
方案3:创建跨线程兼容的Bitmap
通过BitmapFactory.Options设置inMutable=true,创建可跨线程访问的可变Bitmap,降低线程关联性导致的回收风险:
private async Task<NativeBitmap> getNativeBitmapFromDisk(string localPhotoPathAndName) { NativeBitmap nativeBitmap = new(); var options = new Android.Graphics.BitmapFactory.Options(); options.InMutable = true; // 创建可变Bitmap,减少线程绑定限制 using (FileStream fs = File.Open(localPhotoPathAndName, FileMode.Open, FileAccess.Read, FileShare.Read)) { nativeBitmap.bitmap = Android.Graphics.BitmapFactory.DecodeStream(fs, null, options); if (nativeBitmap.bitmap != null) { nativeBitmap.rawWidth = nativeBitmap.bitmap.Width; nativeBitmap.rawHeight = nativeBitmap.bitmap.Height; } } return nativeBitmap; }
内容的提问来源于stack exchange,提问作者mike
相关产品推荐
相关产品推荐

