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

如何避免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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 10:58:18