通常,一个"可变阵列" class被实现为一个简单数组的包装器。当您在结尾添加元素时,包装器会分配更多内存。这是一种常见的数据结构,各种操作的性能是众所周知的。你得到O(1)元素访问,O(N)插入和删除,或O(1)(平均)插入和删除数组的末尾。但是NSMutableArray
是另一回事。例如docs说[强调我的]:
注意:阵列上的大多数操作都需要 常量时间 :访问元素,在任一端添加或删除元素 ,并替换元素。将元素插入数组的中间需要线性时间。
那么,究竟是什么NSMutableArray
?这是在某处记录的吗?
答案 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
因此,每次添加/删除的时间与数组开头或结尾的距离成正比,以较近者为准。在中间添加东西是昂贵的。您最后不必完全 工作;删除靠近开头/结尾的元素也很便宜。
作为循环列表的建议实现省略了一个重要的细节:在最后一个和第一个数组元素的位置之间存在可变大小的间隙。随着数组元素的添加/删除,该间隙的大小会发生变化。需要分配更多内存,当间隙消失并添加更多对象时,需要移动对象指针;当间隙变得太大时,数组可以收缩并且需要移动对象指针。一个简单的改变(允许间隙位于任何位置,而不仅仅是在最后一个元素和第一个元素之间)将允许任何位置的变化快速(只要它是相同的位置),并且将使操作“变薄” “阵列更快。