避免内存泄漏

在托管式 XAML 应用程序中使用 Win2D 控件时,必须注意避免使用引用计数循环,以防止垃圾回收器收回这些控件。

在以下情况下,你将遇到问题…

如果满足所有这些条件,引用计数循环将使 Win2D 控件永远不会被垃圾回收器收回。 每当应用程序移到另一个页面时,都会分配新的 Win2D 资源,但永远不会释放旧资源,因此内存会泄漏。 为了避免这种情况,必须添加代码,以便显式中断此循环。

如何修复此问题

要中断引用计数循环,并让垃圾回收器收回页面:

  • 将包含 Win2D 控件的 XAML 页面的 Unloaded 事件挂钩
  • Unloaded 处理程序中,在 Win2D 控件上调用 RemoveFromVisualTree
  • Unloaded 处理程序中,(通过设置为 null)释放对 Win2D 控件的任何显式引用

示例代码:

void page_Unloaded(object sender, RoutedEventArgs e)
{
    this.canvas.RemoveFromVisualTree();
    this.canvas = null;
}

有关有效示例,请参阅任何示例库演示页面。

如何测试循环泄漏

要测试应用程序是否正确中断 refcount 循环,请将一种终结器方法添加到包含 Win2D 控件的任何 XAML 页面:

~MyPage()
{
    System.Diagnostics.Debug.WriteLine("~" + GetType().Name);
}

App 构造函数中,设置一个计时器,以确保定期执行垃圾回收:

var gcTimer = new DispatcherTimer();
gcTimer.Tick += (sender, e) => { GC.Collect(); };
gcTimer.Interval = TimeSpan.FromSeconds(1);
gcTimer.Start();

导航到此页面,然后离开此页面并前往另一个页面。 如果所有循环都已中断,你将在一两秒钟内在 Visual Studio 输出窗格中看到 Debug.WriteLine 输出。

请注意,调用 GC.Collect 具有破坏性且会降低性能,因此应在完成泄漏测试后立即删除此测试代码!

底层详细信息

当对象 A 引用 B,同时 B 也引用 A,或者当 A 引用 B、B 引用 C、C 引用 A 时,就会发生循环。

当订阅 XAML 控件的事件时,这种循环几乎是不可避免的:

  • XAML 页面包含对它所包含的所有控件的引用
  • 控件保留对已订阅其事件的处理程序委托的引用
  • 每个委托都保留对其目标实例的引用
  • 事件处理程序通常是 XAML 页面类的实例方法,因此它们的目标实例引用会回指向 XAML 页面,并形成一个循环

如果在 .NET 中实现了所涉及的全部对象,则无需担心此类循环,因为 .NET 被垃圾回收器收回,而且垃圾回收算法能够识别和回收对象组,即使它们在一个循环中关联。

与 .NET 不同,C++ 通过引用计数来管理内存,此计数无法检测和回收对象循环。 尽管存在这一局限性,但使用 Win2D 的 C++ 应用程序没有任何问题,因为 C++ 事件处理程序默认保留对其目标实例的弱引用而不是强引用。 因此,页面会引用控件,控件会引用事件处理程序委托,但此委托并不反向引用页面,因此不会形成循环。

当 .NET 应用程序使用 C++ WinRT 组件(例如 Win2D)时,会出现问题:

  • XAML 页面是应用程序的一部分,因此会使用垃圾回收功能
  • Win2D 控件在 C++ 中实现,因此会使用引用计数功能
  • 事件处理程序委托是应用程序的一部分,因此会使用垃圾回收功能并保留对其目标实例的强引用

这样就存在一个循环,但参与此循环的 Win2D 对象并不使用 .NET 垃圾回收功能。 这意味着垃圾回收器看不到整个链,因此无法检测或回收对象。 当发生这种情况时,应用程序必须显式中断循环以帮助解决问题。 为此,可以从页面中释放对控件的所有引用(如上所述),或者从控件中释放对可能回指向页面的事件处理程序委托的所有引用(利用页面 Unloaded 事件取消订阅所有事件处理程序)。