1. NSArray
iOS在创建NSArray时,一般创建的是__NSArrayI对象,但当其只包含一个元素时,创建的是__NSSingleObjectArrayI对象,没有任何对象时,创建的是__NSArray0对象,__NSArrayI,__NSSingleObjectArrayI和__NSArray0都继承自NSArray。
1.1 __NSSingleObjectArrayI
__NSSingleObjectArrayI的结构定义为:
@interface __NSSingleObjectArrayI : NSArray
{
id object;
}
@end
__NSSingleObjectArrayI类内部结构简单,只增加了一个存储对象的属性。
1.2 __NSArrayI
__NSArrayI的结构定义为:
@interface __NSArrayI : NSArray
{
NSUInteger _used;
id _list[0];
}
@end
_used是数组元素个数,调用[array count]返回的就是_used的值。
id _list[0]是c语言里面的柔性数组,不占用内存,且相对指针效率更高。
2. NSMutableArray
iOS 创建NSMutableArray时,实际得到的是其子类__NSArrayM对象。__NSArrayM的结构定义为:
@interface __NSArrayM : NSMutableArray
{
NSUInteger _used;
NSUInteger _offset;
int _size:28;
int _unused:4;
uint32_t _mutations;
id *_list;
}
@end
_list是存储对象数组的一块连续的内存区域
_used是数组元素个数
_offset起始偏移量
_size能存储的的对象个数,_list大小。(大于或等于_used)
_mutations修改标记,每次对__NSArrayM的修改操作都会使_mutations加1,“*** Collection <__NSArrayM: 0x1002076b0> was mutated while being enumerated.”这个异常就是通过对_mutations的识别来引发的。
分析:__NSArrayM并没有使用链表结果存储内存,链表虽然在增删方面有不需要移动数据的有优势,但是查找,分配,占用空间方面效率低下。在查找方面,数组可通过内存地址偏移迅速找到需要的对象,另一方面,因为内存连续,而且每个数组元素都只是一个指针,所以拷贝起来也很快。
__NSArrayM内部是采用数组方式实现环形缓冲区,实现环形缓冲区相对简单数组方式,在增删方面作了一定优化,尽量避免数据移动或减少移动次数。
扩容:当__NSArrayM存储容量不足时,会重新malloc一块新的存储区域,并将数据复制到新的区域,最后释放旧的存储空间。
arrayWithCapacity:方法可以指定__NSArrayM的初始容量,也就是_size/环形缓冲区的初始大小。
3. 遍历性能
对不同数组遍历方式进行性能测试,结果如图所示:

结果显示:
for in> for > 普通枚举 > 多线程并发枚举(NSEnumerationConcurrent),for in的方式性能最佳。
多线程并发枚举(NSEnumerationConcurrent)的回调是乱序,但因为线程开销相对执行任务开销(测试遍历速度,任务开销接近0)巨大,性能测试结果最差。如果在执行任务开销巨大的场景中,该方式因为采取多线程,可能更适合。
for in性能最好,是因为NSArray的几个子类都实现了NSFastEnumeration快速枚举协议。它直接从C数组中取对象。对于可变数组来说,它最多只需要两次就可以获取全部元素。如果数组还没有构成循环,那么第一次就获得了全部元素。但是如果数组构成了循环,那么就需要两次,第一次获取对象数组的起始偏移到循环数组末端的元素,第二次获取存放在循环数组起始处的剩余元素。而for循环之所以慢一点,是因为for循环的时候每次都要调用objectAtIndex:。