通过


避免内存泄漏

在托管 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;
}

有关实际示例,请参阅任意 示例库 演示页。

如何测试循环泄漏

若要测试应用程序是否正确中断引用计数周期,请将终结器方法添加到包含 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 事件取消订阅所有事件处理程序)来完成。