游戏开发中,能否实现Add操作无内存分配的INotifyCollectionChanged?
解决方案
保留INotifyCollectionChanged接口的优化方案
1. 复用自定义单元素列表,避免SingleItemReadOnlyList分配
.NET的NotifyCollectionChangedEventArgs在传入单个元素时会自动创建SingleItemReadOnlyList,这是主要的GC来源。你可以实现一个可复用的单元素只读列表,然后使用接受IList参数的构造函数创建EventArgs,避免每次创建新的只读列表:
public class ReusableSingleItemList<T> : IList<T> { public T CurrentItem { get; set; } public T this[int index] { get => index == 0 ? CurrentItem : throw new IndexOutOfRangeException(); set => throw new NotSupportedException(); } public int Count => 1; public bool IsReadOnly => true; // 实现IList的其他方法,所有修改操作抛出NotSupportedException public void Add(T item) => throw new NotSupportedException(); public void Clear() => throw new NotSupportedException(); public bool Contains(T item) => EqualityComparer<T>.Default.Equals(CurrentItem, item); public void CopyTo(T[] array, int arrayIndex) => array[arrayIndex] = CurrentItem; public IEnumerator<T> GetEnumerator() => new SingleItemEnumerator(this); public int IndexOf(T item) => Contains(item) ? 0 : -1; public void Insert(int index, T item) => throw new NotSupportedException(); public bool Remove(T item) => throw new NotSupportedException(); public void RemoveAt(int index) => throw new NotSupportedException(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); private sealed class SingleItemEnumerator : IEnumerator<T> { private readonly ReusableSingleItemList<T> _list; private bool _moved; public SingleItemEnumerator(ReusableSingleItemList<T> list) => _list = list; public T Current => _moved ? _list.CurrentItem : throw new InvalidOperationException(); object IEnumerator.Current => Current; public bool MoveNext() { if (_moved) return false; _moved = true; return true; } public void Reset() => _moved = false; public void Dispose() {} } }
使用时池化该列表实例:
private readonly ReusableSingleItemList<YourItemType> _reusableList = new ReusableSingleItemList<YourItemType>(); private void OnItemAdded(YourItemType item, int index) { _reusableList.CurrentItem = item; var args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, _reusableList, index); CollectionChanged?.Invoke(this, args); }
2. 反射复用NotifyCollectionChangedEventArgs实例
虽然NotifyCollectionChangedEventArgs的属性是只读的,但可以通过反射修改其内部字段,复用预创建的实例:
// 预创建复用的EventArgs实例 private static readonly NotifyCollectionChangedEventArgs _reusableAddArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, null); private static readonly NotifyCollectionChangedEventArgs _reusableRemoveArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, null); // 缓存反射字段(仅初始化一次) private static readonly FieldInfo _actionField = typeof(NotifyCollectionChangedEventArgs) .GetField("_action", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo _newItemsField = typeof(NotifyCollectionChangedEventArgs) .GetField("_newItems", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo _oldItemsField = typeof(NotifyCollectionChangedEventArgs) .GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo _newIndexField = typeof(NotifyCollectionChangedEventArgs) .GetField("_newStartingIndex", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo _oldIndexField = typeof(NotifyCollectionChangedEventArgs) .GetField("_oldStartingIndex", BindingFlags.NonPublic | BindingFlags.Instance); // 触发添加事件时复用 private void RaiseAddEvent(YourItemType item, int index) { _actionField.SetValue(_reusableAddArgs, NotifyCollectionChangedAction.Add); _newItemsField.SetValue(_reusableAddArgs, new[] { item }); // 可结合上面的复用列表进一步优化 _newIndexField.SetValue(_reusableAddArgs, index); CollectionChanged?.Invoke(this, _reusableAddArgs); }
注意:该方法依赖.NET内部实现细节,不同版本(如.NET Framework/.NET Core/.NET 5+)的字段名可能变化,需针对目标平台测试。
3. 批量合并变更
如果一帧内有多次增删操作,可收集这些变更,在帧末触发一次批量变更事件,减少EventArgs的创建次数:
private readonly List<YourItemType> _addedItems = new List<YourItemType>(); private readonly List<YourItemType> _removedItems = new List<YourItemType>(); public void AddItem(YourItemType item) { _items.Add(item); _addedItems.Add(item); } public void RemoveItem(YourItemType item) { _items.Remove(item); _removedItems.Add(item); } // 在帧末调用该方法 public void FlushChanges() { if (_addedItems.Count > 0) { var args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, _addedItems); CollectionChanged?.Invoke(this, args); _addedItems.Clear(); } if (_removedItems.Count > 0) { var args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, _removedItems); CollectionChanged?.Invoke(this, args); _removedItems.Clear(); } }
若允许逻辑接受全量刷新,也可直接触发Reset事件,完全避免多次变更的分配,但会导致订阅者重新遍历整个集合,适合集合规模不大的场景。
自定义变更通知方案
如果上述优化仍无法满足内存要求,可放弃INotifyCollectionChanged,改用自定义的变更事件和可复用的EventArgs:
public class CustomCollectionChangeArgs<T> : EventArgs { public NotifyCollectionChangedAction Action { get; set; } public T Item { get; set; } public int Index { get; set; } // 重置方法,用于实例复用 public void Reset(NotifyCollectionChangedAction action, T item, int index) { Action = action; Item = item; Index = index; } } public class YourPooledCollection<T> { private readonly ObjectPool<CustomCollectionChangeArgs<T>> _argsPool = new DefaultObjectPool<CustomCollectionChangeArgs<T>>(new DefaultPooledPolicy<CustomCollectionChangeArgs<T>>()); public event EventHandler<CustomCollectionChangeArgs<T>> CustomCollectionChanged; private void RaiseCustomEvent(NotifyCollectionChangedAction action, T item, int index) { var args = _argsPool.Get(); args.Reset(action, item, index); CustomCollectionChanged?.Invoke(this, args); _argsPool.Return(args); } }
这种方案完全消除GC分配,但需要修改所有订阅变更的代码,不再兼容依赖INotifyCollectionChanged的组件(如WPF绑定)。
内容的提问来源于stack exchange,提问作者Victor Chelaru
相关产品推荐
相关产品推荐

