蓝牙扫描在 Xamarin.iOS 中抛出 ArgumentOutOfRangeException

Bluetooth Scan throwing ArgumentOutOfRangeException in Xamarin.iOS

我有一个使用 this plugin to continuously scan for nearby devices. When devices are discovered, they are added to an ObservableRangeCollection (from James Montemagno's MvvmHelpers 的 BLE 扫描应用程序。如果设备已经在列表中并且其某些数据(例如 RSSI 强度)发生变化,则使用 ReplaceRange() 更新集合。如果设备在列表中的时间太长而没有更新,则会将其从集合中删除。

这在 Android 上运行顺利,但在 iOS 上抛出 ArgumentOutOfRange 异常。异常并不总是同时出现,但如果我离开应用 运行 足够长的时间,它最终会抛出。不幸的是,堆栈跟踪没有指向我的代码中的任何内容,因此很难判断这是 BLE 插件、ObservableRangeCollection 插件甚至 Xamarin.Forms 发生的事情。

这是异常消息和堆栈跟踪:

{System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.Parameter name: index
  at Xamarin.Forms.ListProxy.get_Item (System.Int32 index) [0x0000b] in D:\a\s\Xamarin.Forms.Core\ListProxy.cs:129 
  at Xamarin.Forms.ListProxy.System.Collections.IList.get_Item (System.Int32 index) [0x00000] in D:\a\s\Xamarin.Forms.Core\ListProxy.cs:443 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].get_Item (System.Int32 index) [0x00008] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+ListViewDataSource.GetCellForPath (Foundation.NSIndexPath indexPath) [0x00007] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:1327 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+ListViewDataSource.GetCell (UIKit.UITableView tableView, Foundation.NSIndexPath indexPath) [0x00021] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:1036 
  at (wrapper managed-to-native) ObjCRuntime.Messaging.void_objc_msgSend(intptr,intptr)
  at UIKit.UITableView.EndUpdates () [0x0000d] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UITableView.g.cs:294 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+<>c__DisplayClass52_0.<DeleteRows>b__0 () [0x00048] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:599 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.DeleteRows (System.Int32 oldStartingIndex, System.Int32 oldItemsCount, System.Int32 section) [0x00046] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:603 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.UpdateItems (System.Collections.Specialized.NotifyCollectionChangedEventArgs e, System.Int32 section, System.Boolean resetWhenGrouped) [0x00119] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:542 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.OnCollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x00000] in D:\a\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:332 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0000a] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].OnProxyCollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0045f] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.ListProxy.OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0000a] in D:\a\s\Xamarin.Forms.Core\ListProxy.cs:232 
  at (wrapper other) System.Object.gsharedvt_out_sig(object&,intptr)
  at Xamarin.Forms.ListProxy+<>c__DisplayClass34_0.<OnCollectionChanged>b__0 () [0x00018] in D:\a\s\Xamarin.Forms.Core\ListProxy.cs:208 
  at (wrapper other) System.Object.__interp_lmf_mono_interp_entry_from_trampoline(intptr,intptr)
  at Foundation.NSAsyncActionDispatcher.Apply () [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/Foundation/NSAction.cs:152 
  at (wrapper managed-to-native) UIKit.UIApplication.UIApplicationMain(int,string[],intptr,intptr)
  at UIKit.UIApplication.Main (System.String[] args, System.IntPtr principal, System.IntPtr delegate) [0x00005] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UIKit/UIApplication.cs:86 
  at UIKit.UIApplication.Main (System.String[] args, System.String principalClassName, System.String delegateClassName) [0x0000e] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UIKit/UIApplication.cs:65 
  at SDA.iOS.Application.Main (System.String[] args) [0x00001] in C:\Users\shodg\SDA\SDA\SDA.iOS\Main.cs:12 

这是 ObservableRangeCollection 中 adding/updating/removing 设备的视图模型中的相关代码:

        public ObservableRangeCollection<DeviceListItemViewModel> Devices { get; set; } = new ObservableRangeCollection<DeviceListItemViewModel>();

        private void OnDeviceAdded(BleDevice device)
        {
            Devices.Add(new DeviceListItemViewModel(device));
        }

        private void OnDeviceUpdated(BleDevice device)
        {
            Devices.ReplaceRange(Devices.OrderByDescending(x => x.Rssi).ToList());
        }

        private void OnDeviceExpired(BleDevice device)
        {
            var vm = Devices.FirstOrDefault(d => d.Id == device.Id);
            Devices.Remove(vm);
        }

似乎异常与 iOS 上的 ListView 更新有关,我在 problems with ObservableCollections in Xamarin.iOS 上看到了一些线程,但我无法确定这个问题是否出在Montemagno 插件或其他地方,最好的前进道路是什么。

非常欢迎任何见解!

事实证明,连续运行的 BLE 扫描正在产生竞争条件,即在使用已删除设备更新列表的同时删除了一个项目。

在主线程上调用并向 属性 添加锁似乎可以防止抛出该异常:

        private object _deviceListLock = new object();
        public ObservableRangeCollection<DeviceListItemViewModel> Devices { get; set; } = new ObservableRangeCollection<DeviceListItemViewModel>();

        private void OnDeviceAdded(BleDevice device)
        {
            InvokeOnMainThread(() =>
            {
                lock (_deviceListLock)
                {
                    Devices.Add(new DeviceListItemViewModel(device));
                }
            });
        }

        private void OnDeviceUpdated(BleDevice device)
        {
            InvokeOnMainThread(() =>
            {
                lock (_deviceListLock)
                {
                    Devices.ReplaceRange(Devices.OrderByDescending(x => x.Rssi).ToList());
                }
            });
        }

        private void OnDeviceExpired(BleDevice device)
        {
            InvokeOnMainThread(() =>
            {
                lock (_deviceListLock)
                {
                    var vm = Devices.FirstOrDefault(d => d.Id == device.Id);
                    Devices.Remove(vm);
                }
            });
        }

为什么这在 iOS 上是必要的,但在 Android 上不是必要的,我不完全确定。