一、普通C语言的数组实现:
是开辟一段连续的内存空间,缺点:在插入下标为0的元素,会移动其他所有元素。添加,插入,删除同理。
当数组非常大时,这样很快会成为问题。
二、OC的NSMutableArray实现原理:
1、NSMutableArray 是一个类族,【【NSMutableArray new】 class】 打印出来的是 __NSArrayM。
2、通过class-dump逆向查看__NSArrayM里面的比较重要的成员变量:
_used : 计数
_list : 缓冲区指针
_size : 缓冲区大小
_offset : 缓冲区中的数组的第一个元素的索引
3、过程 原理,OC数组比C语言里的数组用了稍微复杂一点的环形缓冲区
环形缓冲区的特点是:在大多数情况下,除非缓冲区满了,否则不需要频繁分配内存。
并且NSMutableArray做到了如果我们在中间进行插入或者删除,只会移动最少的一边的元素。
来看一下 增、删、改、查。
查询:
以 -(void)objectAtIndex:(NSIteger)index; 为例子
答:
-(void)objectAtIndex:(NSIteger)index { NSIteger originalOffset = _offset + index; NSIteger reallOffset = originalOffset - ( _size > originalOffset ? 0 : size ); Return _list[ reallOffset ]; }
分析:分两种情况,需比较 _size 与 _offset + index的大小
A: 当 _size >= _offset + index 时:
B:当 _size < _offset + index 时:
三、证实过程:
增加:
由于逆向不出源码,我们用内存地址来证实这个原理
插入元素代码:
输出:
删除:
在没有插入55对象是的array打印结构:
插入 str5时,打印array对象:
补充:
x/100xb arr的意思是: 读取100个字节,并以16进制显示
- 第一个x是memory read的简写(通过help x可以看出来)
- 第二个x代表16进制
- b代表字节
四、数组扩容
1、初始化给定容量时,系统会开辟对应的容量,在给定容量特别大时,会crash。
总结:
当输入值小于等于128且为偶数的情况下:
初始化最终容量 = 输入的容量的值
初始化最终容量 = 输入的容量的值 + 1
当输入值大于128且为偶数的情况下:
初始化最终容量 = 比输入的容量的值 小且最接近的 2的幂 + 64的倍数
例子:
129 - > 128 + 64 = 192
193 -> 128 + 2* 62 = 256
257 -> 256 + 64 = 320
321 -> 256 + 2* 64 = 384
959 -> 512 + 7* 64 = 960
如果有兴趣,大家根据现象可以推倒一下程序是怎么写的 ,但并不重要
用new的方式初始化,没加入元素的时候,是还没有分配内存。
2、系统扩容:简单记忆:在现有容量 * 1.625(大概)
3、扩容后删除元素,数组地址有缩容
A、删除所有元素
用removeLastObject,所有元素都删除,数组的内存地址还存在,容量为2
用removeAllObjects,所有元素都删除,数组被销毁
B、逐个删除元素
当有9个元素的时候:缩小规律是:10,4,2
当有100个元素的时候:缩小规律是:110,42,16,6,2
当有1205个元素的时候:缩小规律是:1600,640,256,98,38,14,6,2
总结:动态数组在删除元素的时候,会对用缩小容量。规则:d = 数组当前容量 / k ( k 为2.5 到 2.7中间的数,大家有兴趣可以自己逆向试试找一下这个数组); 当数组的元素数量 小于等于 d时,会在内存开辟一个大小为d的线性内存,把数组赋值到新地址中,达到节省内存的目的。
这块有个设计的注意点:扩容和缩容的比例不一样,可以避免临界值的性能问题。不会到达某个值添加一个扩容,删除一个缩小。
注:这块内容网上结识的比较少,是我自己总结,可以大家再一起讨论。