为什么Wpf的DrawingContext.DrawText如此昂贵?

在Wpf(4.0)中,我的列表框(使用VirtualizingStackPanel)包含500个项目。 每个项目都是自定义类型

class Page : FrameworkElement ... protected override void OnRender(DrawingContext dc) { // Drawing 1000 single characters to different positions //(formattedText is a static member which is only instantiated once and contains the string "A" or "B"...) for (int i = 0; i < 1000; i++) dc.DrawText(formattedText, new Point(....)) // Drawing 1000 ellipses: very fast and low ram usage for (int i = 0; i < 1000; i++) dc.DrawEllipse(Brushes.Black, null, new Point(....),10,10) } 

现在,当来回移动列表框的滚动条时,每个项目的视觉效果至少创建一次,一段时间内ram的使用量达到500 Mb,然后 – 一段时间后 – 回到250 Mb但仍然保持在这个水平。 内存泄漏 ? 我认为VirtualizingStackPanel的优点是不需要/可见的视觉效果被处理掉……

无论如何,只有在使用“DrawText”绘制文本时才会出现这种极端ram用法。 绘制像“DrawEllipse”这样的其他对象并不会消耗太多内存。

绘制许多文本项比使用Drawing.Context的“DrawText”更有效吗?

这是完整的示例(只需创建一个新的Wpf应用程序项目并替换window1代码):(我知道有FlowDocument和FixedDocument,但它们别无选择)Xaml:

            

和Window1.xaml.cs:

 public partial class Window1 : Window { readonly ObservableCollection collection = new ObservableCollection(); public Window1() { InitializeComponent(); for (int i = 0; i < 500; i++) { collection.Add(new Page(){ Width = 500, Height = 800 }); } lb.ItemsSource = collection; } } public class Page : FrameworkElement { static FormattedText formattedText = new FormattedText("A", CultureInfo.GetCultureInfo("en-us"), FlowDirection.LeftToRight, new Typeface(new FontFamily("Arial").ToString()), 12,Brushes.Black); protected override void OnRender(DrawingContext dc) { dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, Width, Height)); double yOff = 0; for (int i = 0; i < 1000; i++) // draw 1000 "A"s { dc.DrawText(formattedText, new Point((i % 80) * 5, yOff )); if (i % 80 == 0) yOff += 10; } } } 

虽然这对你来说并不完全有用,但我对VirtualizingStackPanel的体验并不是它处理不在视图中的对象,而是当应用程序需要更多内存时它允许不在视图中的对象来恢复内存,这应该会导致当你的内存可用时,你的内存使用会膨胀。

dc.DrawText是否可能为每个formattedText对象触发BuildGeometry(),并且您可以将它带到循环外部? 我不知道BuildGeometry有多少工作,但是DrawingContext可能只能接受几何,并且你的样本中不必要地调用了BuildGeometry调用999次。 看一下:

http://msdn.microsoft.com/en-us/library/system.windows.media.formattedtext.aspx

看看是否还有其他优化措施。

您是否可以在循环中输出一些内存配置文件数据和一些时序数据,以了解它是否在减速,或者内存在循环期间以非线性方式增加?

一个很大的贡献是事实(根据我对GlyphRun的经验,我认为在幕后使用),它每个字符使用至少2个字典查找来获得字形索引和宽度。 我在我的项目中使用的一个黑客是我找出了ASCII值和我使用的字体的字母数字字符的字形索引之间的偏移量。 然后我用它来计算每个字符的字形索引,而不是查找字典。 这给了我一个体面的加速。 此外,我可以重复使用字形运行,使用转换变换移动它,而无需重新计算所有内容或字典查找。 系统不能自己做这个黑客攻击,因为它不够通用,无法在每种情况下使用。 我想可以为其他字体做类似的黑客攻击。 我只用Arial测试过,其他字体的索引可能不同。 因为你可以假设字形宽度都是相同的而且每个字符只能查找一个而不是一个字符,所以可以更快地使用单倍间距字体,但我没有测试过这个。

另一个减速贡献者是这个小代码,我还没弄明白如何破解它。 typeface.TryGetGlyphTypeface(out glyphTypeface);

这是我的字母数字Arial hack的代码(与其他未知字符的兼容性)

 public GlyphRun CreateGlyphRun(string text,double size) { Typeface typeface = new Typeface("Arial"); GlyphTypeface glyphTypeface; if (!typeface.TryGetGlyphTypeface(out glyphTypeface)) throw new InvalidOperationException("No glyphtypeface found"); ushort[] glyphIndexes = new ushort[text.Length]; double[] advanceWidths = new double[text.Length]; for (int n = 0; n < text.Length; n++) { ushort glyphIndex = (ushort)(text[n] - 29); glyphIndexes[n] = glyphIndex; advanceWidths[n] = glyphTypeface.AdvanceWidths[glyphIndex] * size; } Point origin = new Point(0, 0); GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size, glyphIndexes, origin, advanceWidths, null, null, null, null, null, null); return glyphRun; } 

我发现user638350的解决方案非常有用; 在我的情况下,我只使用一种字体大小,因此以下优化将时间从每帧0.0060毫秒减少到超过20,000帧的0.0000以下。 大多数减速来自’TryGetGlyphTypeface’和’AdvanceWidths’,所以这两个都是缓存的。 另外,添加计算偏移位置并跟踪总宽度。

  private static Dictionary _glyphWidths = new Dictionary(); private static GlyphTypeface _glyphTypeface; public static GlyphRun CreateGlyphRun(string text, double size, Point position) { if (_glyphTypeface == null) { Typeface typeface = new Typeface("Arial"); if (!typeface.TryGetGlyphTypeface(out _glyphTypeface)) throw new InvalidOperationException("No glyphtypeface found"); } ushort[] glyphIndexes = new ushort[text.Length]; double[] advanceWidths = new double[text.Length]; var totalWidth = 0d; double glyphWidth; for (int n = 0; n < text.Length; n++) { ushort glyphIndex = (ushort)(text[n] - 29); glyphIndexes[n] = glyphIndex; if (!_glyphWidths.TryGetValue(glyphIndex, out glyphWidth)) { glyphWidth = _glyphTypeface.AdvanceWidths[glyphIndex] * size; _glyphWidths.Add(glyphIndex, glyphWidth); } advanceWidths[n] = glyphWidth; totalWidth += glyphWidth; } var offsetPosition = new Point(position.X - (totalWidth / 2), position.Y - 10 - size); GlyphRun glyphRun = new GlyphRun(_glyphTypeface, 0, false, size, glyphIndexes, offsetPosition, advanceWidths, null, null, null, null, null, null); return glyphRun; }