Что это за невидимая, работоспособная клетка в утилизации ListView?

Итак, у меня были проблемы с производительностью в приложении Xamarin.Forms(на Android) с помощью ListView. Причина в том, что я использую очень сложный пользовательский элемент управления в 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"
    };
}

Обратите внимание, что CachingStrategy - RetainElement. Каждый TestControl получает уникальный восходящий идентификатор, который отображается в пользовательском интерфейсе. Запустите приложение!

Без утилизации

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'

Хорошо, каждое Связывание увольняется дважды по какой-то причине. Это не происходит в моем приложении, поэтому мне все равно. Я также сравниваю 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 невидима, но ее привязка обновляется много. Я даже не касался экрана один раз, поэтому никаких прокруток не было.

Когда я нажимаю экран и прокручиваю примерно один или два пикселя, привязка идентификатора 1 обновляется примерно еще 15 раз.

Пожалуйста, обратитесь к этому видео прокрутки ListView:
https://www.youtube.com/watch?v=EuWTGclz7uc

Это абсолютный убийца производительности в моем реальном приложении, где TestControl - действительно сложный элемент управления.

Интересно, что в моем реальном приложении это прослушивание ID 2 вместо ID 1. Я предположил, что это всегда вторая ячейка, поэтому я вернулся с мгновенным возвратом, если ID равен 2. Это сделало производительность ListView приятной и гладкой.

Теперь, когда я смог воспроизвести эту проблему с идентификатором, отличным от 2, я боюсь своего решения.

Таким образом, мои вопросы: Что это за невидимая ячейка, почему она получает так много обновлений привязки и как я могу обойти проблемы с производительностью?

Я тестировал версии Xamarin.Forms 2.3.4.247, 2.3.4.270 и 2.4.0.269-pre2 на

  • Samsung Galaxy S5 mini (Android 6.0)
  • Samsung Galaxy Tab S2 (Android 7.0)

Я не тестировал устройство iOS.

Ответ 1

Настройка CachingStrategy - RecycleElement вызывает беспорядок в виде списка, потому что вы используете значение в TextBock, которое не извлекается из BindingContext. (int myid;).

Посмотрите на документацию Xamarin RecycleElement

Однако, как правило, это предпочтительный выбор, а следует использовать в следующие обстоятельства:

  • Когда каждая ячейка имеет небольшое или среднее число привязок.
  • Когда каждая ячейка BindingContext определяет все данные ячейки.
  • Когда каждая ячейка во многом схожа, с неизменным шаблоном ячейки.

Во время виртуализации ячейка будет обновлять контекст привязки, поэтому, если приложение использует этот режим, оно должно гарантировать, что соответствующие обновления контекста обрабатываются соответствующим образом. Все данные о ячейке должны поступать из контекста привязки или могут возникать ошибки согласованности.

Вы должны использовать режим RecycleElement, когда каждая ячейка BindingContext определяет все данные ячейки. Ваш int myid - это данные ячейки, но не определяется контекстом привязки.

Почему?

Я могу предположить, что в режиме RecycleElement при прокрутке: элементы управления не изменяются, изменения вносятся только в их привязки. Я думаю, что это делается для сокращения времени на рендеринг элементов управления. (Также для уменьшения использования памяти для большого количества элементов)

Таким образом, текстовый блок с myId 1 может служить контейнером для значения "Два" . (Это то, что означает виртуализация.)

Ответ: Изменение логики myId для извлечения из BindingContext приведет к трюку.