WPF应用程序中使用页面和BitmapImage时的内存泄漏问题

3

我对C#和编程还比较陌生,但是我已经做了几个应用程序。通常只做一项任务然后退出的应用程序很简单,但是例如每天拍摄500张用户照片的系统给了我一个更大的挑战。

我的问题与WPF中的内存消耗有关。我有以下页面,加载时会不断消耗内存。我尝试使用内存分析工具并创建了一些快照来解决这个问题。然而,我很难理解何时/如何处理对象以确保GC处理其余部分。我特别遇到麻烦的其中一页是这一页:

第二页:

using EDSDKLib;
using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace PhotoBooth.Pages
{
    /// <summary>
    /// Interaction logic for Picture.xaml
    /// </summary>
    public partial class Picture : Page
    {
        int secondsToWait = 4;
        DispatcherTimer dispatcherTimer;
        Action<BitmapImage> SetImageAction;
        ImageBrush bgbrush = new ImageBrush();

        public Picture()
        {
            InitializeComponent();

            // Define steps
            Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);
           
            // Create TempLocation
            Directory.CreateDirectory(Settings.TempLocation);
            
            // Handle the Canon EOS camera
            Global.CameraHandler.ImageSaveDirectory = Settings.TempLocation;
            SetImageAction = (BitmapImage img) => { bgbrush.ImageSource = img; };

            // Configure the camera timer
            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Tick += DispatcherTimer_Tick;
            dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 800);
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            // Subscribe to camera events
            if (Global.CameraHandler != null)
            {
                Global.CameraHandler.LiveViewUpdated += CameraHandler_LiveViewUpdated;
                Global.CameraHandler.ImageSaved += CameraHandler_ImageSaved;
                Global.CameraHandler.CameraSDKError += CameraHandler_CameraSDKError;
            }

            // Start LiveView
            try
            {
                Console.WriteLine(Global.CameraHandler.IsLiveViewOn);
                if (!Global.CameraHandler.IsLiveViewOn)
                {
                    CameraLiveView.Background = bgbrush;
                    Global.CameraHandler.StartLiveView();
                }
            }
            catch (Exception)
            {
                // We cannot recover from that kind of errror. Reboot the application
                CameraCrashHandler();
            }
        }

        private void CameraTrigger_Click(object sender, RoutedEventArgs e)
        {
            // The user has clicked the trigger, change the layout
            CameraTrigger.Visibility = System.Windows.Visibility.Collapsed;
            CameraCountDown.Visibility = System.Windows.Visibility.Visible;
            CameraTrigger.IsEnabled = false;

            // Start the countdown
            secondsToWait = 4;
            dispatcherTimer.Start();
            Global.WriteToLog("INFO", "Camera shutter pressed... waiting for camera to take picture!");
        }

        private void DispatcherTimer_Tick(object sender, EventArgs e)
        {
            // Handles the countdown
            switch (secondsToWait)
            {
                case 4:
                    CameraTimer3.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 3:
                    CameraTimer2.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 2:
                    CameraTimer1.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 1:
                    CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraWhite.png"));
                    Global.CameraFlashEffect(((MasterPage)System.Windows.Application.Current.MainWindow).CameraFlash);
                    Global.CameraHandler.TakePhoto();
                    break;
                case 0:
                    CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraRed.png"));
                    CameraTimer1.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
                    CameraTimer2.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
                    CameraTimer3.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));

                    dispatcherTimer.Stop();
                    break;
                default:
                    break;
            }

            secondsToWait--;
        }

        private void Page_Unloaded(object sender, RoutedEventArgs e)
        {
            // Stop LiveView
            if (Global.CameraHandler.IsLiveViewOn)
            {
                CameraLiveView.Background = null;
                Global.CameraHandler.StopLiveView();
            }

            // Unsubscribe from events
            Global.CameraHandler.LiveViewUpdated -= CameraHandler_LiveViewUpdated;
            Global.CameraHandler.ImageSaved -= CameraHandler_ImageSaved;
            Global.CameraHandler.CameraSDKError -= CameraHandler_CameraSDKError;
        }

        #region CameraHandler

        void CameraHandler_CameraSDKError(string error)
        {
            // Handle known errors
            Global.WriteToLog("ERROR", "CameraSDKError: " + error);
            switch (error)
            {
                case "0x00008D01":
                    // Reset cameraTrigger for taking another photo
                    this.Dispatcher.Invoke((Action)(() =>
                    {
                        CameraTrigger.Visibility = System.Windows.Visibility.Visible;
                        CameraCountDown.Visibility = System.Windows.Visibility.Collapsed;
                        CameraTrigger.IsEnabled = true;
                    }));
                    break;
                default:
                    CameraCrashHandler();
                    break;
            }
        }

        void CameraHandler_ImageSaved(string img)
        {
            // Assign image to user
            Global.PersonObject.Image = img;

            // We have a image, let's navigate to the next page
            this.Dispatcher.Invoke((Action)(() =>
            {
                NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
            }));
        }

        void CameraHandler_LiveViewUpdated(Stream img)
        {
            try
            {
                if (Global.CameraHandler.IsLiveViewOn)
                {
                    using (WrappingStream s = new WrappingStream(img))
                    {
                        img.Position = 0;
                        BitmapImage EvfImage = new BitmapImage();
                        EvfImage.BeginInit();
                        EvfImage.StreamSource = s;
                        EvfImage.CacheOption = BitmapCacheOption.OnLoad;
                        EvfImage.EndInit();
                        EvfImage.Freeze();
                        Application.Current.Dispatcher.Invoke(SetImageAction, EvfImage);
                    }
                }
            }
            catch (Exception ex)
            {
                Global.WriteToLog("ERROR", "LiveViewUpdated failed: " + ex.Message);
            }
        }

        static void CameraCrashHandler()
        {
            // Camera cannot start
            Global.WriteToLog("ERROR", "Unkown CameraSDKError. Automatic reboot needed");
            MessageWindow mw = new MessageWindow("CameraErrorTitle", "CameraErrorMessage");
            mw.ShowDialog();

            // We cannot recover from that kind of errror. Reboot the application
            System.Windows.Forms.Application.Restart();
            System.Windows.Application.Current.Shutdown();
        }
  
        #endregion
    }
}

第三页:

using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PhotoBooth.Pages
{
    /// <summary>
    /// Interaction logic for PreviewID.xaml
    /// </summary>
    public partial class PreviewID : Page
    {
        public PreviewID()
        {
            InitializeComponent();

            // Define steps
            Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);

            // Load image and data
            PreviewIDText = GetIDText(Global.PersonObject, PreviewIDText);
            PreviewIDCard.Source = GetIDPhoto(Global.PersonObject);
            PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));
        }

        private void PreviewIDRetry_Click(object sender, RoutedEventArgs e)
        {
            Global.WriteToLog("INFO", "User did not approve the image, retry!");
            NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Backward)));
        }

        private void PreviewIDAccept_Click(object sender, RoutedEventArgs e)
        {
            Global.WriteToLog("INFO", "User approved the image");
            NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
        }

        public TextBlock GetIDText(Functions.Enums.Person p, TextBlock tb)
        {
            tb.Text = "";
            tb.FontSize = 24;

            if (p.Affiliation == Functions.Enums.Affiliation.Employee)
            {
                // Ansatt
                tb.Inlines.Add(new Run(p.FirstName + " " + p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold, FontSize = 30 });
                tb.Inlines.Add(p.Title + Environment.NewLine);
                tb.Inlines.Add(p.Department + Environment.NewLine);
                tb.Inlines.Add("Ansatt nr: " + p.EmployeeNumber + Environment.NewLine);
            }
            else
            {
                // Student
                tb.Inlines.Add("Last name:  ");
                tb.Inlines.Add(new Run(p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("First name:  ");
                tb.Inlines.Add(new Run(p.FirstName + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("Date of birth:  ");
                tb.Inlines.Add(new Run("dd.mm.yyyy" + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("Studentnr:  ");
                tb.Inlines.Add(new Run("xxxxxx" + Environment.NewLine) { FontWeight = FontWeights.Bold });
            }

            return tb;
        }

        public BitmapImage GetIDPhoto(Functions.Enums.Person p)
        {
            BitmapImage result;
            switch (p.Affiliation)
            {
                case PhotoBooth.Functions.Enums.Affiliation.Student:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
                    break;
                case PhotoBooth.Functions.Enums.Affiliation.Employee:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_employee.png"));
                    break;
                default:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
                    break;
            }

            return result;
        }

        private void Page_Unloaded(object sender, RoutedEventArgs e)
        {
            PreviewIDPhoto.Background = null;
        }
    }
}

虽然大多数全局函数在所有其他页面上都使用,但据我所知,正是这个页面给我带来了大部分麻烦。下面是我的内存性能测试截图。
  1. 用户从第1页导航到第2页(或其他页面),据我所见没有问题。内存使用量似乎很稳定。
  2. 用户触发带有LiveView的佳能相机(第2页)。内存消耗会上下波动,但保持稳定。
  3. 用户拍摄并获得预览图像(第3页),重试(返回第2页),再次拍摄,重试,以此类推......
  4. 每次加载后台代码:

enter image description here

这些BitmapImages是否引起了这个问题? 如果在那段代码中没有明显的问题,我应该如何继续测试内存泄漏?

我一直使用Ants Memory profiler来解决像这样的错误。BitmapImage通常是个麻烦制造者,总是从某些你认为已经被处理掉的视图元素中引用。使用Ants创建一个快照,然后查找所有BitmapImage实例并检查对象图以查看谁在持有引用。这将显示哪些元素停留在内存中,从而导致内存泄漏。 - dodsky
谢谢你的提示。我已经下载了这个工具并尝试了一下,但是我不太确定如何找出到底是什么导致了我的问题... http://i.imgur.com/38FN8yE.png - NeoID
看一下截图,在下面的图表中有一个名为“类列表”的按钮,当你有快照时打开它。在搜索框中输入BitmapImage,这将显示所有实例,然后只需按上方的按钮即可进入内存对象的图形界面。我看到你已经解决了这个问题,这是为了以后参考。 :) - dodsky
2个回答

1
我认为这是由于DispatcherTimer引起的。可能仅仅停止计时器在卸载页面时不足,因为WPF仍会存储对您的类的引用。尝试在卸载页面时注销事件。

谢谢你的建议。我也尝试过了,但是我仍然遇到了相同的内存问题。它一直在增加而不是减少,即使我让应用程序运行很长时间。因此,肯定还有其他东西没有从内存中卸载。 - NeoID
也许冻结BitmapImage会有所帮助。如果它没有被冻结,其他对象可能会注册到BitMapimage的更改事件中,可能导致内存泄漏。 - P3N9U1N
不要再用这个:PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));, 而是应该使用这样的代码:ImageBrush ib = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image))); ib.Freeze(); PreviewIDPhoto.Background = ib;?如果我这样做,并进行另一次分析,我仍然会得到相同的结果。 - NeoID
我现在已经删除了所有的代码,并逐行添加。在添加 new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image))) 之前,内存泄漏不是问题......这就是引起这个问题的原因。你有什么关于如何正确重写它的想法吗?添加 freeze 没有解决它。 - NeoID
var bmi = new BitmapImage(new Uri(Global.PersonObject.Image)); bmi.Freeze(); ImageBrush ib = new ImageBrush(bmi); ib.Freeze(); 冻结画刷和图像是否有帮助?如果没有,我想我不能帮你:( - P3N9U1N
也尝试过这样做,但似乎没有改变任何事情。目前唯一修复问题的方法是完全删除图像(这当然不是解决方案)。我还尝试过在XAML中绑定图像,但那也会造成内存泄漏。我真的不明白这是如何工作的了.... 我以为GC会处理所有这些东西。 - NeoID

0
根据内存分析器,我已经找到并解决了这个问题。虽然我还不太明白为什么,但它现在可以工作了。 :)
问题出在以下代码行:
PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));

一旦我将其注释掉,我就不再有任何内存问题了。然而,我很难重写它使其正常工作。似乎user1690200关于冻结图像的想法是正确的,但他发布的代码并不完全正确。

这对我有用:

            BitmapImage image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.UriSource = new Uri(Global.PersonObject.Image);
            image.DecodePixelWidth = 275; // Important as we do not want to load the whole image
            image.EndInit();

            image.Freeze(); // Call this after EndInit and before using the image.
            PreviewIDPhoto.Source = image;

我不相信 Freeze 解决了问题,它只是允许在另一个线程上访问图像,例如在后台线程中创建图像,然后在 UI 线程上加载它的场景。我认为 BitmapCacheOption.OnLoad 才是解决方法,你告诉 WPF,一旦创建此图像,从内存存储中加载它,而不要再分配它。 - dodsky
但是阅读这个问题的答案可能会说明为什么您看到了内存泄漏。问题链接 - dodsky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接