From 4a7504df5cde656419ad81db4a270dac1bab14e3 Mon Sep 17 00:00:00 2001 From: Niko-O Date: Sat, 12 May 2018 00:24:24 +0200 Subject: [PATCH] Maybe fix memory leak caused by not disposing Icons and Bitmaps. Icon implements IDisposable and must therefore be disposed. AudioDeviceIconExtractor creates and caches a lot of icons but never frees old cache entries. TrayIcon.UpdateIcon also creates many copies of Resources.SoundSwitch16. Getters in the generated Resource class always return new instances of Bitmaps and Icons. This commit removes the caching and adds code to manually dispose icons used in various locations. Some uses of the Resource class are cached instead (where the resource is not disposed anyways) to avoid loading the same resource multiple times from disk. I didn't touch generated files, though. This commit is untested since I cannot compile the project. --- .../Notification/NotificationBanner.cs | 4 +- SoundSwitch/UI/Forms/About.cs | 3 +- SoundSwitch/UI/Forms/Settings.cs | 23 ++++++- SoundSwitch/UI/Forms/UpdateDownloadForm.cs | 4 +- SoundSwitch/Util/AudioDeviceIconExtractor.cs | 66 ++----------------- SoundSwitch/Util/ToolStripDeviceItem.cs | 4 +- SoundSwitch/Util/TrayIcon.cs | 57 +++++++++++----- 7 files changed, 80 insertions(+), 81 deletions(-) diff --git a/SoundSwitch/Framework/NotificationManager/Notification/NotificationBanner.cs b/SoundSwitch/Framework/NotificationManager/Notification/NotificationBanner.cs index 42b6e5bf7e..4dc160c029 100644 --- a/SoundSwitch/Framework/NotificationManager/Notification/NotificationBanner.cs +++ b/SoundSwitch/Framework/NotificationManager/Notification/NotificationBanner.cs @@ -32,11 +32,13 @@ public class NotificationBanner : INotification public void NotifyDefaultChanged(MMDevice audioDevice) { + var Icon = AudioDeviceIconExtractor.ExtractIconFromAudioDevice(audioDevice, true); var toastData = new BannerData { - Image = AudioDeviceIconExtractor.ExtractIconFromAudioDevice(audioDevice, true).ToBitmap(), + Image = Icon.ToBitmap(), Text = audioDevice.FriendlyName }; + Icon.Dispose(); if (Configuration.CustomSound != null && File.Exists(Configuration.CustomSound.FilePath)) { toastData.SoundFile = Configuration.CustomSound; diff --git a/SoundSwitch/UI/Forms/About.cs b/SoundSwitch/UI/Forms/About.cs index 4b8be8faf4..28c33a5559 100644 --- a/SoundSwitch/UI/Forms/About.cs +++ b/SoundSwitch/UI/Forms/About.cs @@ -22,11 +22,12 @@ namespace SoundSwitch.UI.Forms { public partial class About : Form { + private static readonly System.Drawing.Icon helpIcon = Resources.HelpIcon; public About() { InitializeComponent(); - Icon = Resources.HelpIcon; + Icon = helpIcon; } private void About_Load(object sender, System.EventArgs e) diff --git a/SoundSwitch/UI/Forms/Settings.cs b/SoundSwitch/UI/Forms/Settings.cs index e338a8cdea..5c74dd3c21 100644 --- a/SoundSwitch/UI/Forms/Settings.cs +++ b/SoundSwitch/UI/Forms/Settings.cs @@ -38,13 +38,15 @@ namespace SoundSwitch.UI.Forms { public sealed partial class SettingsForm : Form { + private static readonly Icon settingsIcon = Resources.SettingsIcon; + private readonly bool _loaded; public SettingsForm() { // Form itself InitializeComponent(); - Icon = Resources.SettingsIcon; + Icon = settingsIcon; Text = AssemblyUtils.GetReleaseState() == AssemblyUtils.ReleaseState.Beta ? $"{SettingsStrings.settings} {AssemblyUtils.GetReleaseState()}" : SettingsStrings.settings; @@ -234,6 +236,25 @@ private void closeButton_Click(object sender, EventArgs e) Close(); } + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + foreach (var i in playbackListView.SmallImageList.Images) + { + if (i is IDisposable) + { + ((IDisposable)i).Dispose(); + } + } + foreach (var i in recordingListView.SmallImageList.Images) + { + if (i is IDisposable) + { + ((IDisposable)i).Dispose(); + } + } + } + private void tabControl_SelectedIndexChanged(object sender, EventArgs e) { var tabControlSender = (TabControl)sender; diff --git a/SoundSwitch/UI/Forms/UpdateDownloadForm.cs b/SoundSwitch/UI/Forms/UpdateDownloadForm.cs index 580a0e750a..e13a4cfb9f 100644 --- a/SoundSwitch/UI/Forms/UpdateDownloadForm.cs +++ b/SoundSwitch/UI/Forms/UpdateDownloadForm.cs @@ -26,13 +26,15 @@ namespace SoundSwitch.UI.Forms { public sealed partial class UpdateDownloadForm : Form { + private static readonly System.Drawing.Icon updateIcon = Resources.UpdateIcon; + private readonly bool _redirectLinks = false; private readonly WebFile _releaseFile; public UpdateDownloadForm(Release release) { InitializeComponent(); - Icon = Resources.UpdateIcon; + Icon = updateIcon; Text = release.Name; LocalizeForm(); Focus(); diff --git a/SoundSwitch/Util/AudioDeviceIconExtractor.cs b/SoundSwitch/Util/AudioDeviceIconExtractor.cs index d5a83a1509..19bfa51589 100644 --- a/SoundSwitch/Util/AudioDeviceIconExtractor.cs +++ b/SoundSwitch/Util/AudioDeviceIconExtractor.cs @@ -24,51 +24,8 @@ namespace SoundSwitch.Util { internal class AudioDeviceIconExtractor { - private class IconKey : IEquatable - { - private string FilePath { get; } - private bool Large { get; } - - public IconKey(string filePath, bool large) - { - FilePath = filePath; - Large = large; - } - - public override int GetHashCode() - { - unchecked - { - return (FilePath.GetHashCode()*397) ^ Large.GetHashCode(); - } - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((IconKey) obj); - } - - public static bool operator ==(IconKey left, IconKey right) - { - return Equals(left, right); - } - - public static bool operator !=(IconKey left, IconKey right) - { - return !Equals(left, right); - } - - public bool Equals(IconKey other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(FilePath, other.FilePath) && Large == other.Large; - } - } - private static readonly Dictionary IconCache = new Dictionary(); + private static readonly Icon defaultSpeakers = Resources.defaultSpeakers; + private static readonly Icon defaultMicrophone = Resources.defaultMicrophone; /// /// Extract the Icon out of an AudioDevice @@ -78,24 +35,18 @@ public bool Equals(IconKey other) /// public static Icon ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon) { - Icon ico; - var iconKey = new IconKey(audioDevice.IconPath, largeIcon); - if (IconCache.TryGetValue(iconKey, out ico)) - { - return ico; - } try { if (audioDevice.IconPath.EndsWith(".ico")) { - ico = Icon.ExtractAssociatedIcon(audioDevice.IconPath); + return Icon.ExtractAssociatedIcon(audioDevice.IconPath); } else { var iconInfo = audioDevice.IconPath.Split(','); var dllPath = iconInfo[0]; var iconIndex = int.Parse(iconInfo[1]); - ico = IconExtractor.Extract(dllPath, iconIndex, largeIcon); + return IconExtractor.Extract(dllPath, iconIndex, largeIcon); } } catch (Exception e) @@ -104,18 +55,13 @@ public static Icon ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIc switch (audioDevice.DataFlow) { case DataFlow.Capture: - ico = Resources.defaultSpeakers; - break; + return defaultSpeakers; case DataFlow.Render: - ico = Resources.defaultMicrophone; - break; + return defaultMicrophone; default: throw new ArgumentOutOfRangeException(); } } - - IconCache.Add(iconKey, ico); - return ico; } } } \ No newline at end of file diff --git a/SoundSwitch/Util/ToolStripDeviceItem.cs b/SoundSwitch/Util/ToolStripDeviceItem.cs index 6940ee9e47..c574d8e309 100644 --- a/SoundSwitch/Util/ToolStripDeviceItem.cs +++ b/SoundSwitch/Util/ToolStripDeviceItem.cs @@ -24,6 +24,8 @@ namespace SoundSwitch.Util { internal class ToolStripDeviceItem : ToolStripMenuItem { + private static readonly Bitmap check = Resources.Check; + public ToolStripDeviceItem(EventHandler onClick, MMDevice audioDevice) : base(audioDevice.FriendlyName, null, onClick) { @@ -35,7 +37,7 @@ public override Image Image get { if (AudioDevice !=null && AudioController.IsDefault(AudioDevice.ID, (DeviceType) AudioDevice.DataFlow, DeviceRole.Console)) - return Resources.Check; + return check; return null; } diff --git a/SoundSwitch/Util/TrayIcon.cs b/SoundSwitch/Util/TrayIcon.cs index 6a2d75e0d6..440f2d52da 100644 --- a/SoundSwitch/Util/TrayIcon.cs +++ b/SoundSwitch/Util/TrayIcon.cs @@ -37,6 +37,17 @@ namespace SoundSwitch.Util { public sealed class TrayIcon : IDisposable { + private static readonly Bitmap updateBitmap = Resources.Update; + private static readonly Bitmap settingsSmall = Resources.SettingsSmall; + private static readonly Bitmap soundSwitch16 = Resources.SoundSwitch16; + private static readonly Bitmap playbackDevices = Resources.PlaybackDevices; + private static readonly Bitmap mixer = Resources.Mixer; + private static readonly Bitmap infoHelp = Resources.InfoHelp; + private static readonly Bitmap donate = Resources.donate; + private static readonly Bitmap helpSmall = Resources.HelpSmall; + private static readonly Bitmap exit = Resources.exit; + private static readonly Icon updateIcon = Resources.UpdateIcon; + private readonly ContextMenuStrip _selectionMenu = new ContextMenuStrip(); private readonly ContextMenuStrip _settingsMenu = new ContextMenuStrip(); private readonly SynchronizationContext _context = SynchronizationContext.Current ?? new SynchronizationContext(); @@ -55,7 +66,7 @@ public TrayIcon() { UpdateIcon(); _tooltipInfoManager = new TooltipInfoManager(NotifyIcon); - _updateMenuItem = new ToolStripMenuItem(TrayIconStrings.noUpdate, Resources.Update, OnUpdateClick) + _updateMenuItem = new ToolStripMenuItem(TrayIconStrings.noUpdate, updateBitmap, OnUpdateClick) { Enabled = false }; @@ -63,7 +74,7 @@ public TrayIcon() PopulateSettingsMenu(); - _selectionMenu.Items.Add(TrayIconStrings.noDevicesSelected, Resources.SettingsSmall, (sender, e) => ShowSettings()); + _selectionMenu.Items.Add(TrayIconStrings.noDevicesSelected, settingsSmall, (sender, e) => ShowSettings()); NotifyIcon.MouseDoubleClick += (sender, args) => { @@ -104,15 +115,29 @@ public void Dispose() { _selectionMenu.Dispose(); _settingsMenu.Dispose(); + if (NotifyIcon.Icon != null) + { + NotifyIcon.Icon.Dispose(); + } NotifyIcon.Dispose(); _updateMenuItem.Dispose(); } + private void ReplaceIcon(Icon newIcon) + { + var oldIcon = NotifyIcon.Icon; + NotifyIcon.Icon = newIcon; + if (oldIcon != null) + { + oldIcon.Dispose(); + } + } + public void UpdateIcon() { if (AppConfigs.Configuration.KeepSystrayIcon) { - NotifyIcon.Icon = Icon.FromHandle(Resources.SoundSwitch16.GetHicon()); + ReplaceIcon(Icon.FromHandle(soundSwitch16.GetHicon())); return; } @@ -120,7 +145,7 @@ public void UpdateIcon() { var defaultDevice = AppModel.Instance.ActiveAudioDeviceLister.GetPlaybackDevices() .First(device => AudioController.IsDefault(device.ID, (DeviceType)device.DataFlow, DeviceRole.Console)); - NotifyIcon.Icon = AudioDeviceIconExtractor.ExtractIconFromAudioDevice(defaultDevice, false); + ReplaceIcon(AudioDeviceIconExtractor.ExtractIconFromAudioDevice(defaultDevice, false)); } catch (InvalidOperationException) { @@ -134,17 +159,17 @@ private void PopulateSettingsMenu() var readmeHtml = Path.Combine(applicationDirectory, "Readme.html"); _settingsMenu.Items.Add( Application.ProductName + ' ' + AssemblyUtils.GetReleaseState() + " (" + Application.ProductVersion + - ")", Resources.SoundSwitch16); + ")", soundSwitch16); _settingsMenu.Items.Add("-"); - _settingsMenu.Items.Add(TrayIconStrings.playbackDevices, Resources.PlaybackDevices, + _settingsMenu.Items.Add(TrayIconStrings.playbackDevices, playbackDevices, (sender, e) => { Process.Start(new ProcessStartInfo("control", "mmsys.cpl sounds")); }); - _settingsMenu.Items.Add(TrayIconStrings.mixer, Resources.Mixer, + _settingsMenu.Items.Add(TrayIconStrings.mixer, mixer, (sender, e) => { Process.Start(new ProcessStartInfo("sndvol.exe")); }); _settingsMenu.Items.Add("-"); _settingsMenu.Items.Add(_updateMenuItem); - _settingsMenu.Items.Add(TrayIconStrings.settings, Resources.SettingsSmall, (sender, e) => ShowSettings()); + _settingsMenu.Items.Add(TrayIconStrings.settings, settingsSmall, (sender, e) => ShowSettings()); _settingsMenu.Items.Add("-"); - _settingsMenu.Items.Add(TrayIconStrings.help, Resources.InfoHelp, (sender, e) => + _settingsMenu.Items.Add(TrayIconStrings.help, infoHelp, (sender, e) => { if (!File.Exists(readmeHtml)) { @@ -153,10 +178,10 @@ private void PopulateSettingsMenu() } Process.Start(readmeHtml); }); - _settingsMenu.Items.Add(TrayIconStrings.donate, Resources.donate, (sender, e) => Process.Start("https://soundswitch.aaflalo.me/?utm_source=application")); - _settingsMenu.Items.Add(TrayIconStrings.about, Resources.HelpSmall, (sender, e) => new About().Show()); + _settingsMenu.Items.Add(TrayIconStrings.donate, donate, (sender, e) => Process.Start("https://soundswitch.aaflalo.me/?utm_source=application")); + _settingsMenu.Items.Add(TrayIconStrings.about, helpSmall, (sender, e) => new About().Show()); _settingsMenu.Items.Add("-"); - _settingsMenu.Items.Add(TrayIconStrings.exit, Resources.exit, (sender, e) => Application.Exit()); + _settingsMenu.Items.Add(TrayIconStrings.exit, exit, (sender, e) => Application.Exit()); } private void OnUpdateClick(object sender, EventArgs eventArgs) @@ -190,7 +215,7 @@ private void SetEventHandlers() { return; } - NotifyIcon.Icon = AudioDeviceIconExtractor.ExtractIconFromAudioDevice(audioChangeEvent.Device, false); + ReplaceIcon(AudioDeviceIconExtractor.ExtractIconFromAudioDevice(audioChangeEvent.Device, false)); }; AppModel.Instance.NewVersionReleased += (sender, @event) => { @@ -224,9 +249,9 @@ private void StartAnimationIconUpdate() var tick = 0; _animationTimer.Tick += (sender, args) => { - NotifyIcon.Icon = tick == 0 - ? Icon.FromHandle(Resources.SoundSwitch16.GetHicon()) - : Resources.UpdateIcon; + ReplaceIcon(tick == 0 + ? Icon.FromHandle(soundSwitch16.GetHicon()) + : (Icon)updateIcon.Clone()); tick = ++tick % 2; }; }