什么是回收ListView中这种看不见的性能食物细胞?

Phy*_*dha 11 c# xaml listview xamarin.android xamarin.forms

所以我的Xamarin.Forms应用程序(在Android上)使用了一个性能问题ListView.原因是,因为我在ListView中使用了一个非常复杂的自定义控件ItemTemplate.

为了提高性能,我在自定义控件中实现了许多缓存功能,并将ListView设置CachingStrategyRecycleElement.

表现并没有好转.所以我试图找出原因是什么.

我终于发现了一些非常奇怪的bug并将其隔离在一个新的空应用中.代码如下:

MainPage.xaml中

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:c="clr-namespace:ListViewBug.Controls"
             xmlns:vm="clr-namespace:ListViewBug.ViewModels"
             x:Class="ListViewBug.MainPage">
    <ContentPage.BindingContext>
        <vm:MainViewModel />
    </ContentPage.BindingContext>

    <ListView ItemsSource="{Binding Numbers}" CachingStrategy="RetainElement"
              HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
              HasUnevenRows="True">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <c:TestControl Foo="{Binding}" />
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>
Run Code Online (Sandbox Code Playgroud)

TestControl.cs

public class TestControl : Grid
{
    static int id = 0;
    int myid;

    public static readonly BindableProperty FooProperty = BindableProperty.Create("Foo", typeof(string), typeof(TestControl), "", BindingMode.OneWay, null, (bindable, oldValue, newValue) =>
    {
        int sourceId = ((TestControl)bindable).myid;
        Debug.WriteLine(String.Format("Refreshed Binding on TestControl with ID {0}. Old value: '{1}', New value: '{2}'", sourceId, oldValue, newValue));
    });

    public string Foo
    {
        get { return (string)GetValue(FooProperty); }
        set { SetValue(FooProperty, value); }
    }

    public TestControl()
    {
        this.myid = ++id;

        Label label = new Label
        {
            Margin = new Thickness(0, 15),
            FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
            Text = this.myid.ToString()
        };
        this.Children?.Add(label);
    }
}
Run Code Online (Sandbox Code Playgroud)

MainViewModel.cs

public class MainViewModel
{
    public List<string> Numbers { get; set; } = new List<string>()
    {
        "one",
        "two",
        "three",
        "four",
        "five",
        "six",
        "seven",
        "eight",
        "nine",
        "ten",
        "eleven",
        "twelve",
        "thirteen",
        "fourteen",
        "fifteen",
        "sixteen",
        "seventeen",
        "eighteen",
        "nineteen",
        "twenty"
    };
}
Run Code Online (Sandbox Code Playgroud)

注意CachingStrategy就是RetainElement.每个都TestControl获得一个唯一的升序ID,显示在UI中.让我们运行应用程序!

没有回收

禁用回收的屏幕截图

[0:] Refreshed Binding on TestControl with ID 1. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 1. Old value: '', New value: 'one'
[0:] Refreshed Binding on TestControl with ID 2. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 2. Old value: '', New value: 'two'
[0:] Refreshed Binding on TestControl with ID 3. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 3. Old value: '', New value: 'three'
[0:] Refreshed Binding on TestControl with ID 4. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 4. Old value: '', New value: 'four'
[0:] Refreshed Binding on TestControl with ID 5. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 5. Old value: '', New value: 'five'
[0:] Refreshed Binding on TestControl with ID 6. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 6. Old value: '', New value: 'six'
[0:] Refreshed Binding on TestControl with ID 7. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 7. Old value: '', New value: 'seven'
[0:] Refreshed Binding on TestControl with ID 8. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 8. Old value: '', New value: 'eight'
[0:] Refreshed Binding on TestControl with ID 9. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 9. Old value: '', New value: 'nine'
[0:] Refreshed Binding on TestControl with ID 10. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 10. Old value: '', New value: 'ten'
[0:] Refreshed Binding on TestControl with ID 11. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 11. Old value: '', New value: 'eleven'
[0:] Refreshed Binding on TestControl with ID 12. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 12. Old value: '', New value: 'twelve'
Run Code Online (Sandbox Code Playgroud)

好吧,每个Binding由于某种原因被解雇两次.这不会发生在我的实际应用中,因此我并不在乎.我还比较了oldValue和newValue,如果它们是相同的则不执行任何操作,因此这种行为不会影响性能.

有趣的事情发生在我们设置CachingStrategyRecycleElement:

随着回收

启用回收的屏幕截图

[0:] Refreshed Binding on TestControl with ID 1. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 1. Old value: '', New value: 'one'
[0:] Refreshed Binding on TestControl with ID 2. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 2. Old value: '', New value: 'one'
[0:] Refreshed Binding on TestControl with ID 3. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 3. Old value: '', New value: 'two'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'one', New value: 'two'
[0:] Refreshed Binding on TestControl with ID 4. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 4. Old value: '', New value: 'three'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'two', New value: 'three'
[0:] Refreshed Binding on TestControl with ID 5. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 5. Old value: '', New value: 'four'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'three', New value: 'four'
[0:] Refreshed Binding on TestControl with ID 6. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 6. Old value: '', New value: 'five'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'four', New value: 'five'
[0:] Refreshed Binding on TestControl with ID 7. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 7. Old value: '', New value: 'six'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'five', New value: 'six'
[0:] Refreshed Binding on TestControl with ID 8. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 8. Old value: '', New value: 'seven'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'six', New value: 'seven'
[0:] Refreshed Binding on TestControl with ID 9. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 9. Old value: '', New value: 'eight'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'seven', New value: 'eight'
[0:] Refreshed Binding on TestControl with ID 10. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 10. Old value: '', New value: 'nine'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'eight', New value: 'nine'
[0:] Refreshed Binding on TestControl with ID 11. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 11. Old value: '', New value: 'ten'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'nine', New value: 'ten'
[0:] Refreshed Binding on TestControl with ID 12. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 12. Old value: '', New value: 'eleven'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'ten', New value: 'eleven'
[0:] Refreshed Binding on TestControl with ID 13. Old value: '', New value: ''
[0:] Refreshed Binding on TestControl with ID 13. Old value: '', New value: 'twelve'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'eleven', New value: 'twelve'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'twelve', New value: 'one'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'one', New value: 'two'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'two', New value: 'three'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'three', New value: 'four'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'four', New value: 'five'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'five', New value: 'six'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'six', New value: 'seven'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'seven', New value: 'eight'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'eight', New value: 'nine'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'nine', New value: 'ten'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'ten', New value: 'eleven'
[0:] Refreshed Binding on TestControl with ID 1. Old value: 'eleven', New value: 'twelve'
Run Code Online (Sandbox Code Playgroud)

哎呀.单元格1是不可见的,但它的Binding更新了很多.我甚至没有触摸屏幕一次,因此不涉及滚动.

当我点击屏幕并向下滚动大约一个或两个像素时,ID 1的绑定会再次刷新15次.

请参阅此视频,向我滚动ListView:https:
//www.youtube.com/watch?v = EuWTGclz7uc

这是我真实应用程序中的绝对性能杀手,这TestControl是一个非常复杂的控件.

有趣的是,在我的真实应用程序中,它是ID 2而不是ID 1,这是错误的.我假设它总是第二个单元格,所以如果ID为2,我最终会立即返回.这使得ListView性能更加流畅.

现在我已经能够用2以外的ID重现这个问题,我害怕我自己的解决方案.

因此我的问题是:这个看不见的单元是什么,为什么它会得到如此多的绑定更新以及如何绕过性能问题?

我测试了Xamarin.Forms版本2.3.4.247,2.3.4.270和2.4.0.269-pre2 on

  • 三星Galaxy S5 mini(Android 6.0)
  • 三星Galaxy Tab S2(Android 7.0)

我没有在iOS设备上测试.

Ara*_*yan 3

设置CachingStrategyRecycleElement会弄乱您的列表视图,因为您使用的值TextBock不是从 BindingContext 中检索的。( int myid;)。

我们来看看Xamarin文档RecycleElement

然而,它通常是首选,并且应该在以下情况下使用

  1. 当每个细胞具有少量到中等数量的结合时。
  2. 当每个单元格的 BindingContext 定义所有单元格数据时。
  3. 当每个单元格很大程度上相似时,单元格模板不变。

在虚拟化期间,单元将更新其绑定上下文,因此如果应用程序使用此模式,则必须确保正确处理绑定上下文更新。有关单元格的所有数据都必须来自绑定上下文,否则可能会出现一致性错误。

RecycleElement当每个单元格的 BindingContext 定义了所有单元格数据时,您应该考虑使用模式。您int myid是单元格数据,但不是由绑定上下文定义的。

为什么?

我可以猜测,在RecycleElement滚动模式下:控件没有被更改,仅对其绑定进行更改。我认为这样做是为了减少渲染控件的时间。(也可以减少大量项目的内存使用)

myId 因此带有1 的文本块可以作为“Two”值的容器。(这就是虚拟化的意义。)

答案:改变你的myId逻辑,从中检索它就BindingContext可以了。