1. 程式人生 > >iOS NSMutableArray 的記憶體管理原理

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 ↩︎