[自带避雷针]DropShadowEffect导致内存暴涨
周银辉
从学习WPF开始, 就知道"位图效果"不是什么省油的灯, 但我只知道它会占用很多cpu时间, 而显得效率低下, 但完全不料的是在某种特殊的硬件环境下其会导致内存暴涨.
事情是这样的, 我写了一个小程序, 看起来还不错, 但并不变态的日本朋友在变态的硬件环境下测试我的程序说"操作一段时间后程序会崩溃". 我远程VNC过去,发现程序每进行一个小操作(甚至是点击一下鼠标)内存会增长好几M, 照这个速度, 一会准崩溃, NND
但, 郁闷的事情是, 我无法重现这个Bug, 在开发团队和测试团队的机器上都没法重现(但在日本那边,这个bug是百分百重现)...内存泄露还没法重现, 好搞笑...更搞笑的事情在后面
最后我发现日本那边采用了如下的显示器配置: 其主机上连接了3个显示器, 第一个为普通屏, 第二和第三个屏是医学专用的高清屏(传说分辨率超高,当然价格也超贵), 当连接上这两个屏幕的时候, 我的程序就死翘翘了. 首先排除单纯的多屏问题,因为我的开发机就是双屏的, 然后我又排除了高清屏的问题, 因为禁用掉普通屏,而使用高清屏的时候,一切OK.
经过一系列的排查, 得到的诡异结论是: 当普通屏和高清屏同时启用, 并且我的程序窗口显示在普通屏上的时候, 内存暴涨, 最后死翘翘, 其他情况一切OK...
为什么要在这么诡异的环境下才能重现这个问题呢, 完全无解.
好吧, 我暂且承认是自己代码烂, 内存泄露了吧, 还好我有 .net memory profile & ANTS Memory Profiler
经过一番折腾之后, 这两个被我奉为"神器"的内存工具并没有给我任何答案: 从变化曲线上看内存是增长了许多(动不动就上百兆), 但内存快照中各个类型对象的内存占有率以及变化率都非常地正常, 晕, 丢失的内存哪里去了~~
我开始怀疑是P/Invoke之类的丢失了内存, 因为程序中有大量的win32 API平台调用, 然后,我注释掉了这些代码... 很不幸, 答案是NO.
没辙, 再来一招: 功能裁剪,这是经常使用的一招, 这让我们比较容易地缩小代码范围. 由于UI层和后台代码的耦合度非常低, 直接注释掉UI层上的XAML代码, 功能就能被很好地裁掉, 而不用更改逻辑代码, 所以功能裁剪显得比较容易. 我的程序都快被裁成空壳了, 问题依然存在...
还有一个很搞怪的问题是, 比如我们定位到一个比较复杂的函数A会导致问题, 然后从A开始跟踪, 到B -> C -> D 绕了很长一圈后, 会到达一个及其简单的代码上比如 : this.width = myWidth; 注释到这个简单的代码就没问题, 否则导致问题, 诶, 难道是宽度的改变会引发什么事件,或者重写了控件重绘等等 而导致的问题? 这一点点希望很快就被抹杀了, 控件已经被我们抽取到足够的简单, 几乎没有什么代码.... 耍我么...
一般, 在我绝望的时候, 我会使用我的大绝招: 我猜
我一直比较相信写程序还是要靠灵感的(当然, 建立在你基础知识比较扎实的基础之上的). 那么我先猜是那个控件导致的问题呢, 我当然不会怀疑自己的代码咯, 我怀疑微软的代码, 这不是自大, 而是经验值, 我搞了不少无厘头的问题, 最后根源都在微软的代码上, 如果你有机会玩玩微软的RichTextBox控件再加上一个日文的Atok输入法, 你就会相信我说的话的(会搞死人的). 所以我猜ViewBox, 程序中的一个绘图面板放在ViewBox中(为实现缩放功能), 那好, 把绘图板从面板中剪切出来吧.
哈哈, OK啦, 把绘图板从面板中剪切出来后真的没问题也, 难道我运气这么好, 一下就猜中了...
为了验证, 我重新做了一个很简单的关于viewbox的DEMO, 放在日本那边的机器上, 没问题, 我晕
然后我注意到源程序中, viewbox放在一个ScrollViewer中, 并且这个ScrollViewer有自己的模板, もしかしてなの ...
把画图板重新放回到ViewBox中, 在scrollViewer中动刀. 我发现, 我惊奇地发现:
在该ScrollViewer控件模板中, 居然有一个DropShadowEffect, 天杀的. 另外, 这个effect在我们的程序里面看不到阴影效果, 我们的程序也不需要这个效果, 所以一直没察觉.
删掉! 整个世界都安静了~
为了证明这个的确会导致问题, 所以我做了一个简单的DEMO,
xmlns =""
xmlns:x =""
Title ="Window1" Height ="300" Width ="300" >
< Canvas x:Name ="canvas" >
< Canvas.Effect >
< DropShadowEffect />
</ Canvas.Effect >
< Button Width ="100" Height ="30" Content ="Click me" Click ="Button_Click" />
< Rectangle Fill ="White" Stroke ="Black" Width ="53" Height ="43" Canvas.Left ="37" Canvas.Top ="71" />
< Rectangle Fill ="#FFAF8C8C" Stroke ="Black" Width ="54" Height ="72" Canvas.Left ="158" Canvas.Top ="42" />
< Rectangle Fill ="White" Stroke ="Black" Width ="54" Height ="47" Canvas.Left ="117" Canvas.Top ="130" />
< Rectangle Fill ="#FF21CB63" Stroke ="Black" Width ="44" Height ="65" Canvas.Left ="212" Canvas.Top ="130" />
< Rectangle Fill ="White" Stroke ="Black" Width ="42" Height ="49" Canvas.Left ="58" Canvas.Top ="177" />
< Rectangle Fill ="White" Stroke ="Black" Width ="53" Height ="45" Canvas.Left ="171" Canvas.Top ="195" />
< Rectangle Fill ="#FFDE13EE" Stroke ="Black" Width ="70" Height ="69" Canvas.Left ="224" Canvas.Top ="30" />
< Rectangle Fill ="White" Stroke ="Black" Width ="21" Height ="43" Canvas.Left ="100" Canvas.Top ="42" />
< Rectangle Fill ="White" Stroke ="Black" Width ="21" Height ="22" Canvas.Left ="171" Canvas.Top ="8" />
< Rectangle Fill ="White" Stroke ="Black" Width ="37" Height ="43" Canvas.Left ="117" Canvas.Top ="71" />
< Rectangle Fill ="White" Stroke ="Black" Width ="41" Height ="33" Canvas.Left ="49" Canvas.Top ="130" />
< Rectangle Fill ="#FFFF2B2B" Stroke ="Black" Width ="54" Height ="31" Canvas.Left ="100" Canvas.Top ="195" />
</ Canvas >
</ Window >
然后点击Button的时候,做如下操作:
{
for ( int i = 0 ; i < canvas.Children.Count; i ++ )
{
var c = (FrameworkElement) canvas.Children[i];
c.Width += 1 ;
c.Height += 1 ;
}
}
在我的开发机上(在你的机器上可能也是), 连续点击button, 内存会有所增长, 但增长比较慢, 并且过一小会就释放带了, 所以内存基本维持到十多兆(图中18756K):
放在日本那个特殊的环境下,情况就完全不一样了(下图99340K, 并且好玩的是, 下图有黄色桌面背景的是第一个显示器, 蓝色背景的是第二个显示器, 把window1拖到第二个显示器上内存表现会很正常哦) :
恩, 人生在于折腾
ps: 这台机器插了两张显卡: nVIDIA Quadro FX370 和 VREngine SMD5