4

更新:解决了!请参阅下面的答案以获取解决方案。

我的应用程序在 UICollectionView 中显示了许多图像。当新项目的插入速度太快以至于集合视图无法处理时,我目前遇到了 insertItemsAtIndexPaths 的问题。以下是例外情况:

NSInternalInconsistencyException 原因:一个视图上有太多更新动画 - 一次限制为 31 个在飞行中

事实证明,这是由于我的模型缓冲了多达 20 个新图像并将它们一次推送到数据源而不是在集合视图批量更新块内。没有批量更新不是由于我的懒惰造成的,而是因为我的数据源之间的抽象层实际上是一个 .Net Observable 集合(下面的代码)。

我想知道的是开发人员应该如何防止在飞行中达到 31 个动画的硬编码限制?我的意思是,当它发生时,你就完蛋了。那么苹果是怎么想的呢?

阅读代码的 Monotouch 开发人员注意:

崩溃实际上是由 UICollectionViewDataSourceFlatReadOnly 用 CollectionChanged 事件压倒 UIDataBoundCollectionView 引起的,它代表底层可观察集合代理到控件。这导致collectionview 被非批处理的InsertItems调用锤击。(是的,保罗,它是一个 ReactiveCollection)。

UIDataBoundCollectionView

/// <summary>
/// UITableView subclass that supports automatic updating in response 
/// to DataSource changes if the DataSource supports INotifiyCollectionChanged
/// </summary>
[Register("UIDataBoundCollectionView")]
public class UIDataBoundCollectionView : UICollectionView,
  IEnableLogger
{
  public override NSObject WeakDataSource
  {
    get
    {
      return base.WeakDataSource;
    }

    set
    {
      var ncc = base.WeakDataSource as INotifyCollectionChanged;
      if(ncc != null)
      {
        ncc.CollectionChanged -= OnDataSourceCollectionChanged;
      }

      base.WeakDataSource = value;

      ncc = base.WeakDataSource as INotifyCollectionChanged;
      if(ncc != null)
      {
        ncc.CollectionChanged += OnDataSourceCollectionChanged;
      }
    }
  }

  void OnDataSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  {
    NSIndexPath[] indexPaths;

    switch(e.Action)
    {
      case NotifyCollectionChangedAction.Add:
        indexPaths = IndexPathHelper.FromRange(e.NewStartingIndex, e.NewItems.Count);
        InsertItems(indexPaths);
        break;

      case NotifyCollectionChangedAction.Remove:
        indexPaths = IndexPathHelper.FromRange(e.OldStartingIndex, e.OldItems.Count);
        DeleteItems(indexPaths);
        break;

      case NotifyCollectionChangedAction.Replace:
      case NotifyCollectionChangedAction.Move:
        PerformBatchUpdates(() =>
        {
          for(int i=0; i<e.OldItems.Count; i++)
            MoveItem(NSIndexPath.FromItemSection(e.OldStartingIndex + i, 0), NSIndexPath.FromItemSection(e.NewStartingIndex + i, 0));
        }, null);
        break;

      case NotifyCollectionChangedAction.Reset:
        ReloadData();
        break;
    }
  }
}

UICollectionViewDataSourceFlatReadOnly

/// <summary>
/// Binds a table to an flat (non-grouped) items collection 
/// Supports dynamically changing collections through INotifyCollectionChanged 
/// </summary>
public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource,
  ICollectionViewDataSource,
  INotifyCollectionChanged
{
  /// <summary>
  /// Initializes a new instance of the <see cref="UICollectionViewDataSourceFlat"/> class.
  /// </summary>
  /// <param name="table">The table.</param>
  /// <param name="items">The items.</param>
  /// <param name="cellProvider">The cell provider</param>
  public UICollectionViewDataSourceFlatReadOnly(IReadOnlyList<object> items, ICollectionViewCellProvider cellProvider)
  {
    this.items = items;
    this.cellProvider = cellProvider;

    // wire up proxying collection changes if supported by source
    var ncc = items as INotifyCollectionChanged;
    if(ncc != null)
    {
      // wire event handler
      ncc.CollectionChanged += OnItemsChanged;
    }
  }

  #region Properties
  private IReadOnlyList<object> items;
  private readonly ICollectionViewCellProvider cellProvider;
  #endregion

  #region Overrides of UICollectionViewDataSource

  public override int NumberOfSections(UICollectionView collectionView)
  {
    return 1;
  }

  public override int GetItemsCount(UICollectionView collectionView, int section)
  {
    return items.Count;
  }

  /// <summary>
  /// Gets the cell.
  /// </summary>
  /// <param name="tableView">The table view.</param>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
  {
    // reuse or create new cell
    var cell = (UICollectionViewCell) collectionView.DequeueReusableCell(cellProvider.Identifier, indexPath);

    // get the associated collection item
    var item = GetItemAt(indexPath);

    // update the cell
    if(item != null)
      cellProvider.UpdateCell(cell, item, collectionView.GetIndexPathsForSelectedItems().Contains(indexPath));

    // done
    return cell;
  }

  #endregion

  #region Implementation of ICollectionViewDataSource

  /// <summary>
  /// Gets the item at.
  /// </summary>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public object GetItemAt(NSIndexPath indexPath)
  {
    return items[indexPath.Item];
  }

  public int ItemCount
  {
    get
    {
      return items.Count;
    }
  }

  #endregion

  #region INotifyCollectionChanged implementation

  // UIDataBoundCollectionView will subscribe to this event
  public event NotifyCollectionChangedEventHandler CollectionChanged;

  #endregion

  void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
  {
    if(CollectionChanged != null)
      CollectionChanged(sender, e);
  }
}
4

2 回答 2

3

凉爽的!最新版本的 RxUI 有一个类似的 UITableView 类,即ReactiveTableViewSource。我也遇到了一些棘手的问题NSInternalInconsistencyException

  1. 如果您的任何更新是重置,您需要忘记做其他所有事情
  2. 如果应用程序在同一次运行中添加和删除了相同的项目,您需要检测并消除它(即甚至不要告诉 UIKit)。当您意识到 Add / Remove 可以更改一系列索引而不仅仅是单个索引时,这会变得更加棘手。
于 2013-09-28T18:11:38.327 回答
2

更新:在我写完这个答案将近一年后,我强烈建议使用Paul Betts 提到的 ReactiveUI CollectionView/TableView 绑定功能。现在处于更加成熟的状态。


结果证明解决方案比预期的要困难一些。多亏了 RX,在 UICollectionViewDataSourceFlatReadOnly 中很容易解决限制每个项目插入或删除的速率。下一步涉及在 UIDataBoundCollectionView 中将这些更改一起批处理。PerformBatchUpdate 在这里没有帮助,但使用所有插入的 IndexPaths 发出单个 InsertItems 调用确实解决了问题。

由于 UICollectionView 验证其内部一致性的方式(即,它在每个 InsertItem 或 DeleteItems 等之后调用 GetItemsCount),我不得不将 ItemCount 管理交给 UIDataBoundCollectionView(这很难接受,但别无选择)。

顺便说一句,性能是一流的。

这是任何感兴趣的人的更新源:

ICollectionViewDataSource

public interface ICollectionViewDataSource
{
  /// <summary>
  /// Gets the bound item at the specified index
  /// </summary>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  object GetItemAt(NSIndexPath indexPath);

  /// <summary>
  /// Gets the actual item count.
  /// </summary>
  /// <value>The item count.</value>
  int ActualItemCount { get; }

  /// <summary>
  /// Gets or sets the item count reported to UIKit
  /// </summary>
  /// <value>The item count.</value>
  int ItemCount { get; set; }

  /// <summary>
  /// Observable providing change monitoring
  /// </summary>
  /// <value>The collection changed observable.</value>
  IObservable<NotifyCollectionChangedEventArgs[]> CollectionChangedObservable { get; }
}

UIDataBoundCollectionView

[Register("UIDataBoundCollectionView")]
public class UIDataBoundCollectionView : UICollectionView,
  IEnableLogger
{
  public UIDataBoundCollectionView (NSObjectFlag t) : base(t)
  {
  }

  public UIDataBoundCollectionView (IntPtr handle) : base(handle)
  {
  }

  public UIDataBoundCollectionView (RectangleF frame, UICollectionViewLayout layout) : base(frame, layout)
  {
  }

  public UIDataBoundCollectionView (NSCoder coder) : base(coder)
  {
  }

  protected override void Dispose(bool disposing)
  {
    base.Dispose(disposing);

    if(collectionChangedSubscription != null)
    {
      collectionChangedSubscription.Dispose();
      collectionChangedSubscription = null;
    }
  }

  IDisposable collectionChangedSubscription;

  public override NSObject WeakDataSource
  {
    get
    {
      return base.WeakDataSource;
    }

    set
    {
      if(collectionChangedSubscription != null)
      {
        collectionChangedSubscription.Dispose();
        collectionChangedSubscription = null;
      }

      base.WeakDataSource = value;

      collectionChangedSubscription = ICVS.CollectionChangedObservable
        .Subscribe(OnDataSourceCollectionChanged);
    }
  }

  ICollectionViewDataSource ICVS
  {
    get { return (ICollectionViewDataSource) WeakDataSource; }
  }

  void OnDataSourceCollectionChanged(NotifyCollectionChangedEventArgs[] changes)
  {
    List<NSIndexPath> indexPaths = new List<NSIndexPath>();
    int index = 0;

    for(;index<changes.Length;index++)
    {
      var e = changes[index];

      switch(e.Action)
      {
        case NotifyCollectionChangedAction.Add:
          indexPaths.AddRange(IndexPathHelper.FromRange(e.NewStartingIndex, e.NewItems.Count));
          ICVS.ItemCount++;

          // attempt to batch subsequent changes of the same type
          if(index < changes.Length - 1)
          {
            for(int i=index + 1; i<changes.Length; i++)
            {
              if(changes[i].Action == e.Action)
              {
                indexPaths.AddRange(IndexPathHelper.FromRange(changes[i].NewStartingIndex, changes[i].NewItems.Count));
                index++;
                ICVS.ItemCount++;
              }
            }
          }

          InsertItems(indexPaths.ToArray());
          indexPaths.Clear();
          break;

        case NotifyCollectionChangedAction.Remove:
          indexPaths.AddRange(IndexPathHelper.FromRange(e.OldStartingIndex, e.OldItems.Count));
          ICVS.ItemCount--;

          // attempt to batch subsequent changes of the same type
          if(index < changes.Length - 1)
          {
            for(int i=index + 1; i<changes.Length; i++)
            {
              if(changes[i].Action == e.Action)
              {
                indexPaths.AddRange(IndexPathHelper.FromRange(changes[i].OldStartingIndex, changes[i].OldItems.Count));
                index++;
                ICVS.ItemCount--;
              }
            }
          }

          DeleteItems(indexPaths.ToArray());
          indexPaths.Clear();
          break;

        case NotifyCollectionChangedAction.Replace:
        case NotifyCollectionChangedAction.Move:
          PerformBatchUpdates(() =>
          {
            for(int i=0; i<e.OldItems.Count; i++)
              MoveItem(NSIndexPath.FromItemSection(e.OldStartingIndex + i, 0), NSIndexPath.FromItemSection(e.NewStartingIndex + i, 0));
          }, null);
          break;

        case NotifyCollectionChangedAction.Reset:
          ICVS.ItemCount = ICVS.ActualItemCount;
          ReloadData();
          break;
      }
    }
  }
}

UICollectionViewDataSourceFlatReadOnly

public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource,
  ICollectionViewDataSource
{
  /// <summary>
  /// Initializes a new instance of the <see cref="UICollectionViewDataSourceFlat"/> class.
  /// </summary>
  /// <param name="table">The table.</param>
  /// <param name="items">The items.</param>
  /// <param name="cellProvider">The cell provider</param>
  public UICollectionViewDataSourceFlatReadOnly(IReadOnlyList<object> items, ICollectionViewCellProvider cellProvider)
  {
    this.items = items;
    this.cellProvider = cellProvider;

    // wire up proxying collection changes if supported by source
    var ncc = items as INotifyCollectionChanged;
    if(ncc != null)
    {
      collectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
        h => ncc.CollectionChanged += h, h => ncc.CollectionChanged -= h)
        .SubscribeOn(TaskPoolScheduler.Default)
        .Select(x => x.EventArgs)
        .Buffer(TimeSpan.FromMilliseconds(100), 20)
        .Where(x => x.Count > 0)
        .Select(x => x.ToArray())
        .ObserveOn(RxApp.MainThreadScheduler)
        .StartWith(new[] { reset});   // ensure initial update
    }

    else
      collectionChangedObservable = Observable.Return(reset);
  }

  #region Properties
  private IReadOnlyList<object> items;
  private readonly ICollectionViewCellProvider cellProvider;
  IObservable<NotifyCollectionChangedEventArgs[]> collectionChangedObservable;
  static readonly NotifyCollectionChangedEventArgs[] reset = new[] { new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) };
  #endregion

  #region Overrides of UICollectionViewDataSource

  public override int NumberOfSections(UICollectionView collectionView)
  {
    return 1;
  }

  public override int GetItemsCount(UICollectionView collectionView, int section)
  {
    return ItemCount;
  }

  /// <summary>
  /// Gets the cell.
  /// </summary>
  /// <param name="tableView">The table view.</param>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
  {
    // reuse or create new cell
    var cell = (UICollectionViewCell) collectionView.DequeueReusableCell(cellProvider.Identifier, indexPath);

    // get the associated collection item
    var item = GetItemAt(indexPath);

    // update the cell
    if(item != null)
      cellProvider.UpdateCell(cell, item, collectionView.GetIndexPathsForSelectedItems().Contains(indexPath));

    // done
    return cell;
  }

  #endregion

  #region Implementation of ICollectionViewDataSource

  /// <summary>
  /// Gets the item at.
  /// </summary>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public object GetItemAt(NSIndexPath indexPath)
  {
    return items[indexPath.Item];
  }

  public int ActualItemCount
  {
    get
    {
      return items.Count;
    }
  }

  public int ItemCount { get; set; }

  public IObservable<NotifyCollectionChangedEventArgs[]> CollectionChangedObservable
  {
    get
    {
      return collectionChangedObservable;
    }
  }

  #endregion
}
于 2013-09-27T13:03:13.013 回答