NSMutableArray背后的数据结构是什么?

时间:2014-03-23 13:05:19

标签: objective-c data-structures nsmutablearray foundation

通常,一个"可变阵列" class被实现为一个简单数组的包装器。当您在结尾添加元素时,包装器会分配更多内存。这是一种常见的数据结构,各种操作的性能是众所周知的。你得到O(1)元素访问,O(N)插入和删除,或O(1)(平均)插入和删除数组的末尾。但是NSMutableArray是另一回事。例如docs说[强调我的]:

  

注意:阵列上的大多数操作都需要 常量时间 :访问元素,在任一端添加或删除元素 ,并替换元素。将元素插入数组的中间需要线性时间。

那么,究竟是什么NSMutableArray?这是在某处记录的吗?

2 个答案:

答案 0 :(得分:23)

它是circular buffer的包装。

这既没有记录也没有开源,但this blog post显示了一个惊人的逆向工程作业NSMutableArray,我认为你会觉得非常有趣。

NSMutableArray类集群由名为__NSArrayM的具体私有子类支持。

最大的发现是NSMutableArray不是CFArray周围的薄包装,可以合理地认为:​​CFArray是开源的,它不使用循环缓冲区,而__NSArrayM确实如此。

通过阅读文章的评论,它似乎从iOS 4开始就是这种方式,而在以前的SDK NSMutableArray内部实际使用CFArray并且__NSArrayM不是即使在那里。

直接来自我上面提到的博客文章

  

数据结构

     

正如您可能已经猜到的那样,__NSArrayM使用了循环缓冲区。   这种数据结构非常简单,但更多一点   比常规数组/缓冲区复杂。循环内容   当达到任何一端时,缓冲区可以回绕。

     

循环缓冲区有一些非常酷的属性。值得注意的是,除非   缓冲区已满,任何一端的插入/删除都不需要   要移动的记忆。

objectAtIndex:的伪代码如下:

- (id)objectAtIndex:(NSUInteger)index {
    if (_used <= index) {
        goto ThrowException;
    }

    NSUInteger fetchOffset = _offset + index;
    NSUInteger realOffset = fetchOffset - (_size > fetchOffset ? 0 : _size);

    return _list[realOffset];

ThrowException:
    // exception throwing code
}

其中ivars定义为

  • _used:数组所包含的元素数量
  • _list:指向循环缓冲区的指针
  • _size:缓冲区的大小
  • _offset:缓冲区中数组的第一个元素的索引

同样,我对上述所有信息都不予以赞扬,因为它们直接来自amazing blog post by Bartosz Ciechanowski

答案 1 :(得分:1)

做了一些测量:从一个空数组开始,添加@“Hello”100,000次,然后将其删除100,000次。不同的模式:在结尾,开始,中间,接近开始时添加/删除(在可能的情况下在索引20处),接近结束(尽可能远离结束的20个索引),以及我交替的模式在接近开始和结束之间。这是100,000个物体的时间(在Core 2 Duo上测量):

Adding objects = 0.006593 seconds
Removing objects at the end = 0.004674 seconds
Adding objects at the start = 0.003577 seconds
Removing objects at the start = 0.002936 seconds
Adding objects in the middle = 3.057944 seconds
Removing objects in the middle = 3.059942 seconds
Adding objects close to the start = 0.010035 seconds
Removing objects close to the start = 0.007599 seconds
Adding objects close to the end = 0.008005 seconds
Removing objects close to the end = 0.008735 seconds
Adding objects close to the start / end = 0.008795 seconds
Removing objects close to the start / end = 0.008853 seconds

因此,每次添加/删除的时间与数组开头或结尾的距离成正比,以较近者为准。在中间添加东西是昂贵的。您最后不必完全 工作;删除靠近开头/结尾的元素也很便宜。

作为循环列表的建议实现省略了一个重要的细节:在最后一个和第一个数组元素的位置之间存在可变大小的间隙。随着数组元素的添加/删除,该间隙的大小会发生变化。需要分配更多内存,当间隙消失并添加更多对象时,需要移动对象指针;当间隙变得太大时,数组可以收缩并且需要移动对象指针。一个简单的改变(允许间隙位于任何位置,而不仅仅是在最后一个元素和第一个元素之间)将允许任何位置的变化快速(只要它是相同的位置),并且将使操作“变薄” “阵列更快。