You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI中CollectionView在iOS上比Android慢100倍问题排查

.NET MAUI CollectionView在iOS加载729条数据异常缓慢(Android正常)

问题现象

Android端CollectionView加载729条数据仅需2秒,iOS设备(iPhone 11)加载相同数据耗时60-65秒。调试确认数据库取数仅0.5秒,耗时点集中在数据填充到CollectionView并渲染UI的阶段。

测试环境

  • Android:Pixel 4模拟器(API 33),调试模式
  • iOS:iPhone 11真机,热重载调试模式
  • 框架:.NET 7.0
  • 数据量:729行

相关代码

View(XAML)

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         x:Class="Hm.View.Dava_Dosyalari.GetFilesPage"
         xmlns:model="clr-namespace:Hm.Model"
         xmlns:view ="clr-namespace:Hm.View.Files"
         xmlns:viewmodel="clr-namespace:Hm.ViewModel.Files"
         x:DataType="viewmodel:FilesViewModel"
         Title="Files">
<Grid RowDefinitions="40, *">
    <Frame Grid.Row="0" BackgroundColor="AntiqueWhite" Padding="0">
        <Grid ColumnDefinitions="*,Auto" ColumnSpacing="5">
            <SearchBar Grid.Column="0" x:Name="searchBar" TextChanged="searchBar_TextChanged" TextColor="Black" VerticalOptions="Center"/>
            <Label Grid.Column="1" VerticalOptions="Center" Text="{Binding TotalCount, StringFormat='Total: {0}'}" Margin="0,0,5,0"/>
        </Grid>
    </Frame>
    <RefreshView Margin="10" 
                 Grid.Row="1"
                 Command="{Binding GetFilesCommand}"
                 IsRefreshing="{Binding IsRefreshing}">
        <ContentView>
            <CollectionView ItemsSource="{Binding files}"
                        SelectionMode="None">
                <CollectionView.EmptyView>
                    <StackLayout Padding="50" Spacing="10">
                        <Image Source="aa.png" Aspect="AspectFit"
                       VerticalOptions="Center"
                       HorizontalOptions="Center"/>
                        <Label Text="Wait for getting files..."
                           Style="{StaticResource MediumLabel}"
                           HorizontalOptions="Center"/>
                    </StackLayout>
                </CollectionView.EmptyView>

                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="model:Files">
                        <Grid Padding="10" HorizontalOptions="Fill">
                            <Frame HeightRequest="150" CornerRadius="20">
                                <Frame.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:FilesViewModel}}, Path=GoToFileCommand}"
                                                          CommandParameter="{Binding .}"/>
                                </Frame.GestureRecognizers>
                                <VerticalStackLayout VerticalOptions="Center" Spacing="2">
                                    <Label FontAutoScalingEnabled="True" Style="{StaticResource SmallLabel}" Text="{Binding attr1, StringFormat='Attr1: {0}'}" Margin="3,0,0,0"/>
                                    <Label FontAutoScalingEnabled="True" Style="{StaticResource SmallLabel}" Text="{Binding attr2, StringFormat='Attr2: {0}'}" Margin="3,0,0,0"/>
                                    <Label FontAutoScalingEnabled="True" Style="{StaticResource MicroLabel}" Text="{Binding attr3, StringFormat='Attr3: {0}'}" Margin="3,0,0,0"/>
                                    <Label FontAutoScalingEnabled="True" Style="{StaticResource MicroLabel}" Text="{Binding attr4, StringFormat='Attr4: {0}'}" Margin="3,0,0,0"/>
                                </VerticalStackLayout>
                            </Frame>
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </ContentView>
    </RefreshView>

    <ActivityIndicator
        Grid.ColumnSpan="1"
        Grid.RowSpan="2"
        HorizontalOptions="Fill"
        IsRunning="{Binding IsBusy}"
        IsVisible="{Binding IsBusy}"
        VerticalOptions="Center" />
</Grid>
</ContentPage>

ViewModel(C#)

public partial class FilesViewModel : BaseViewModel
{
    [ObservableProperty]
    bool isRefreshing;
    

    FilesService fileService;

    public ObservableCollection<Files> files { get; set; } = new();
    public ObservableCollection<Files> _allfiles { get; set; } = new();

    [ObservableProperty]
    string totalCount;

    public FilesViewModel(FileService service)
    {
        fileService = service;
        GetFilesCommand.ExecuteAsync(null);
    }

    [RelayCommand]
    async Task GetFilesAsync()
    {
        if (IsBusy)
            return;

        try
        {
            IsBusy = true;

            var fileList = await fileService.GetFilesFromDb();

            if (files != null || files.Count > 0)
                files.Clear();
            if (_allfiles != null || _allfiles.Count > 0)
                _allfiles.Clear();

            await App.Current.Dispatcher.DispatchAsync(() =>
            {
                fileList.ForEach(i => files.Add(i));
                fileList.ForEach(i => _allfiles.Add(i));
                TotalCount = files.Count.ToString();
            });

        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Warning", ex.Message, "OK");
            return;
        }
        finally
        {
            IsRefreshing = false;
            IsBusy = false;
        }
    }

    [RelayCommand]
    async Task GoToFileAsync(Files files)
    {
        if(files== null)
            return;

        await Shell.Current.GoToAsync(nameof(GoToFilePage), true, new Dictionary<string, object>()
        {
            {"Att1", files },
            {"Id", files.id }
        });
    }
}

问题分析与优化方案

1. 集合更新方式导致频繁UI触发

当前代码用fileList.ForEach(i => files.Add(i))逐条添加数据,ObservableCollection每添加一条就会触发一次CollectionChanged事件,iOS UI线程处理这类频繁更新的开销远高于Android。

优化:直接替换整个集合,仅触发一次PropertyChanged通知:

// 替换原逐条添加代码
Files = new ObservableCollection<Files>(fileList);
AllFiles = new ObservableCollection<Files>(fileList);

注意:需将files和_allfiles改为用[ObservableProperty]标记的自动属性,确保替换时能触发绑定更新。

2. ItemTemplate布局嵌套过重

ItemTemplate中嵌套了Grid→Frame→VerticalStackLayout→多个Label,Frame是渲染成本较高的控件(带阴影、圆角等效果),iOS上多层嵌套会放大性能损耗。

优化:

  • 移除不必要的嵌套,比如外层Grid可直接用控件的Padding/Margin替代;
  • 用更轻量的Border控件替换Frame;
  • 改用Grid布局Label,减少布局层级:
<CollectionView.ItemTemplate>
    <DataTemplate x:DataType="model:Files">
        <Border HeightRequest="150" 
                CornerRadius="20"
                Padding="10"
                Margin="10">
            <Border.GestureRecognizers>
                <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:FilesViewModel}}, Path=GoToFileCommand}"
                                      CommandParameter="{Binding .}"/>
            </Border.GestureRecognizers>
            <Grid>
                <Label FontAutoScalingEnabled="True" Style="{StaticResource SmallLabel}" Text="{Binding attr1, StringFormat='Attr1: {0}'}" Margin="3,0,0,0"/>
                <Label FontAutoScalingEnabled="True" Style="{StaticResource SmallLabel}" Text="{Binding attr2, StringFormat='Attr2: {0}'}" Margin="3,20,0,0"/>
                <Label FontAutoScalingEnabled="True" Style="{StaticResource MicroLabel}" Text="{Binding attr3, StringFormat='Attr3: {0}'}" Margin="3,40,0,0"/>
                <Label FontAutoScalingEnabled="True" Style="{StaticResource MicroLabel}" Text="{Binding attr4, StringFormat='Attr4: {0}'}" Margin="3,60,0,0"/>
            </Grid>
        </Border>
    </DataTemplate>
</CollectionView.ItemTemplate>

3. 热重载调试模式的影响

iOS热重载调试模式本身会增加性能开销,建议关闭热重载,用普通调试或Release模式测试,确认性能是否改善。

4. 空值判断逻辑错误

代码中if (files != null || files.Count > 0)逻辑错误,应改为&&,不过由于集合已初始化,该判断实际多余,可直接调用Clear()。

总结

该问题主要由代码写法导致,而非.NET MAUI本身的问题。通过优化集合更新方式、简化布局层级,基本可解决iOS端加载缓慢的问题。

内容的提问来源于stack exchange,提问作者sagopaj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 11:34:55