ListView 性能

编写移动应用程序时,性能很重要。 用户期望平滑滚动,还期望加载速度快。 如果未能满足用户的期望,在应用程序商店中的排名就会下降,或者在业务线应用程序的情况下,则会耗费组织的时间和金钱。

Xamarin.FormsListView 是显示数据的强大视图,但它也有一些限制。 使用自定义单元格时,滚动性能可能会受到影响,尤其是在单元格包含深度嵌套视图层次结构,或使用某些需要复杂度量的布局时。 幸运的是,可以使用一些技巧来避免出现性能欠佳的情况:

缓存策略

ListView 通常用于显示远远超出屏幕尺寸的数据。 例如,音乐应用的歌曲库可能有数千个条目。 为每个条目创建一个项会浪费宝贵的内存,并导致性能欠佳。 不断地创建和销毁行需要应用程序不断地实例化和清理对象,这也会导致性能不佳。

为了节省内存,每个平台的本机 ListView 等效项都具有可供重复使用行的内置功能。 只有屏幕上可见的单元格会加载到内存中,内容加载到现有单元格中。 此模式可防止应用程序实例化数千个对象,从而节省时间和内存。

Xamarin.Forms 允许通过 ListViewCachingStrategy 枚举重用 ListView 单元格,该枚举具有以下值:

public enum ListViewCachingStrategy
{
    RetainElement,   // the default value
    RecycleElement,
    RecycleElementAndDataTemplate
}

注意

通用 Windows 平台 (UWP) 会忽略 RetainElement 缓存策略,因为它始终使用缓存来提高性能。 因此在默认情况下,它的行为就像应用了 RecycleElement 缓存策略一样。

RetainElement

RetainElement 缓存策略指定 ListView 将为列表中的每个项生成一个单元格,并且是默认 ListView 行为。 它应在以下情况下使用:

  • 每个单元格都有大量绑定 (20-30+)。
  • 单元格模板经常更改。
  • 测试表明,RecycleElement 缓存策略会降低执行速度。

使用自定义单元格时,必须认识到 RetainElement 缓存策略的后果。 每次创建单元格时都需要运行单元格初始化代码,该代码可能需要每秒运行多次。 在这种情况下,页面上效果很好的布局技术,例如使用多个嵌套的 StackLayout 时,在用户滚动时被实时设置和销毁,成为了性能瓶颈。

RecycleElement

RecycleElement 缓存策略指定 ListView 将尝试通过回收列表单元格来最大程度减少其内存占用和降低执行速度。 这种模式并不总能提高性能,应执行测试以确定是否有任何改进。 但是,这是首选方案,并应在以下情况下使用:

  • 每个单元格具有少量到中等数量的绑定。
  • 每个单元格的 BindingContext 定义所有单元格数据。
  • 每个单元格大体相似,单元格模板保持不变。

在虚拟化期间,单元格将更新其绑定上下文,因此如果应用程序使用此模式,它必须确保正确处理绑定上下文更新。 有关单元格的所有数据都必须来自绑定上下文,否则可能会出现一致性错误。 可以通过使用数据绑定显示单元格数据来避免此问题。 或者,应在 OnBindingContextChanged 重写中设置单元格数据,而不是在自定义单元格的构造函数中设置,如以下代码示例所示:

public class CustomCell : ViewCell
{
    Image image = null;
    
    public CustomCell ()
    {
        image = new Image();
        View = image;
    }
    
    protected override void OnBindingContextChanged ()
    {
        base.OnBindingContextChanged ();
        
        var item = BindingContext as ImageItem;
        if (item != null) {
            image.Source = item.ImageUrl;
        }
    }
}

有关详细信息,请参阅绑定上下文更改

在 iOS 和 Android 上,如果单元格使用自定义呈现器,它们必须确保正确实现属性更改通知。 在单元格被重复使用的情况下,当绑定上下文更新为可用单元格的绑定上下文时,它们的属性值会更改,并引发 PropertyChanged 事件。 有关详细信息,请参阅自定义 ViewCell

使用 DataTemplateSelector 的 RecycleElement

ListView 使用 DataTemplateSelector 来选择 DataTemplate 时,RecycleElement 缓存策略不会缓存 DataTemplate。 而是为列表中的每个数据项选择 DataTemplate

注意

RecycleElement 缓存策略有一个先决条件(在 Xamarin.Forms 2.4 中引入),那就是当 DataTemplateSelector 必须选择 DataTemplate 时,每个 DataTemplate 必须返回相同的 ViewCell 类型。 例如,给定一个带有可返回 MyDataTemplateA(其中 MyDataTemplateA 返回一个类型为 MyViewCellAViewCell)或 MyDataTemplateB(其中 MyDataTemplateB 返回一个类型为 MyViewCellBViewCell)的 DataTemplateSelectorListView,返回 MyDataTemplateA 时必须返回 MyViewCellA,否则将引发异常。

RecycleElementAndDataTemplate

RecycleElementAndDataTemplate 缓存策略建立在 RecycleElement 缓存策略的基础上,此外,还确保当 ListView 使用 DataTemplateSelector 来选择 DataTemplate 时,会按列表中的项类型缓存 DataTemplate。 因此,为每个项类型选择一次 DataTemplate,而不是为每个项实例选择一次。

注意

RecycleElementAndDataTemplate 缓存策略具有一个先决条件,那就是 DataTemplateSelector 返回的 DataTemplate 必须使用采用 TypeDataTemplate 构造函数。

设置缓存策略

ListViewCachingStrategy 枚举值是使用 ListView 构造函数重载指定的,如以下代码示例所示:

var listView = new ListView(ListViewCachingStrategy.RecycleElement);

在 XAML 中,按以下 XAML 所示设置 CachingStrategy 属性:

<ListView CachingStrategy="RecycleElement">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
              ...
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

此方法与在 C# 中的构造函数中设置缓存策略参数的效果相同。

在子类化的 ListView 中设置缓存策略

在子类化的 ListView 上从 XAML 设置 CachingStrategy 特性不会生成所需的行为,因为 ListView 上没有 CachingStrategy 属性。 此外,如果启用了 XAMLC,将生成以下错误消息:“找不到 "CachingStrategy" 的属性、可绑定属性或事件”

此问题的解决方案是,在接受 ListViewCachingStrategy 参数并将其传递到基类的子类化 ListView 上指定构造函数:

public class CustomListView : ListView
{
    public CustomListView (ListViewCachingStrategy strategy) : base (strategy)
    {
    }
    ...
}

然后,可以使用 x:Arguments 语法从 XAML 指定 ListViewCachingStrategy 枚举值:

<local:CustomListView>
    <x:Arguments>
        <ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
    </x:Arguments>
</local:CustomListView>

ListView 性能建议

有许多技术可用来提高 ListView 的性能。 以下建议可提高 ListView 的性能

  • ItemsSource 属性绑定到 IList<T> 集合而不是 IEnumerable<T> 集合,因为 IEnumerable<T> 集合不支持随机访问。
  • 尽可能地使用内置单元格(如 TextCell / SwitchCell)而不是 ViewCell
  • 使用更少的元素。 例如,请考虑使用单个 FormattedString 标签而不是多个标签。
  • 当显示非同质数据(即不同类型的数据)时,将 ListView 替换为 TableView
  • 限制对 Cell.ForceUpdateSize 方法的使用。 如果过度使用该方法,会降低性能。
  • 在 Android 上,不要在实例化 ListView 的行分隔符后设置它的可见性或颜色,因为这会导致性能大幅下降。
  • 不要根据 BindingContext 更改单元格布局。 更改布局会产生大量的度量和初始化成本。
  • 避免深度嵌套的布局层次结构。 使用 AbsoluteLayoutGrid 来帮助减少嵌套。
  • 避免 Fill 之外的特定 LayoutOptions(对计算来说,Fill 成本最低)。
  • 出于以下原因,不要将 ListView 放置在 ScrollView 内:
    • ListView 实现其自己的滚动。
    • ListView 不会接收任何手势,这些手势将由父级 ScrollView 处理。
    • ListView 可以显示一个自定义页眉和页脚,该页眉页脚随列表的元素一起滚动,可能提供使用了 ScrollView 的功能。 有关详细信息,请参阅页面和页脚
  • 如果需要单元格中显示的特定复杂设计,请考虑自定义呈现器。

AbsoluteLayout 有可能在没有单个度量调用的情况下执行布局,这使得它具有高性能。 如果无法使用 AbsoluteLayout,请考虑使用 RelativeLayout。 如果使用 RelativeLayout,则直接传递约束比使用表达式 API 要快得多。 此方法速度更快,因为表达式 API 使用 JIT,并且在 iOS 上必须解释树,这会拖慢速度。 表达式 API 适用于只有初始布局和旋转需要它的页面布局,但在 ListView 中,它在滚动期间不断运行,因此会损害性能。

要减少布局计算对滚动性能的影响,一种方法是为 ListView 或其单元格生成自定义呈现器。 有关详细信息,请参阅自定义 ListView自定义 ViewCell