iOS NSMutableArray 的内存管理原理

我一直想知道NSMutableArray内部如何运作。不要误会我的意思,不可变阵列肯定会带来巨大的好处。它们不仅是线程安全的,而且复制它们基本上是免费的。它并没有改变它们非常沉闷的事实 - 它们的内容无法修改。我发现实际的内存操作细节令人着迷,这就是本文关注可变数组的原因。
由于我或多或少地描述了我曾经调查的完整过程NSMutableArray,所以这篇文章相当技术性。有一整节讨论ARM64程序集,所以如果你觉得无聊,那就不要犹豫了。一旦我们通过低级别的细节,我就会呈现 该类的非显而易见的特征。
NSMutableArray出于某种原因,实施细节是私有的。它们在任何时候都可能发生变化,包括底层子类和它们的ivar布局,以及基础算法和数据结构。无论这些警告如何,都值得深入NSMutableArray了解并弄清楚它是如何工作的以及我们对它的期望。以下研究基于iOS 7.0 SDK。
像往常一样,你可以在我的GitHub上找到随附的Xcode项目。
普通老C阵列的问题

每个自尊的程序员都知道C数组是如何工作的。它归结为连续的内存段,可以轻松读取和写入。虽然数组和指针不一样(参见专家C编程或本文),但将“ malloc-ed内存块”视为数组并不是一种滥用。
使用线性存储器最明显的缺点之一是在索引0处插入元素需要通过以下方式移动所有其他元素memmove:
在索引0处插入C数组
类似地,删除第一个元素也需要移动操作,假设有人想要将相同的内存指针保持为第一个元素的地址:
从索引0处的C数组中删除
对于非常大的阵列,这很快就成了问题。显然,直接指针访问不一定是数组世界中最高级别的抽象。虽然C风格的数组通常很有用,但Obj-C程序员每天都需要一个可变的索引容器NSMutableArray。
NSMutableArray里

潜水

尽管Apple 发布了许多库的源代码,但Foundation和它NSMutableArray并不是开源的。然而,有一些工具可以让它更容易揭开它的神秘面纱。我们以最高级别开始我们的旅程,进入较低层以获得其他无法访问的细节。
获取和倾倒课程

NSMutableArray是一个类集群 - 它的具体实现实际上是NSMutableArray它自己的子类。哪个类实际+[NSMutableArray new]返回的实例?使用LLDB,我们甚至不需要编写任何代码来解决这个问题:

1 2
(lldb) po [[NSMutableArray new] class] __NSArrayM

有了类名,我们就可以进行类转储了。这个方便的实用程序伪造了通过分析提供的二进制文件获得的类头。使用以下单行程,我们可以提取我们感兴趣的ivar布局:

1
./class-dump
–arch arm64
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.0.sdk/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
| pcregrep -M “1__NSArrayM[\s\w:{;]*}”

我吮吸正则表达式,所以上面使用的那个可能不是杰作,但它提供了丰硕的成果:

1 2 3 4 5 6 7 8 9 10 11 12
@interface __NSArrayM : NSMutableArray { unsigned long long _used; unsigned longlong _doHardRetain:1; unsigned long long _doWeakAccess:1; unsigned long long_size:62; unsigned long long _hasObjects:1; unsigned long long_hasStrongReferences:1; unsigned long long _offset:62; unsigned long long_mutations; id *_list; }

原始输出中的位域是键入的unsigned int,但显然您不能将62位装入32位整数 - 尚未修补类转储以正确解析ARM64库。尽管存在这些小缺陷,但只要看一下它的静脉就可以说出很多关于这个课程的知识。
拆卸类

我调查中最重要的工具是Hopper。我爱上了这个反汇编程序。对于那些必须知道一切如何运作的好奇灵魂来说,这是一个必不可少的工具。Hopper的最佳功能之一是它生成类似C的伪代码,它通常足够清晰,可以掌握实现的要点。
理解的关键方法__NSArrayM是- objectAtIndex:。虽然Hopper在为ARMv7提供伪代码方面做得很好,但这个功能还不适用于ARM64。我认为手动执行此操作是一个很好的练习,其中包含ARMv7对应的一些提示。
解剖方法

使用ARMv8指令集概述(需要注册)和另一方面的一堆有根据的猜测我认为我已正确解密了程序集。但是,您不应将以下分析视为智慧的终极来源。我是新来的。
传递参数

作为一个起点,让我们注意每个Obj-C方法实际上都是一个带有两个附加参数的C函数。第一个是self指向作为方法调用的接收者的对象的指针。第二个_cmd代表当前选择器。
有人可能会说该- objectAtIndex:函数的等效C风格声明是:

id objectAtIndex(NSArray *self, SEL _cmd, NSUInteger index);

由于上ARM64这些类型的参数在连续的寄存器被传递,我们可以预期的self指针是在x0寄存器中,_cmd在x1寄存器和index对象在x2寄存器中。有关参数传递的详细信息,请参阅ARM过程调用标准,注意Apple的iOS版本存在一些差异。
Analizing Assembly

这看起来很吓人。由于一次分析大量的装配并不合理,我们将逐步完成以下代码,弄清楚每条线的作用。

0xc2d4 stp x29, x30, [sp, #0xfffffff0]! 0xc2d8 mov x29, sp 0xc2dc sub sp, sp, #0x200xc2e0 adrp x8, #0x1d1000 0xc2e4 ldrsw x8, [x8, #0x2c] 0xc2e8 ldr x8, [x0, x8]0xc2ec cmp x8, x2 0xc2f0 b.ls 0xc33c 0xc2f4 adrp x8, #0x1d1000 0xc2f8 ldrsw x8, [x8,#0x30] 0xc2fc ldr x8, [x0, x8] 0xc300 lsr x8, x8, #0x2 0xc304 adrp x9, #0x1d10000xc308 ldrsw x9, [x9, #0x34] 0xc30c ldr x9, [x0, x9] 0xc310 add x9, x2, x9, lsr #20xc314 cmp x8, x9 0xc318 csel x8, xzr, x8, hi 0xc31c sub x8, x9, x8 0xc320 adrp x9,#0x1d1000 0xc324 ldrsw x9, [x9, #0x38] 0xc328 ldr x9, [x0, x9] 0xc32c ldr x0, [x9,x8, lsl #3] 0xc330 mov sp, x29 0xc334 ldp x29, x30, [sp], #0x10 0xc338 ret

安装程序

我们从似乎是ARM64 功能的序幕开始。我们正在保存x29并x30在堆栈上注册然后我们将当前堆栈指针移动到x29寄存器:

0xc2d4 stp x29, x30, [sp, #0xfffffff0]! 0xc2d8 mov x29, sp

我们在堆栈上腾出一些空间(减去,因为堆栈向下增长):

0xc2dc sub sp, sp, #0x20

我们感兴趣的路径代码似乎没有使用这个空间。然而,抛出代码的“越界”异常确实调用了一些其他函数,因此序言必须促进这两个选项。
获取数量

接下来的两行执行程序计数器相对寻址。地址编码的细节非常复杂,文献很少,但Hopper会自动计算更合理的偏移量:

0xc2e0 adrp x8, #0x1d1000 0xc2e4 ldrsw x8, [x8, #0x2c]

以上两行将获取位于其中的内存内容0x1d102c并将其存储到x8寄存器中。那边有什么?Hopper非常友好地帮助我们:

OBJC_IVAR$___NSArrayM._used: 0x1d102c dd 0x00000008

这是课堂_used内ivar 的偏移量__NSArrayM。为什么要经历额外获取的麻烦,而不是简单地将值8放入程序集中?这是因为脆弱的基类问题。现代的Objective-C运行时通过给自己一个覆盖值的选项来处理这个问题0x1d102c(以及所有其他的ivar偏移)。如果两个NSObject或NSArray或NSMutableArray添加新的实例变量,老的二进制文件仍然可以工作。
运行时可以动态修改ivars的偏移量而不会破坏兼容性
虽然CPU必须进行额外的内存提取,但这是一个很好的解决方案,详见Hamster Emporium和Cocoa with Love。
此时我们知道_used了班级内的偏移量。而且由于Obj-C对象只不过是structs,并且我们有指向这个结构的指针x0,我们所要做的就是获取值:

0xc2e8 ldr x8, [x0, x8]

上述代码的C等价物是:

unsigned long long newX8 = *(unsigned long long *)((char *)(__bridge void *)self +x8);

我更喜欢装配版。对反汇编- count方法的快速分析__NSArrayM表明,_usedivar包含了元素的数量__NSArrayM,并且到目前为止,我们在x8寄存器中有这个值。
检查边界

请求索引x2并在x8代码中计数比较两者:

0xc2ec cmp x8, x2 0xc2f0 b.ls 0xc33c

当值为x8低或相同时,x2我们跳转到0xc33c处理异常抛出的代码。这基本上是边界检查。如果我们测试失败(计数低于或等于索引),我们抛出异常。我不会讨论拆卸的那些部分,因为它们并没有真正引入任何新东西。如果我们通过测试(计数大于索引),那么我们只是按顺序继续执行指令。
计算内存偏移量

我们之前见过这个,这次我们取得了_sizeivar 的偏移位于0x1d1030:

0xc2f4 adrp x8, #0x1d1000 0xc2f8 ldrsw x8, [x8, #0x30]

然后我们检索其内容并将其向右移动两位:

0xc2fc ldr x8, [x0, x8] 0xc300 lsr x8, x8, #0x2

转变的是什么?我们来看看转储标题:

unsigned long long _doHardRetain:1; unsigned long long _doWeakAccess:1; unsignedlong long _size:62;

事实证明,所有三个位域共享相同的存储空间,因此要获得实际值_size,我们必须将值向右移位,丢弃位_doHardRetain和_doWeakAccess。Ivar偏移量_doHardRetain和_doWeakAccess完全相同,但它们的位访问代码明显不同。
更进一步,它是相同的演习,我们得到_offsetivar(at 0x1d1034)的内容到x9寄存器:

0xc304 adrp x9, #0x1d1000 0xc308 ldrsw x9, [x9, #0x34] 0xc30c ldr x9, [x0, x9]

在下一行中,我们将存储的请求索引添加x2到2位右移_offset(它也是62位位域),然后我们将其全部存储回来x9。装配不是很棒吗?

0xc310 add x9, x2, x9, lsr #2

接下来的三行是最重要的一行。首先,我们将_size(in x8)与_offset + index(in x9)进行比较

0xc314 cmp x8, x9

然后我们根据先前比较的结果有条件地选择一个寄存器的值。

0xc318 csel x8, xzr, x8, hi

这或多或少等于C中的?:运算符:

x8 = hi ? xzr : x8; // csel x8, xzr, x8, hi

该xzr寄存器是一个零寄存器包含值0,并且hi是的名称条件码的csel指令应检查。在这种情况下,我们检查比较结果是否更高(如果值x8大于x9)。
最后,我们x8从_offset + index(in x9)中减去新值,然后x8再将它存储起来

0xc31c sub x8, x9, x8

刚刚发生了什么?首先让我们看看等效的C代码:

int tempIndex = _offset + index; // add x9, x2, x9, lsr #2 BOOL isInRange = _size >tempIndex; // cmp x8, x9 int diff = isInRange ? 0 : _size; // csel x8, xzr, x8, hiint fetchIndex = tempIndex - diff; // sub x8, x9, x8

在C代码中,我们不必向右移动_size也不_offset向右移动,因为编译器会自动为位域访问执行此操作。
获取数据

我们快到了。让我们将_listivar(0x1d1038)的内容提取到x9寄存器中:

0xc320 adrp x9, #0x1d1000 0xc324 ldrsw x9, [x9, #0x38] 0xc328 ldr x9, [x0, x9]

此时x9指向包含数据的内存段的开头。
最后,将存储的fetch index的值x8向左移3位,将其添加到x9并将内存的内容放入该位置x0

0xc32c ldr x0, [x9, x8, lsl #3]

这里有两件事很重要。首先,每个数据偏移都以字节为单位。将值向左移3可相当于将其乘以8,这是64位体系结构上指针的大小。其次,结果进入x0哪个寄存器存储函数返回的返回值NSUInteger。
此时我们已经完成了。我们已经获取了存储在数组中的正确值。
功能Epilog

还有一些样板操作可以在调用之前恢复寄存器和堆栈指针的状态。我们正在扭转功能的序幕并返回:

0xc330 mov sp, x29 0xc334 ldp x29, x30, [sp], #0x10 0xc338 ret

把它们放在一起

我解释了这段代码的作用,但我们现在要回答的问题是为什么?
伊娃的意义

让我们快速总结每个ivar的含义:

  • _used 很重要
  • _list 是指向缓冲区的指针
  • _size 是缓冲区的大小
  • _offset 是缓冲区中数组的第一个元素的索引
    C代码

考虑到ivars并分析了反汇编,我们现在可以编写一个执行相同操作的等效Objective-C代码:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

  • (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 }

大会肯定更加广泛。
内存布局

最关键的部分是决定是否realOffset应该等于fetchOffset(减去零)或fetchOffset减去_size。由于盯着干代码并不一定描绘出完美的画面,让我们考虑一下对象提取如何工作的两个例子。
_size > fetchOffset

在此示例中,偏移量相对较低:
一个简单的例子
要在索引处获取对象,0我们计算fetchOffsetas 3 + 0。由于_size大于fetchOffset所述realOffset等于3为好。代码返回值_list[3]。在索引处获取对象4使得fetchOffset等于3 + 4并且代码返回_list[7]。
_size <= fetchOffset

偏移量大时会发生什么?
一个更难的例子
在指数抓取对象0,使fetchOffset等于7 + 0该调用返回_list[7]预期。然而,在索引获取对象4使得fetchOffset等于7 + 4 = 11,这是大于_size。获得realOffset需要减去_size值从fetchOffset这使得它11 - 10 = 1并且该方法返回_list[1]。
我们基本上是做模arithmethic,盘旋穿越缓冲区边界时回缓冲区的另一端。
数据结构

您可能已经猜到了,__NSArrayM使用循环缓冲区。这种数据结构非常简单,但比常规数组/缓冲区稍微复杂一些。当到达任一端时,循环缓冲区的内容可以环绕。
循环缓冲区有一些非常酷的属性。值得注意的是,除非缓冲区已满,否则从任一端插入/删除不需要移动任何内存。让我们分析一下这个类如何利用循环缓冲区在行为方面优于C阵列。
__NSArrayM特征

虽然对其余的反汇编方法进行逆向工程可以提供__NSArrayM内部的明确解释,但我们可以使用已发现的数据点来更高层次地研究类。
在运行时检查

要__NSArrayM在运行时检查,我们不能简单地粘贴转储的标头。首先,测试应用程序不会在没有为类提供至少空@implementation块的情况下进行链接__NSArrayM。添加此@implementation块可能不是一个好主意。而应用程序构建和实际运行,我不完全知道如何不运行时决定使用哪一个类(如果你不知道,请让我知道)。为了安全起见,我将类名重命名为独特的 - BCExploredMutableArray。
其次,ARC不会让我们在id *_list没有指明其所有权的情况下编译ivar。我们不打算写入ivar,所以预先id设置__unsafe_unretained 应该对内存管理提供最少的干扰。但是,我选择宣布伊娃,void **_list并且很快就会清楚原因。
打印输出代码

我们可以创建一个类别NSMutableArray来打印ivars的内容以及数组中包含的所有指针的列表:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

  • (NSString *)explored_description { assert([NSStringFromClass([self class])isEqualToString:@"__NSArrayM"]); BCExploredMutableArray *array =(BCExploredMutableArray *)self; NSUInteger size = array->_size; NSUInteger offset= array->_offset; NSMutableString *description = [NSMutableStringstringWithString:@"\n"]; [description appendFormat:@“Size: %lu\n”, (unsignedlong)size]; [description appendFormat:@“Count: %llu\n”, (unsigned long long)array->_used]; [description appendFormat:@“Offset: %lu\n”, (unsigned long)offset];[description appendFormat:@“Storage: %p\n”, array->_list]; for (int i = 0; i <size; i++) { [description appendFormat:@"[%d] %p\n", i, array->_list[i]]; } returndescription; }

结果

插入和删除两端都很快

让我们考虑一个非常简单的例子:

1 2 3 4 5 6 7 8 9 10
NSMutableArray *array = [NSMutableArray array]; for (int i = 0; i < 5; i++) {[array addObject:@(i)]; } [array removeObjectAtIndex:0]; [arrayremoveObjectAtIndex:0]; NSLog(@"%@", [array explored_description]);

输出显示删除索引0处的对象两次只是清除指针并相应地移动_offsetivar:

1 2 3 4 5 6 7 8 9 10
Size: 6 Count: 3 Offset: 2 Storage: 0x178245ca0 [0] 0x0 [1] 0x0 [2]0xb000000000000022 [3] 0xb000000000000032 [4] 0xb000000000000042 [5] 0x0

这是对正在发生的事情的直观解释:
删除索引0处的对象两次
添加怎么样?让我们对一个全新的阵列进行另一次测试:

1 2 3 4 5 6
NSMutableArray *array = [NSMutableArray array]; for (int i = 0; i < 4; i++) {[array addObject:@(i)]; } [array insertObject:@(15) atIndex:0];

在索引0处插入对象使用循环缓冲区魔术将新插入的对象放在缓冲区的末尾:

1 2 3 4 5 6 7 8 9 10
Size: 6 Count: 5 Offset: 5 Storage: 0x17004a560 [0] 0xb000000000000002 [1]0xb000000000000012 [2] 0xb000000000000022 [3] 0xb000000000000032 [4] 0x0 [5]0xb0000000000000f2

在视觉上:
在索引0处添加对象
这是个好消息!这意味着__NSArrayM可以从任何一方进行处理。您可以使用__NSArrayM堆栈或队列,而不会有任何性能命中。
另外,您可以看到64位体系结构如何NSNumber使用标记指针进行存储。
非整数增长因子

好的,对于这个,我有点作弊。虽然我也做了一些实证测试,但我想要具有确切的价值,而且我已经深入了解了它的反汇编insertObject:atIndex:。每当缓冲区变满时,它就会以1.625倍的大小重新分配。我感到非常惊讶,因为它不等于2。
更新: Mike Curtiss 提供了一个非常好的解释,说明为什么调整大小因子等于2是次优的。
一旦长大,不会缩小

这是一个令人震惊的 - __NSArrayM永远不会减小它的大小!我们运行以下测试代码:

1 2 3 4 5 6
NSMutableArray *array = [NSMutableArray array]; for (int i = 0; i < 10000; i++) {[array addObject:[NSObject new]]; } [array removeAllObjects];

即使此时数组为空,它仍然保留大缓冲区:

1
Size: 14336

除非您使用NSMutableArray加载大量数据然后清除阵列以释放空间,否则这不是您应该担心的问题。
初始容量几乎无关紧要

让我们分配新的数组,初始容量设置为连续2的幂:

1 2 3
for (int i = 0; i < 16; i++) { NSLog(@"%@", [[[NSMutableArray alloc]initWithCapacity:1 << i] explored_description]); }

惊喜惊喜:

1 2 3 4 5 6 7 8 9
Size:2 // requested capacity - 1 Size:2 // requested capacity - 2 Size:4 // requested capacity - 4 Size:8 // requested capacity - 8 Size:16 // requested capacity - 16 Size:16 // requested capacity - 32 Size:16 // requested capacity - 64Size:16 // requested capacity - 128 … // Size:16 all the way down

删除时它不会清除它的指针

这几乎不重要,但我发现它仍然很有趣:

1 2 3 4 5 6 7 8
NSMutableArray *array = [NSMutableArray array]; for (int i = 0; i < 6; i++) {[array addObject:@(i)]; } [array removeObjectAtIndex:1]; [arrayremoveObjectAtIndex:1]; [array removeObjectAtIndex:1];

并输出:

1 2 3 4 5 6 7 8 9 10
Size: 6 Count: 3 Offset: 3 Storage: 0x17805be10 [0] 0xb000000000000002 [1]0xb000000000000002 [2] 0xb000000000000002 [3] 0xb000000000000002 [4]0xb000000000000042 [5] 0xb000000000000052

__NSArrayM向前移动物体时,不需要清除以前的空间。但是,对象确实被取消分配。它也不是在NSNumber做它的魔力,NSObject相应的行为。
这就解释了为什么我选择将_listivar 定义为void **。如果_list声明为,id *那么以下循环将在object赋值时崩溃:

1 2 3 4
for (int i = 0; i < size; i++) { id object = array->_list[i]; NSLog("%p", object);}

ARC隐式插入保留/释放对,并访问释放的对象。虽然前面加上id object与__unsafe_unretained修复这个问题,我绝对不希望任何事情/任何人呼吁这串野指针的任何方法。这是我的void **理由。
最糟糕的情况是从中间添加/删除

在这两个例子中,我们将从数组的大致中间删除元素:

1 2 3 4 5 6
NSMutableArray *array = [NSMutableArray array]; for (int i = 0; i < 6; i++) {[array addObject:@(i)]; } [array removeObjectAtIndex:3];

在输出中,我们看到顶部向下移动,其中向下是较低的索引(注意杂散指针处于[5])

1 2 3 4 5 6
[0] 0xb000000000000002 [1] 0xb000000000000012 [2] 0xb000000000000022 [3]0xb000000000000042 [4] 0xb000000000000052 [5] 0xb000000000000052

删除索引3处的对象
但是,当我们调用时[array removeObjectAtIndex:2],底部向上移动,向上移动的指数更高:

1 2 3 4 5 6
[0] 0xb000000000000002 [1] 0xb000000000000002 [2] 0xb000000000000012 [3]0xb000000000000032 [4] 0xb000000000000042 [5] 0xb000000000000052

删除索引2处的对象
在中间插入对象具有非常相似的结果。合理的解释是__NSArrayM尝试最小化移动的内存量,因此它将最多移动一半的元素。
做一个好的子类公民

正如在NSMutableArray类参考中所讨论的,每个NSMutableArray子类必须实现以下七种方法:

    • count
    • objectAtIndex:
    • insertObject:atIndex:
    • removeObjectAtIndex:
    • addObject:
    • removeLastObject
    • replaceObjectAtIndex:withObject:
      不出所料,__NSArrayM满足了这一要求。但是,实现的所有方法的列表__NSArrayM非常短,并且不包含NSMutableArray标题中列出的21个其他方法。谁负责执行这些方法?
      事实证明,他们都是NSMutableArray班级本身的一部分。这非常方便 - 任何子类都NSMutableArray只能实现七种最基本的方法。所有其他更高级别的抽象都建立在它们之上。例如,- removeAllObjects方法只是向后迭代,- removeObjectAtIndex:逐个调用。这是伪代码:

1 2 3 4 5 6 7 8 9 10 11 12
// we actually know this is safe, since count is stored on 62 bits // and casting to NSInteger will not overflow NSInteger count = (NSInteger)[self count]; if(count == 0) { return; } count–; do { [self removeObjectAtIndex:count]; count–;} while (count >= 0);

但是,有意义的__NSArrayM 是重新实现它的一些超类的方法。例如,虽然NSArray提供了NSFastEnumeration协议- countByEnumeratingWithState:objects:count:方法的默认实现,但也有自己的代码路径。了解其内部存储可以提供更高效的实施。__NSArrayM__NSArrayM
基金会

我一直有这个想法,基金会是CoreFoundation的一个薄包装。我的论点很简单 - 当CF *对应物可用时,没有必要用全新的NS *类实现重新发明轮子。我很震惊地意识到这两者NSArray也NSMutableArray没有任何共同之处CFArray。
CFArray

关于CFArray最好的事情是它是开源的。这将是一个非常快速的概述,因为源代码是公开可用的,急切地等待阅读。最重要的功能CFArray是_CFArrayReplaceValues。它被称为:

  • CFArrayAppendValue
  • CFArraySetValueAtIndex
  • CFArrayInsertValueAtIndex
  • CFArrayRemoveValueAtIndex
  • CFArrayReplaceValues(注意缺少前导下划线)。
    基本上,CFArray移动内存以最有效的方式适应变化,类似于__NSArrayM其工作方式。但是,CFArray也不能使用循环缓冲区!相反,它有一个较大的缓冲区,从两端填充零,这使得枚举和获取正确的对象更容易。在任一端添加元素只会占用剩余的填充。
    最后的话

尽管CFArray必须提供稍微更为一般的用途,但我觉得它内部的工作原理__NSArrayM并不像以前那样令人着迷。虽然我认为找到共同点并制定单一的规范实施是有意义的,但也许还有一些其他因素会影响这种分离。
这两者有什么共同之处?它们是称为deque的抽象数据类型的具体实现。尽管它的名字,NSMutableArray是一个关于类固醇的阵列,剥夺了C风格对手的缺点。
就个人而言,我最喜欢从任何一端插入/删除的恒定时间性能。我不再需要使用NSMutableArray队列来质疑自己。它工作得非常好。
此文章来自 Bartosz Ciechanowski发表 2014年3月5 日 转载请尊重原作者,原链接为http://ciechanowski.me/blog/2014/03/05/exposing-nsmutablearray/


  1. @\w\s ↩︎

猜你喜欢

转载自blog.csdn.net/weixin_43883776/article/details/84655622