【问题标题】:Windows Phone 8.1 WinRT memory leak with ObservableCollectionWindows Phone 8.1 WinRT 内存泄漏与 ObservableCollection
【发布时间】:2014-11-29 07:46:48
【问题描述】:

我正在处理大量显示在 MapControl 上的对象 (POI)。我正在使用 MVVM Light 帮助自己遵守 MVVM 方法的规则。

由于我有义务在地图上显示每个对象,因此我必须使用 MapItemsControl 集合,而不是 MapElements 集合。 此集合绑定到对应 ViewModel 中的 ObservableCollection<PushpinViewModel> 对象 (Pushpins)。当我想刷新Pushpins 时,一切都按预期工作。问题是内存泄漏。但首先,一些代码可以可视化问题:

XAML:

<maps:MapControl x:Name="Map"
                 x:Uid="MapControl">
  <maps:MapItemsControl ItemsSource="{Binding Pushpins}">
    <maps:MapItemsControl.ItemTemplate>
      <DataTemplate>
        <Image Source="{Binding Image}"/>
      </DataTemplate>
    </maps:MapItemsControl.ItemTemplate>
  </maps:MapItemsControl>

主视图模型:

public class MainViewModel : ViewModelBase
{
    public RelayCommand AddCommand { get; set; }
    public RelayCommand ClearCommand { get; set; }
    public RelayCommand CollectCommand { get; set; }

    public ObservableCollection<PushpinViewModel> Pushpins { get; set; }

    /* Ctor, initialization of Pushpins and stuff like that */

    private void Collect()
    {
        GC.Collect(2);
        GC.WaitForPendingFinalizers();
        GC.Collect(2);
        PrintCurrentMemory();
    }

    private void Clear()
    {
        Pushpins.Clear();
        PrintCurrentMemory();
    }

    private void Add()
    {
        for (int i = 0; i < 1000; i++)
        {
            Pushpins.Add(new PushpinViewModel());
        }
        PrintCurrentMemory();
    }

    private void PrintCurrentMemory()
    {
        Logger.Log(String.Format("Total Memory: {0}", GC.GetTotalMemory(true) / 1024.0));
    }
}

PushpinViewModel:

public class PushpinViewModel: ViewModelBase
{
    public string Image { get { return "/Assets/SomeImage.png"; } }

    ~PushpinViewModel()
    {
        Logger.Log("This finalizer never gets called!");
    }
}

现在,考虑以下场景。我添加到 Pushpins 集合 1000 PushpinViewModel 元素。它们被渲染,内存被分配,一切都很好。现在我想清除集合,并添加另一个(在实际场景中不同)1000 个元素。所以,我调用Clear() 方法。但是..什么都没有发生! Pushpins 被清除,但 PushpinViewModel 的终结器未被调用!然后我再次添加 1000 个元素,我的内存使用量翻了一番。 你可以猜到接下来会发生什么。当我重复这个 Clear() - Add() 过程 3-5 次时,我的应用程序崩溃了。

那么,问题出在哪里?显然,ObservableCollection 在对其执行Clear() 之后持有对PushpinViewModel 对象的引用,因此它们不能被垃圾回收。当然,强制 GC 执行垃圾回收无济于事(有时甚至会使情况变得更糟)。

现在困扰了我 2 天,我尝试了许多不同的方案来尝试克服这个问题,但老实说,没有任何帮助。 只有一件事一文不值——我不记得确切的场景,但是当我分配了Pushpins = null,然后做了更多的事情时,VehiceViewModel 被破坏了。但这对我不起作用,因为我还记得在 Clear() 之后在地图上显示这些图钉时遇到了问题。

您有什么想法会导致这种内存泄漏吗?如何强制OC的成员销毁?也许OC 有某种替代方案? 提前感谢您的帮助!

编辑:

我使用 XAML Map Control - https://xamlmapcontrol.codeplex.com/ 进行了一些测试,结果令人惊讶。添加了 >1000 个元素的整体地图性能比原生 MapControl 差,但是,如果我调用 Add() x1000,然后是 Clear(),然后是 Add() x1000,PushpinViewModel 的终结器正在获得叫!内存被释放,应用程序不会崩溃。所以微软的MapControl肯定有问题...

【问题讨论】:

  • 问题可能是您加载到表单的位图正在缓存到内存中。收集 GC 不会将其从缓存中删除。请参阅theserelated 问题。
  • 感谢您的回答。我正在调查这种可能性,并同意这确实可能是一个原因。我检查了您的链接,遗憾的是,发布的答案大多适用于 WPF 应用程序(我在 WinRT - 通用应用程序中)。我确实设法从流中加载了一张图片(作为一种异步方法,但它有效) - 不幸的是它没有帮助。内存消耗更大:(
  • 我无法谈论您的具体问题,但 MapItemsControl 也有一些问题。我的设置与您所做的几乎完全相同——使用 MVVM 绑定到 ObservableCollection。如果我从地图导航到另一个视图,然后返回地图,并来回重复 3-5 次,我(通常)会收到“访问冲突”错误,我将其缩小到 MapItemsControl。我将其替换为获取项目源并为我在地图上绘制图钉的行为,如果您希望我发布源,请告诉我。
  • 如果这不是问题@paul.abbott.wa.us,我将不胜感激。我联系了一些与微软相关的人,他们已经确认这是一个错误:social.msdn.microsoft.com/Forums/windowsapps/en-US/…,甚至提出了一个解决方法,但是,它不起作用。提前致谢!

标签: c# windows-phone-8 memory-leaks windows-phone-8.1 observablecollection


【解决方案1】:

好的,这是我模仿MapItemsControl 所做的行为。请注意,这是未经测试的——它适用于我的应用程序,但实际上还没有在其他任何地方尝试过。而且我从未测试过RemoveItems 函数,因为我的应用程序只是将项目添加到ObservableCollection 并清除它们;它从不增量删除项目。

另请注意,它使用绑定到的项目的哈希码标记 XAML 图钉;如果集合发生变化,这就是它识别从地图中删除哪些图钉的方式。这可能不适用于您的情况,但似乎很有效。

用法:

注意:NumberedCircle 是一个用户控件,它只是一个红色圆圈,其中显示一个数字;替换为您想用作图钉的任何 XAML 控件。 Destinations 是我的 ObservableCollection 对象,这些对象具有 Number 属性(显示在图钉内)和 Point 属性(图钉位置)。

<map:MapControl>
   <i:Interaction.Behaviors>
      <behaviors:PushpinCollectionBehavior ItemsSource="{Binding Path=Destinations}">
         <behaviors:PushpinCollectionBehavior.ItemTemplate>
            <DataTemplate>
               <controls:NumberedCircle Number="{Binding Path=Number}" map:MapControl.Location="{Binding Path=Point}" />
            </DataTemplate>
         </behaviors:PushpinCollectionBehavior.ItemTemplate>
      </behaviors:PushpinCollectionBehavior>
   </i:Interaction.Behaviors>
</map:MapControl>

代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Xaml.Interactivity;

using Windows.Devices.Geolocation;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls.Maps;

namespace Foo.Behaviors
{
    /// <summary>
    /// Behavior to draw pushpins on a map.  This effectively replaces MapItemsControl, which is flaky as hell.
    /// </summary>
    public class PushpinCollectionBehavior : DependencyObject, IBehavior
    {
        #region IBehavior

        public DependencyObject AssociatedObject { get; private set; }

        public void Attach(Windows.UI.Xaml.DependencyObject associatedObject)
        {
            var mapControl = associatedObject as MapControl;

            if (mapControl == null)
                throw new ArgumentException("PushpinCollectionBehavior can be attached only to MapControl");

            AssociatedObject = associatedObject;

            mapControl.Unloaded += MapControlUnloaded;
        }

        public void Detach()
        {
            var mapControl = AssociatedObject as MapControl;

            if (mapControl != null)
                mapControl.Unloaded -= MapControlUnloaded;
        }

        #endregion

        #region Dependency Properties

        /// <summary>
        /// The dependency property of the item that contains the pushpin locations.
        /// </summary>
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(object), typeof(PushpinCollectionBehavior), new PropertyMetadata(null, OnItemsSourcePropertyChanged));

        /// <summary>
        /// The item that contains the pushpin locations.
        /// </summary>
        public object ItemsSource
        {
            get { return GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        /// <summary>
        /// Adds, moves, or removes the pushpin when the item source changes.
        /// </summary>
        private static void OnItemsSourcePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var behavior = dependencyObject as PushpinCollectionBehavior;
            var mapControl = behavior.AssociatedObject as MapControl;

            // add the items

            if (behavior.ItemsSource is IList)
                behavior.AddItems(behavior.ItemsSource as IList);
            else
                throw new Exception("PushpinCollectionBehavior needs an IList as the items source.");

            // subscribe to changes in the collection

            if (behavior.ItemsSource is INotifyCollectionChanged)
            {
                var items = behavior.ItemsSource as INotifyCollectionChanged;
                items.CollectionChanged += behavior.CollectionChanged;
            }
        }

        // <summary>
        /// The dependency property of the pushpin template.
        /// </summary>
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(PushpinCollectionBehavior), new PropertyMetadata(null));

        /// <summary>
        /// The pushpin template.
        /// </summary>
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        #endregion

        #region Events

        /// <summary>
        /// Adds or removes the items on the map.
        /// </summary>
        private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    AddItems(e.NewItems);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    RemoveItems(e.OldItems);
                    break;

                case NotifyCollectionChangedAction.Reset:
                    ClearItems();
                    break;
            }
        }

        /// <summary>
        /// Removes the CollectionChanged event handler from the ItemsSource when the map is unloaded.
        /// </summary>
        void MapControlUnloaded(object sender, RoutedEventArgs e)
        {
            var items = ItemsSource as INotifyCollectionChanged;

            if (items != null)
                items.CollectionChanged -= CollectionChanged;
        }

        #endregion

        #region Private Functions

        /// <summary>
        /// Adds items to the map.
        /// </summary> 
        private void AddItems(IList items)
        {
            var mapControl = AssociatedObject as MapControl;

            foreach (var item in items)
            {
                var templateInstance = ItemTemplate.LoadContent() as FrameworkElement;

                var hashCode = item.GetHashCode();

                templateInstance.Tag = hashCode;
                templateInstance.DataContext = item;

                mapControl.Children.Add(templateInstance);

                Tags.Add(hashCode);
            }
        }

        /// <summary>
        /// Removes items from the map.
        /// </summary>
        private void RemoveItems(IList items)
        {
            var mapControl = AssociatedObject as MapControl;

            foreach (var item in items)
            {
                var hashCode = item.GetHashCode();

                foreach (var child in mapControl.Children.Where(c => c is FrameworkElement))
                {
                    var frameworkElement = child as FrameworkElement;

                    if (hashCode.Equals(frameworkElement.Tag))
                    {
                        mapControl.Children.Remove(frameworkElement);
                        continue;
                    }
                }

                Tags.Remove(hashCode);
            }
        }

        /// <summary>
        /// Clears items from the map.
        /// </summary>
        private void ClearItems()
        {
            var mapControl = AssociatedObject as MapControl;

            foreach (var tag in Tags)
            {
                foreach (var child in mapControl.Children.Where(c => c is FrameworkElement))
                {
                    var frameworkElement = child as FrameworkElement;

                    if (tag.Equals(frameworkElement.Tag))
                    {
                        mapControl.Children.Remove(frameworkElement);
                        continue;
                    }
                }
            }

            Tags.Clear();
        }

        #endregion

        #region Private Properties

        /// <summary>
        /// The object tags of the items this behavior has placed on the map.
        /// </summary>
        private List<int> Tags
        {
            get
            {
                if (_tags == null)
                    _tags = new List<int>();

                return _tags;
            }
        }
        private List<int> _tags;

        #endregion
    }
}

【讨论】:

  • 感谢您的解决方案。
  • 谢谢,重复 2-3 次后出现“访问冲突”错误,此解决方案有帮助
  • 非常感谢,给我带来了麻烦。尽管我在 MapItemsControl DataTemplate 中的命令有问题,但在我切换到您的代码后该命令停止工作。知道为什么吗?
猜你喜欢
  • 1970-01-01
  • 2015-03-02
  • 2014-05-03
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 2014-04-09
  • 2015-03-07
  • 1970-01-01
相关资源
最近更新 更多