Итак, у меня были проблемы с производительностью в приложении 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
получает уникальный восходящий идентификатор, который отображается в пользовательском интерфейсе. Запустите приложение!
Без утилизации
[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
:
При утилизации
[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.