UI 随着图表更新而迅速变慢

UI rapidly slows as chart updates

我正在使用 Xamarin.Forms 开发移动应用程序。该应用程序连接到 BLE 设备,该设备每 100 毫秒传输 16 个字节的数据。我正在使用 Syncfusion 以条形图格式绘制数据。

我可以毫无问题地连接到设备并接收数据。但是在很短的时间后,应用程序的性能开始显着下降。此后不久,它就完全停止了。显然我在处理传入数据时做错了(除非它是 Syncfusion 图表的性能问题)。

简而言之,这就是我在应用程序中经历的过程

  1. 与设备配对(在应用程序之外)
  2. 连接到设备(在应用程序中)
  3. 设置传输
  4. 通过名为 SpectrogramModel
  5. Model 处理传入数据
  6. 在名为 DataPage 的 View 中使用 Syncfusion 绘制数据,它绑定到名为 DataViewModelViewModel

深入了解这一切,配对并连接到设备后,将调用以下方法。可能是 Device.BeginInvokeOnMainThread() 调用最终开始阻止该应用程序吗?此方法是从 Connection class 调用的,它引用了 DataViewModel

private void UpdateSpectrogramChart(object sender, EventArgs e)
    {
        DebugHelper.Message(Type.Method, "UpdateSpectrogramChart");

        _characteristic.ValueUpdated += (o, args) =>
        {
            var raw = args.Characteristic.Value;

            for (int i = 0; i < raw.Length; i++)
            {
                Debug.WriteLine("Level[{0}] = {1}", i, raw[i]);
            }

            Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
            {
                DataPageViewModel.Levels.Clear();

                for (int i = SpectrogramModel.FrequencyOffset; i < raw.Length; i++)
                {
                    if (SettingsViewModel.IsViewRawData)
                    {
                        DataPageViewModel.Title = "Raw data";

                        DataPageViewModel
                        .Levels
                        .Add(
                            new SpectrogramModel(
                                raw[i],
                                1 + (i - SpectrogramModel.FrequencyOffset))
                                );
                    }
                    if (SettingsViewModel.IsViewProcessedData)
                    {
                        DataPageViewModel.Title = "Processed data";

                        DataPageViewModel
                        .Levels
                        .Add(
                            new SpectrogramModel(
                                raw[i],
                                1 + (i - SpectrogramModel.FrequencyOffset),
                                i));
                    }
                }
            });
        };
    }

SpectrogramModel 看起来像这样

public class SpectrogramModel
{
    public SpectrogramModel(byte level, int frequency)
    {
        Level = level;
        Frequency = frequency;
    }

    public SpectrogramModel(byte level, int frequency, int index) : this(level, frequency)
    {
        Level = ProcessRawLevel(level, index);
    }

    private double ProcessRawLevel(byte b, int index)
    {
        double multiplier = 0.75;
        double val = b;
        val *= multiplier;
        return val;
    }

    public static readonly int FrequencyOffset = 4;

    ...

DataPage 看起来像这样

<chart:SfChart>

        <chart:SfChart.Title>
            <chart:ChartTitle
                        Text="{Binding Title}">
            </chart:ChartTitle>
        </chart:SfChart.Title>

        <chart:SfChart.PrimaryAxis>

            <chart:CategoryAxis>
            </chart:CategoryAxis>

        </chart:SfChart.PrimaryAxis>

        <chart:SfChart.SecondaryAxis>

            <chart:NumericalAxis
                Minimum="20" 
                Maximum="100">
            </chart:NumericalAxis>

        </chart:SfChart.SecondaryAxis>

        <chart:SfChart.Series>

            <chart:ColumnSeries ItemsSource="{Binding Levels}" XBindingPath="Frequency" YBindingPath="Level"/>

        </chart:SfChart.Series>

    </chart:SfChart>

最后是DataPage绑定的DataViewModel

public class DataViewModel : BaseViewModel
{
    public DataViewModel()
    {
        Init();
    }

    private void Init()
    {
        Levels = new ObservableCollection<SpectrogramModel>();

        for (int i = 0; i < 16; i++) Levels.Add(new SpectrogramModel(20, i));
    }

    private ObservableCollection<SpectrogramModel> _levels;
    public ObservableCollection<SpectrogramModel> Levels
    {
        get { return _levels; ; }
        set
        {
            _levels = value;
            OnPropertyChanged();
        }
    }

    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            OnPropertyChanged();
        }
    }
}

需要注意的是UpdateSpectrogramChart()是包裹在定时器里面的,看起来像这样

public void InitTimers()
    {
        DebugHelper.Message(Type.Method, "InitTimers");
        int SECOND = 1000 * 2;
        SpectrogramChartTimer = new Timer();
        SpectrogramChartTimer.Elapsed += new ElapsedEventHandler(UpdateSpectrogramChart);
        SpectrogramChartTimer.Interval = SECOND;
    }

我将对 UpdateSpectrogramChart() 方法的调用包装在一次(明显)失败的尝试中以减少性能下降。

为了完整起见,这里是设置从 BLE 设备接收的方法的方法体

public async Task ReceiveFromGattCharacteristic(string service, string characteristic, string descriptor = null)
    {
        DebugHelper.Message(Type.Method, "ReceiveFromGattCharacteristic");

        bleAdapter.DeviceConnected += async (s, e) =>
        {
            try
            {
                DebugHelper.Message(Type.Info, "bleAdapter.DeviceConected += async (s, e) ...");

                string[] deviceInfo = { e.Device.Name, e.Device.Id.ToString() };

                // Connect to service
                try
                {
                    DebugHelper.Message(Type.Info, "Connecting to service...");
                    _service = await e.Device.GetServiceAsync(Guid.Parse(service));
                    DebugHelper.Message(Type.Info, "OK");
                }

                catch (Exception)
                {
                    DebugHelper.Error(ErrorType.GATT, "Could not connect to service");
                }

                // Connect to characteristic
                try
                {
                    DebugHelper.Message(Type.Info, "Connecting to characteristic...");
                    _characteristic = await _service.GetCharacteristicAsync(Guid.Parse(characteristic));
                    DebugHelper.Message(Type.Info, "OK");
                }

                catch (Exception)
                {
                    DebugHelper.Error(ErrorType.GATT, "Could not connect to characteristic");
                }

                await ConfigureSpectrogram(UpdateFrequency.High, 0x1);

                try
                {
                    await _characteristic.StartUpdatesAsync();
                }

                catch
                {
                    DebugHelper.Error(ErrorType.GATT, "Error starting UpdatesAsync");
                }

                _characteristic.ValueUpdated += (o, args) =>
                {
                    var raw = args.Characteristic.Value;

                    for (int i = 4; i < raw.Length; i++)
                    {
                        Debug.WriteLine("Level[{0}] = {1}", i - 4, raw[i]);
                    }
                };
            }

            catch (Exception)
            {
                DebugHelper.Error(ErrorType.GATT, "Error in ReceiveFromGattCharacteristic");
            }
        };
    }

好吧,我不确定这个 真的 是否可以作为答案,但我似乎已经解决了这个问题,尽管我不能确定为什么它已经解决了。

在摆弄了一个 BackgroundWorker 之后,它引入了更多的错误(可能是因为我不是它的用法专家),我修改了代码并移动了 ModelView 直接进入 ReceiveFromGattCharacteristic() 方法,而不是在单独的方法中更新 ModelView,如下所示:

public void ReceiveFromGattCharacteristic(string service, string characteristic, string descriptor = null)
    {
        DebugHelper.Message(Type.Method, "ReceiveFromGattCharacteristic");

        bleAdapter.DeviceConnected += async (s, e) =>
        {
            try
            {
                DebugHelper.Message(Type.Info, "bleAdapter.DeviceConected += async (s, e) ...");

                string[] deviceInfo = { e.Device.Name, e.Device.Id.ToString() };

                // Connect to service
                try
                {
                    DebugHelper.Message(Type.Info, "Connecting to service...");
                    _service = await e.Device.GetServiceAsync(Guid.Parse(service));
                    DebugHelper.Message(Type.Info, "OK");
                }

                catch (Exception)
                {
                    DebugHelper.Error(ErrorType.GATT, "Could not connect to service");
                }

                // Connect to characteristic
                try
                {
                    DebugHelper.Message(Type.Info, "Connecting to characteristic...");
                    _characteristic = await _service.GetCharacteristicAsync(Guid.Parse(characteristic));
                    DebugHelper.Message(Type.Info, "OK");
                }

                catch (Exception)
                {
                    DebugHelper.Error(ErrorType.GATT, "Could not connect to characteristic");
                }

                await ConfigureSpectrogram(UpdateFrequency.High, 0x1);

                try
                {
                    await _characteristic.StartUpdatesAsync();
                }

                catch
                {
                    DebugHelper.Error(ErrorType.GATT, "Error starting UpdatesAsync");
                }
                // ADDITION
                _characteristic.ValueUpdated += (o, args) =>
                {
                    var raw = args.Characteristic.Value;

                    Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
                    {
                        DataPageViewModel.Levels.Clear();

                        for (int i = Models.Spectrogram.FrequencyOffset; i < raw.Length; i++)
                        {
                            if (SettingsViewModel.IsViewRawData)
                            {
                                DataPageViewModel.Title = "Raw data";

                                DataPageViewModel
                                .Levels
                                .Add(
                                    new Models.Spectrogram(
                                        raw[i],
                                        1 + (i - Models.Spectrogram.FrequencyOffset))
                                        );
                            }

                            if (SettingsViewModel.IsViewProcessedData)
                            {
                                DataPageViewModel.Title = "Processed data";

                                DataPageViewModel
                                .Levels
                                .Add(
                                    new Models.Spectrogram(
                                        raw[i],
                                        1 + (i - Models.Spectrogram.FrequencyOffset),
                                        i));
                            }
                        }
                    });
                };
            }
            // END OF ADDITION

            catch (Exception)
            {
                DebugHelper.Error(ErrorType.GATT, "Error in ReceiveFromGattCharacteristic");
            }
        };
    }

我们想让您知道,在使用大量数据和提高性能的同时,一些 SfChart 配置需要考虑。

  1. 利用 SuspendSeriesNotification 和 ResumeSeriesNoification。

我们可以停止为项目源集合中的每次修改更新图表。通过使用 SuspendSeriesNotification 和 ResumeSeriesNotification 方法。

Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
            {
                DataPageViewModel.Levels.Clear();

Chart.SuspendSeriesNotification();

                for (int i = SpectrogramModel.FrequencyOffset; i < raw.Length; i++)
                {
                    if (SettingsViewModel.IsViewRawData)
                    {
                        DataPageViewModel.Title = "Raw data";

                        DataPageViewModel
                        .Levels
                        .Add(
                            new SpectrogramModel(
                                raw[i],
                                1 + (i - SpectrogramModel.FrequencyOffset))
                                );
                    }
                    if (SettingsViewModel.IsViewProcessedData)
                    {
                        DataPageViewModel.Title = "Processed data";

                        DataPageViewModel
                        .Levels
                        .Add(
                            new SpectrogramModel(
                                raw[i],
                                1 + (i - SpectrogramModel.FrequencyOffset),
                                i));
                    }
                }
Chart.ResumeSeriesNotification();
            });
  1. 避免使用分类轴。

我们发现您正在使用带有列系列的类别轴。我们总是建议将数字或日期时间轴与快速线系列一起使用以获得更好的性能。如果您确实需要类别轴和列系列,请告诉我们您的图表将加载多少数据或使用类别轴的任何原因。

从 SfChart 获得更好性能的一些技巧,请阅读下面的博客。 https://blog.syncfusion.com/post/7-tips-to-optimize-xamarin-charts-performance.aspx#comment-10677

此致, 巴拉蒂.