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

时间:2017-09-15 09:27:16

标签: c# xaml listview xamarin.forms xamarin.android

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

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

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

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

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>

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);
    }
}

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"
    };
}

请注意CachingStrategyRetainElement。每个TestControl都会获得一个唯一的升序ID,该ID会显示在UI中。让我们运行应用程序!

不回收

Screenshot with recycling disabled

[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'

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

当我们将CachingStrategy设置为RecycleElement时会发生有趣的事情:

随着回收

Screenshot with recycling enabled

[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'

糟糕。单元格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进行了测试

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

我没有在iOS设备上测试。

1 个答案:

答案 0 :(得分:3)

CachingStrategy设置为RecycleElement会使列表视图变得混乱,因为您使用的是TextBock中未从BindingContext检索的值。 (int myid;)。

让我们看一下Xamarin文档RecycleElement

  

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

     
      
  1. 当每个细胞具有小到中等数量的结合时。
  2.   
  3. 当每个单元格的BindingContext定义所有单元格数据时。
  4.   
  5. 当每个细胞大致相似时,细胞模板不变。
  6.   

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

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

为什么?

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

因此myId 1 的文字广告可以作为&#34;两个&#34; 值的容器。 (这就是虚拟化的含义。)

答案:更改myId逻辑,从BindingContext检索它将会解决问题。