【问题标题】:Custom virtualizing panel freezes UI thread when user scrolls自定义虚拟化面板在用户滚动时冻结 UI 线程
【发布时间】:2020-11-12 03:11:36
【问题描述】:

我发现了一个 VirtualizingWrapPanel(VWP) 项目here,我认为它可以帮助我优化我的 ListView 滚动性能。 listView 必须有四列和多行才能显示源项目。所以我尝试使用这个 VWP,但是滚动(我把它做成 DoubleAnimation)平滑度仍然很差,帧率下降到大约 30 fps 而且非常明显。我从上面的链接中复制了源代码来尝试调试它并了解性能如此糟糕的原因。

几个小时后发现VWP的ArrangeOverride方法和它的“继承父”的MeasureOverride方法" 可以执行大约 20-30ms (1000ms / 30ms = ~30fps) 就是这样,我找到了它这么慢的原因......但是为什么呢?

MeasureOverride 方法调用两个可能很重的方法:RealizeItems 和 VirtualizeItems

protected virtual void RealizeItems()
    {
        var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex);

        int childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1;

        using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true))
        {
            for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++)
            {
                UIElement child = (UIElement)ItemContainerGenerator.GenerateNext(out bool isNewlyRealized);
                if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child))
                {
                    if (childIndex >= InternalChildren.Count)
                    {
                        AddInternalChild(child);
                    }
                    else
                    {
                        InsertInternalChild(childIndex, child);
                    }
                    ItemContainerGenerator.PrepareItemContainer(child);

                    child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                }

                if (child is IHierarchicalVirtualizationAndScrollInfo groupItem)
                {
                    groupItem.Constraints = new HierarchicalVirtualizationConstraints(
                        new VirtualizationCacheLength(0),
                        VirtualizationCacheLengthUnit.Item,
                        new Rect(0, 0, ViewportWidth, ViewportHeight));
                    child.Measure(new Size(ViewportWidth, ViewportHeight));
                }
            }
        }
    }

    protected virtual void VirtualizeItems()
    {
        for (int childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--)
        {
            var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex);

            int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition);

            if (!ItemRange.Contains(itemIndex))
            {

                if (VirtualizationMode == VirtualizationMode.Recycling)
                {
                    ItemContainerGenerator.Recycle(generatorPosition, 1);
                }
                else
                {
                    ItemContainerGenerator.Remove(generatorPosition, 1);
                }
                RemoveInternalChildRange(childIndex, 1);
            }
        }
    }

所以我知道开发人员可能会在代码中犯错,我也知道可能没有一个可以优化的东西,但我真的不明白为什么它这么慢..循环迭代大约 40- 50 个 UIElement 对象,每个对象都有两个子对象(TextBlock 和 ImageSource),这并不是一个庞大的对象数量..

我想在这些循环中使用并行任务,但 UIElement 和其他不是线程安全并且只能在UI 线程...或可以访问?

如何提高这些方法的性能?可以实现高效的多线程吗? 谢谢!

【问题讨论】:

  • 您不能并行化容器生成。容器必须在 UI 线程上创建。
  • 是否有任何替代方案可以提高性能?
  • 一般来说,您发布的代码看起来没问题。您可以通过检查例如检查是否真的有必要重新完成所有容器实现。如果传递给MeasureOverride 的约束大小已更改或超过某个阈值。基本上尝试优化或添加有助于避免冗余迭代或通过使用突破标准缩短它们的约束。在用户方面,尽量保持容器的可视化树尽可能小。删除所有未使用的元素,如边框和布局属性。检查缓存策略,现金是否太大或太小?
  • 也许分析负责在偏移更改上生成新项目的代码部分。从SetHorizontalOffsetSetVerticalOffset 开始。也许这里的代码生成了太多的项目,你找到了优化算法的方法。
  • 很奇怪..我通过仅使用更改的项目进行操作来减少循环迭代次数,但几乎没有任何改变:(现在循环影响了几个(1-6)个对象,但是 child.Measure( ), child.Arrange() 和其他方法总共花费了太多时间..

标签: c# wpf multithreading performance ui-thread


【解决方案1】:

我找到了这个方法,每次垂直滚动偏移发生变化时都会调用InvalidateMeasure()。由于 DoubleAnimation 我用来动画垂直滚动偏移,这是真正经常调用的代码,但如果没有改变测量和排列方法的任何东西,总共执行大约 1-2 毫秒,否则 - 会慢很多。

public void SetVerticalOffset(double offset)
{
    if (offset < 0 || Viewport.Height >= Extent.Height)
    {
        offset = 0;
    }
    else if (offset + Viewport.Height >= Extent.Height)
    {
        offset = Extent.Height - Viewport.Height;
    }
    Offset = new Point(Offset.X, offset);
    ScrollOwner?.InvalidateScrollInfo();
    InvalidateMeasure();
}

更新:我打开 Profiler 后发现,布局操作占用了大量时间(7-10 毫秒或更多毫秒)。

更新 2:下面的代码偏移(排列)所有可见项目

protected override Size ArrangeOverride(Size finalSize)
    {
        double offsetX = GetX(Offset);
        double offsetY = GetY(Offset);

        /* When the items owner is a group item offset is handled by the parent panel. */
        if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
        {
            offsetY = 0;
        }

        Size childSize = CalculateChildArrangeSize(finalSize);

        CalculateSpacing(finalSize, out double innerSpacing, out double outerSpacing);

        for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
        {
            UIElement child = InternalChildren[childIndex];

            int itemIndex = GetItemIndexFromChildIndex(childIndex);

            int columnIndex = itemIndex % itemsPerRowCount;
            int rowIndex = itemIndex / itemsPerRowCount;

            double x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing);
            double y = rowIndex * GetHeight(childSize);

            if (GetHeight(finalSize) == 0.0)
            {
                /* When the parent panel is grouping and a cached group item is not 
                 * in the viewport it has no valid arrangement. That means that the 
                 * height/width is 0. Therefore the items should not be visible so 
                 * that they are not falsely displayed. */
                child.Arrange(new Rect(0, 0, 0, 0));
            }
            else
            {
                child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height));
            }
        }
        return finalSize;
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-21
    • 1970-01-01
    • 1970-01-01
    • 2011-03-31
    • 2015-12-11
    • 2011-06-22
    • 1970-01-01
    相关资源
    最近更新 更多