普通 C 数组的问题
任何典型的程序员都知道 C 数组的原理。可以归结为一段能被方便读写的连续内存空间。
使用一段线性内存空间的一个最明显的缺点是,在下标 0 处插入一个元素时,需要移动其它所有的元素,即 memmove 的原理:
memmove用于拷贝字节,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,但复制后源内容会被更改。但是当目标区域与源区域没有重叠则和memcpy函数功能相同。
图1
同样地,假如想要保持相同的内存指针作为首个元素的地址,移除第一个元素需要进行相同的动作
图2
当数组非常大时,这样很快会成为问题。显而易见,直接指针存取在数组的世界里必定不是最高级的抽象。C 风格的数组通常很有用,但 Obj-C 程序员每天的主要工作使得它们需要 NSMutableArray 这样一个可变的、可索引的容器。
ivars 的意思
我们来概括下每个 ivar 的意思:
_used 是计数的意思
_list 是缓冲区指针
_size 是缓冲区的大小
_offset 是在缓冲区里的数组的第一个元素索引
内存布局
最关键的部分是决定 realOffset 应该等于 fetchOffset(减去 0)还是 fetchOffset 减 _size。看着纯代码不一定能画出完美的图画,我们设想一下两个关于如何获取对象的例子。
_size > fetchOffset
这个例子中,偏移量相对较小:
为了获取 0 处的对象,我们计算出 fetchOffset 等于 3 + 0。因为 _size 大于 fetchOffset,realOffset 也等于 3。代码返回 _list[3] 的值。而获取 4 处的对象时,fetchOffset 等于 3 + 4,代码返回 _list[7]。
_size <= fetchOffset
当偏移量比较大时会怎样?
图4
获取 0 处的对象,使得 fetchOffset 等于 7 + 0,调用方法后如期望的返回 _list[7]。然而,获取 4 处的对象时,fetchOffset 等于 7 + 4 = 11,要大于 _size。获得的 realOffset 要从 fetchOffset 减去 _size,即 11 - 10 = 1,方法返回 list[1]。
我们基本上是在做取模运算,当穿过缓存区边界时会转回缓冲区的另一端。
数据结构
正如你会猜测的,__NSArrayM 用了环形缓冲区 (circular buffer)。这个数据结构相当简单,只是比常规数组或缓冲区复杂点。环形缓冲区的内容能在到达任意一端时绕向另一端。
环形缓冲区有一些非常酷的属性。尤其是,除非缓冲区满了,否则在任意一端插入或删除均不会要求移动任何内存。我们来分析这个类如何充分利用环形缓冲区来使得自身比 C 数组强大得多。在任意一端插入或者删除,只是修改offset参数,不需要移动内存,我们访问的时候只是不和普通的数组一样index多少就是多少,这里会计算加上offset之后处理的值取数据,而不是插入头和尾巴的时候,环形结构会根据最少移动内存指针的方式插入,例如要在A和B之间插入,按照C的数组,我们需要把B到E的元素移动内存,但是环形缓冲区的设计,我们只要把A的值向前移动一个单位内存,即可,同时修改offset偏移量,就能保证最小的移动单元来完成中间插入
在两端插入或删除会相当地快
我么来思考一下一个非常简单的例子:
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
[array addObject:@(i)];
}
[array removeObjectAtIndex:0];
[array removeObjectAtIndex:0];
NSLog(@"%@", [array explored_description]);
输出显示移除位于 0 处的对象两次后,只是简单地清除了指针并由此而移动了 _offset ivar:
Size: 6
Count: 3
Offset: 2
Storage: 0x178245ca0
[0] 0x0
[1] 0x0
[2] 0xb000000000000022
[3] 0xb000000000000032
[4] 0xb000000000000042
[5] 0x0
图5
头部插入
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 4; i++) {
[array addObject:@(i)];
}
[array insertObject:@(15) atIndex:0];
在 0 处插入对象用了环形缓冲区魔法来将新插入的对象放置在缓存区的末端:
Size: 6
Count: 5
Offset: 5
Storage: 0x17004a560
[0] 0xb000000000000002
[1] 0xb000000000000012
[2] 0xb000000000000022
[3] 0xb000000000000032
[4] 0x0
[5] 0xb0000000000000f2
图6
可以看到插入头尾只是修改offset指针而已,如果插入数据到达阀值,一样需要扩容。
最糟糕的就是中间插入和删除中间
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
[array addObject:@(i)];
}
[array removeObjectAtIndex:3];
从输出中我们看到顶部的元素往下移动,底部为低索引(注意 [5] 处的游离指针):
[0] 0xb000000000000002
[1] 0xb000000000000012
[2] 0xb000000000000022
[3] 0xb000000000000042
[4] 0xb000000000000052
[5] 0xb000000000000052
然而,当我们调用 [array removeObjectAtIndex:2] 时,底部的元素往上移动,顶部为高索引:
往中部插入对象有非常相似的结果。合理的解释就是,__NSArrayM 试着去最小化内存的移动,因此会移动最少的一边元素。
感谢:
https://blog.csdn.net/Deft_MKJing/article/details/82732833
http://blog.joyingx.me/2015/05/03/NSMutableArray%20%E5%8E%9F%E7%90%86%E6%8F%AD%E9%9C%B2/
网友评论