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

iOS 16真机NET MAUI应用崩溃:未手动释放Observer对象

.NET MAUI iOS端相册加载崩溃问题解决

问题现象

在iOS 16的iPhone 8真机热重启测试场景下,页面每次OnAppearing都会执行相册图片加载方法,但应用最小化后返回触发OnAppearing时,应用直接终止,报错信息如下:

2022-11-22 13:58:31.287 Xamarin.PreBuilt.iOS[1018:30754] Warning: observer object was not disposed manually with Dispose()

Android端实现无此异常,图片可正常添加到ObservableCollection并展示。

核心问题分析

报错直接指向未手动释放观察者对象,结合代码细节排查出三个关键点:

  1. PHCachingImageManager作为iOS相册缓存管理类,属于NSObject子类,必须手动调用Dispose()释放资源,否则会引发内存泄漏和观察者未清理问题。
  2. 页面重复触发加载时,旧的PHCachingImageManager实例未被释放,叠加新实例导致资源冲突。
  3. 页面消失时未终止正在进行的相册加载任务,后台任务持有已销毁页面的引用,触发崩溃。

修复方案

1. 让MediaService实现资源释放逻辑

修改MediaService继承IDisposable接口,手动管理PHCachingImageManager和任务取消令牌:

public class MediaService : IDisposable
{
    bool requestStop = false;
    bool _isLoading = false;
    public bool IsLoading => _isLoading;
    public event EventHandler<MediaEventArgs> OnMediaAssetLoaded;
    
#if IOS
    private PHCachingImageManager _imageManager;
    private CancellationTokenSource _cts; // 用于取消正在进行的加载任务
#endif

    // ... 原有RequestPermissionAsync等代码保持不变 ...

    public void Dispose()
    {
#if IOS
        requestStop = true;
        _cts?.Cancel();
        _cts?.Dispose();
        // 停止缓存并释放图片管理器资源
        _imageManager?.StopCaching();
        _imageManager?.Dispose();
#endif
    }

    public async Task<IList<MediaAsset>> RetrieveMediaAssetsAsync(CancellationToken? cancelToken = null)
    {
#if IOS
        requestStop = false;
        // 先取消之前未完成的任务
        _cts?.Cancel();
        _cts?.Dispose();
        _cts = new CancellationTokenSource();

        // 合并外部传入令牌与内部取消令牌
        var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(
            cancelToken ?? CancellationToken.None, 
            _cts.Token).Token;

        var taskCompletionSource = new TaskCompletionSource<IList<MediaAsset>>();
        combinedToken.Register(() =>
        {
            requestStop = true;
            taskCompletionSource.TrySetCanceled();
        });

        _isLoading = true;
        var task = LoadMediaAsync();
        var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
        _isLoading = false;
        return await completedTask;
#endif
        // Android端原有代码保持不变
        List<MediaAsset> mediaAssets = new List<MediaAsset>();
        return mediaAssets;
    }

    async Task<IList<MediaAsset>> LoadMediaAsync()
    {
        IList<MediaAsset> assets = new List<MediaAsset>();
#if IOS
        // 初始化新的图片管理器,旧实例已在Dispose中释放
        _imageManager = new PHCachingImageManager();
        var hasPermission = await RequestPermissionAsync();
        if (hasPermission)
        {
            await Task.Run(async () =>
            {
                // ... 原有缩略图配置、资源获取逻辑保持不变 ...
            });
            _imageManager.StopCaching();
        }
#endif
        return assets;
    }
}

2. 页面消失时释放ViewModel资源

让ViewModel实现IDisposable,绑定Disappearing事件触发资源清理:

public class YourViewModel : INotifyPropertyChanged, IDisposable
{
    private MediaService _mediaService;
    public ObservableCollection<MediaAsset> MediaAssets { get; set; } = new();

    public YourViewModel()
    {
        _mediaService = new MediaService();
        _mediaService.OnMediaAssetLoaded += MediaService_OnMediaAssetLoaded;
    }

    [RelayCommand]
    public async void LoadNewPictures()
    {
        if (_mediaService.IsLoading) return; // 防止重复触发加载
        var fetch = await _mediaService.RetrieveMediaAssetsAsync();
        // 处理加载结果到ObservableCollection
    }

    [RelayCommand]
    public void DisatachCommand()
    {
        Dispose();
    }

    private void MediaService_OnMediaAssetLoaded(object sender, MediaEventArgs e)
    {
        // 在主线程更新UI集合
        MainThread.BeginInvokeOnMainThread(() =>
        {
            MediaAssets.Add(e.Asset);
        });
    }

    public void Dispose()
    {
        _mediaService.OnMediaAssetLoaded -= MediaService_OnMediaAssetLoaded;
        _mediaService.Dispose();
    }
}

3. 优化异步回调的同步逻辑

原有图片请求使用异步回调,可能导致asset未完全初始化就被添加到集合,改用TaskCompletionSource等待回调完成:

// 替换原有缩略图请求逻辑
var thumbnailTcs = new TaskCompletionSource<UIImage>();
imageManager.RequestImageForAsset(phAsset, thumbnailSize, PHImageContentMode.AspectFit, thumbnailRequestOptions, (image, info) =>
{
    thumbnailTcs.TrySetResult(image);
});
var thumbnailImage = await thumbnailTcs.Task;

if (thumbnailImage != null)
{
    // 处理缩略图保存逻辑
    NSData imageData = thumbnailImage.CGImage.RenderingIntent == CGColorRenderingIntent.Default 
        ? thumbnailImage.AsJPEG(0.8f) 
        : thumbnailImage.AsPNG();
    
    if (imageData != null)
    {
        var fileName = Path.Combine(tmpPath, $"tmp_thumbnail_{Path.GetFileNameWithoutExtension(name)}.jpg");
        NSError error = null;
        imageData.Save(fileName, true, out error);
        if (error == null)
        {
            asset.PreviewPath = fileName;
        }
    }
}

关键注意事项

  • iOS平台的NSObject子类(如PHCachingImageManager、PHAsset)必须手动调用Dispose(),否则会引发内存泄漏和观察者未清理问题。
  • 页面切换时必须终止正在进行的异步任务,避免后台任务持有已销毁页面的引用导致崩溃。
  • 热重启场景下iOS的资源回收机制更敏感,必须严格遵循内存管理规范。

内容的提问来源于stack exchange,提问作者Alexander Petrov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 10:40:30