在 WPF 中,使用 Stroke 类时,可能会出现内存泄露,原因是 DrawingAttributes 的事件被监听没有释放。本文将从源代码的角度告诉大家这个内存泄露问题和如何解决

在满足如下条件的时候,将会让 Stroke 类出现内存泄露

  1. 存在一个 Stroke 被强引用,将这个 Stroke 记为 A 对象
  2. 取 A 对象的 DrawingAttributes 属性,创建出另一个新的 Stroke 对象,将这个对象记为 B 对象

此时将会发现 B 对象不会被释放,如 demo 所示,点击按钮可以看到内存不会释放

实现上面条件的代码很简单,请看代码

      var stroke = new Stroke(new StylusPointCollection(stylusPointList), StrokeVisual.Stroke.DrawingAttributes);

上面代码的 StrokeVisual 是一个窗口的属性,也就是说 StrokeVisual.Stroke 是存在强引用。此时创建出来的 stroke 对象将不会被回收

按钮点击的代码如下

        public StrokeVisual StrokeVisual { get; set; }

        private void Button_OnClick(object sender, RoutedEventArgs e)
        {
            for (int i = 0; i < 100; i++)
            {
                var stylusPointList = new List<StylusPoint>();
                for (int j = 0; j < 1000; j++)
                {
                    stylusPointList.Add(new StylusPoint(i, i));
                }

                var stroke = new Stroke(new StylusPointCollection(stylusPointList), StrokeVisual.Stroke.DrawingAttributes);
            }
        }

关于 StrokeVisual 的定义,请看 WPF 最简逻辑实现多指顺滑的笔迹书写

那为什么使用一个被强引用的 Stroke 的 DrawingAttributes 去创建另一个 Stroke 对象,会让另一个 Stroke 不会被释放

通过 WPF 的源代码可以看到,在 Stroke 里面是将 DrawingAttributes 作为属性存放,因此 Stroke 强引用 DrawingAttributes 对象。如果使用被强引用的 Stroke 的 DrawingAttributes 去创建另一个 Stroke 对象,因为在 Stroke 对象的构造函数里面有如下代码

internal Stroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes, ExtendedPropertyCollection extendedProperties)
{
    _drawingAttributes = drawingAttributes;
    _drawingAttributes.AttributeChanged += new PropertyDataChangedEventHandler(DrawingAttributes_Changed);
}

也就说在 Stroke 里面添加了 _drawingAttributes 的事件,也就说 Stroke 也被 DrawingAttributes 强引用。而如上面描述,这里的 DrawingAttributes 被另一个 Stroke 强引用。因此如果多个 Stroke 使用相同的一个 DrawingAttributes 对象,有一个 Stroke 被强引用,那么其他的所有的 Stroke 对象也不会被释放

本文的测试代码放在 githubgitee 欢迎下来进行调试

那如何解决此问题?在 DrawingAttributes 对象里面提供了 Clone 方法,在使用某个 Stroke 的 DrawingAttributes 对象创建一个新的 Stroke 的时候,如果要解决本文提到的的坑,可以调用 DrawingAttributes 的 Clone 方法创建一个新的 DrawingAttributes 对象,此时这个新的 DrawingAttributes 对象将不会被原有的 Stroke 强引用,因此也就不会让新创建的 Stroke 因为被 DrawingAttributes 强引用的原因内存泄露

  var stroke = new Stroke(new StylusPointCollection(stylusPointList), StrokeVisual.Stroke.DrawingAttributes.Clone());

如上面代码,通过在新建 Stroke 的时候,传入的 DrawingAttributes 是调用 Clone 方法创建的

这个问题报告给了 WPF 官方,请看 WPF Stroke may memory leak

当然了,这个也不算是坑,通过 VisualStudio 进行内存调试,是可以找到这个坑的

其实通过阅读源代码,如果给多个 Stroke 对象使用相同的一个 StylusPointCollection 对象,那么也有相同的坑。此时只要有一个 Stroke 被强引用了,那么所有的 Stroke 都不会释放

internal Stroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes, ExtendedPropertyCollection extendedProperties)
{
    _drawingAttributes = drawingAttributes;
    _drawingAttributes.AttributeChanged += new PropertyDataChangedEventHandler(DrawingAttributes_Changed);

    _stylusPoints = stylusPoints;
    _stylusPoints.Changed += new EventHandler(StylusPoints_Changed);
    _stylusPoints.CountGoingToZero += new CancelEventHandler(StylusPoints_CountGoingToZero);
}

当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建

更多笔迹相关请看


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-%E8%AF%BB-WPF-%E6%BA%90%E4%BB%A3%E7%A0%81%E7%AC%94%E8%AE%B0-Stroke-%E7%B1%BB%E5%8F%AF%E8%83%BD%E5%AD%98%E5%9C%A8%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利