原文地址:devblogs.microsoft.com/oldnewthing…
原文作者:devblogs.microsoft.com/oldnewthing…
发布时间:2019年5月22日
在C#和其他GC语言(如JavaScript)中,委托人(最典型的是作为事件处理程序使用)在其闭包中捕获对对象的强引用。这意味着你可以创建超出GC收集能力的引用循环。
using Windows.Devices.Enumeration;
class Circular
{
DeviceWatcher watcher;
public Circular()
{
watcher = DeviceInformation.CreateWatcher();
watcher.Added += OnDeviceAdded;
}
void OnDeviceAdded(DeviceWatcher sender, DeviceInformation info)
{
...
}
}
Circular类包含一个对DeviceWatcher的引用,而DeviceWatcher又包含一个回到Circular的引用(通过委托人)。这个循环引用永远不会被收集,因为其中一个参与者是一个DeviceWatcher,这超出了垃圾收集器的知识范围。
从垃圾收集器的角度来看,系统是这样的。
? → delegate → Circular → DeviceWatcher
垃圾收集器完全知道绿色框 "delegate "和 "Circular",因为它们是CLR对象。垃圾收集器不知道虚线框,因为它们是CLR范围之外的外部对象。
垃圾收集器知道的是,有一个来自某个未知外部来源的对delegate的未完成引用,它知道该delegate有一个对Circular对象的引用,它知道Circular对象有一个对某个外部对象的引用,这个外部对象的名字叫DeviceWatcher.但是它不知道DeviceWatcher对象可能有什么引用,因为DeviceWatcher不是一个CLR对象。它不知道DeviceWatcher其实一直都是问号¹。
为了避免内存泄漏,你必须打破这种循环引用。理想情况下,有一些自然的地方可以进行这种清理。例如,如果你是一个Page,你可以在你的OnNavigatedFrom方法中进行清理,或者响应Unloaded事件。不太理想的情况下,你可以添加一个清理方法,可能编入IDisposable模式。
有一种特殊情况。XAML框架与CLR有一个秘密协议,XAML可以共享它所拥有的引用的更多详细信息。这些信息使得CLR有可能打破某些类别的循环引用,这些循环引用在XAML代码中是经常出现的。例如,CLR可以在XAML框架提供的信息的帮助下检测到这种循环引用。
<!-- XAML -->
<Page x:Name="AwesomePage" ...>
...
<Button x:Name="SomeNamedButton" ... >
...
</Page>
// C#代码背后
partial class AwesomePage : Page
{
AwesomePage()
{
InitializeComponent();
SomeNamedButton.Click += SomeNamedButton_Click;
}
void SomeNamedButton_Click(object sender, RoutedEventArgs e)
{
...
}
}
这里有一个AwesomePage和SomeNamedButton之间的循环引用,但XAML框架提供的额外信息为CLR提供了足够的信息来识别循环,并在它成为垃圾时收集它。
¹"一直以来都是问号",听起来就像一部M.Night Shyamalan的烂片中的破坏者。