1. 程式人生 > >iOS NSArray 、NSMutableArray原理揭露

iOS NSArray 、NSMutableArray原理揭露

在iOS開發中,我們在非常非常多的地方用到了陣列。而關於陣列,有很多需要注意和優化的細節,需要我們潛入到下面,去了解。

閱讀《Effective Objective-C 2.0》的原版的時候,我發現了之前沒怎麼注意到的一段話:

In the case of NSArray, when an instance is allocated, it’s an instance of another class that’s allocated (during a call to alloc), known as a placeholder array. This placeholder array is then converted to an instance of another class, which is a concrete subclass of NSArray.

在使用了NSArray的alloc方法來獲取例項時,該方法首先會分類一個屬於某類的例項,此例項充當“佔位陣列”。該陣列稍後會轉為另一個類的例項,而那個類則是NSArray的實體子類。

話不多說,程式碼寫兩行:

NSArray*placeholder = [ NSArrayalloc]; 

NSArray*arr1 = [[ NSArrayalloc] init]; 

NSArray*arr2 = [[ NSArrayalloc] initWithObjects:@ 0, nil]; 

NSArray*arr3 = [[ NSArrayalloc] initWithObjects:@ 0, @ 1, nil]; 

NSArray*arr4 = [[ NSArrayalloc] initWithObjects:@ 0, @ 1, @ 2, nil]; 

NSLog( @"placeholder: %s", object_getClassName(placeholder)); 

NSLog( @"arr1: %s", object_getClassName(arr1)); 

NSLog( @"arr2: %s", object_getClassName(arr2)); 

NSLog( @"arr3: %s", object_getClassName(arr3)); 

NSLog( @"arr4: %s", object_getClassName(arr4)); 

NSMutableArray*mPlaceholder = [ NSMutableArrayalloc]; 

NSMutableArray*mArr1 = [[ NSMutableArrayalloc] init]; 

NSMutableArray*mArr2 = [[ NSMutableArrayalloc] initWithObjects:@ 0, nil]; 

NSMutableArray*mArr3 = [[ NSMutableArrayalloc] initWithObjects:@ 0, @ 1, nil]; 

NSLog( @"mPlaceholder: %s", object_getClassName(mPlaceholder)); 

NSLog( @"mArr1: %s", object_getClassName(mArr1)); 

NSLog( @"mArr2: %s", object_getClassName(mArr2)); 

NSLog( @"mArr3: %s", object_getClassName(mArr3)); 

打印出來的結果是這樣的:

2018 -02-2509 :09:15.628381+0800NSArrayTest[44716:5228210]placeholder: __ NSPlaceholderArray

2018 -02-2509 :09:15.628749+0800NSArrayTest[44716:5228210]arr1: __ NSArray0

2018 -02-2509 :09:15.629535+0800NSArrayTest[44716:5228210]arr2: __ NSSingleObjectArrayI

2018 -02-2509 :09:15.630635+0800NSArrayTest[44716:5228210]arr3: __ NSArrayI

2018 -02-2509 :09:15.630789+0800NSArrayTest[44716:5228210]arr4: __ NSArrayI

2018 -02-2509 :09:15.630993+0800NSArrayTest[44716:5228210]mPlaceholder: __ NSPlaceholderArray

2018 -02-2509 :09:15.631095+0800NSArrayTest[44716:5228210]mArr1: __ NSArrayM

2018 -02-2509 :09:15.631954+0800NSArrayTest[44716:5228210]mArr2: __ NSArrayM

2018 -02-2509 :09:15.632702+0800NSArrayTest[44716:5228210]mArr3: __ NSArrayM

清晰易懂,我們可以看到,不管建立的事可變還是不可變的陣列,在alloc之後得到的類都是 __NSPlaceholderArray。而當我們init一個不可變的空陣列之後,得到的是**__NSArray0**;如果有且只有一個元素,那就是 __NSSingleObjectArrayI;有多個元素的,叫做 __NSArrayI;init出來一個可變陣列的話,都是 __NSArrayM。

我們看到__NSPlaceholderArray的名字就知道它是用來佔位的。

那它是什麼呢?我們繼續寫幾行程式碼:

NSArray*placeholder1 = [ NSArrayalloc]; 

NSArray*placeholder2 = [ NSArrayalloc]; 

NSLog( @"placeholder1: %p", placeholder1); 

NSLog( @"placeholder2: %p", placeholder2); 

打印出來的結果很有意思

2018 -02-2509 :41:45.097431+0800NSArrayTest[45228:5277101]placeholder1: 0 x604000005d90

2018 -02-2509 :41:45.097713+0800NSArrayTest[45228:5277101]placeholder2: 0 x604000005d90

這兩個記憶體地址是一樣的,我們可以猜測,這裡是生成了一個單例,在執行init之後就被新的例項給更換掉了。該類內部只有一個isa指標,除此之外沒有別的東西。

由於蘋果沒有公開此處的原始碼,我查閱了別的類似的開源以及資料,得到如下的結論:

  1. 當元素為空時,返回的是__NSArray0的單例;
  2. 當元素僅有一個時,返回的是__NSSingleObjectArrayI的例項
  3. 當元素大於一個的時候,返回的是__NSArrayI的例項;
  4. 網上的資料,大多未提及__NSSingleObjectArrayI,可能是後面新增的,理由大概還是為了效率,在此不深究。

為了區別可變和不可變的情況,在init的時候,會根據是NSArray還是NSMutableArray來建立immutablePlaceholder和mutablePlaceholder,它們都是__NSPlaceholderArray型別的。

建立陣列

在上面的多種建立陣列的方法裡,都是最後呼叫了initWithObjects:count:函式。

@interfaceNSArray<__covariantObjectType> : NSObject<NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property( readonly) NSUIntegercount; 

- (ObjectType)objectAtIndex:( NSUInteger)index; 

- ( instancetype)init NS_DESIGNATED_INITIALIZER; 

- ( instancetype)initWithObjects:( constObjectType _Nonnull [_Nullable])objects count:( NSUInteger)cnt NS_DESIGNATED_INITIALIZER; 

- ( nullableinstancetype)initWithCoder:( NSCoder*)aDecoder NS_DESIGNATED_INITIALIZER; 

@end

這就是類族的優點,在建立某個類族的子類的時候,我們不需要實現所有的功能。在CoreFoundation的類蔟的抽象工廠基類(如NSArray、NSString、NSNumber等)中,Primitive methods指的就是這些核心的方法,也就是那些在建立子類時必須要重寫的方法,通常在類的interface中宣告,在文件中一般也會說明。其他可選實現的方法在Category中宣告。同時還需要注意其整個繼承樹的祖先的Primitive methods也都需要實現。

CFArray和NSMutableArray

CFArray是CoreFoundation中的,和Foundation中的NSArray相對應,他們是Toll-Free Bridged的。通過閱讀 ibireme的這篇部落格,我們可以知道,CFArray最開始是使用雙端佇列實現的,但是因為效能問題,後來發生了改變,因為沒有開原始碼,ibireme只能通過測試來猜測它可能換成圓形緩衝區來實現了(但是現在可以確定還是雙端佇列)。

任何典型的程式設計師都知道 C 陣列的原理。可以歸結為一段能被方便讀寫的連續記憶體空間。陣列和指標並不相同 ,不能說:一塊被 malloc過的記憶體空間等同於一個數組 (一種被濫用了的說法)。

使用一段線性記憶體空間的一個最明顯的缺點是,在下標 0 處插入一個元素時,需要移動其它所有的元素,即 memmove的原理:

同樣地,假如想要保持相同的記憶體指標作為首個元素的地址,移除第一個元素需要進行相同的動作:

當陣列非常大時,這樣很快會成為問題。顯而易見,直接指標存取在陣列的世界裡必定不是最高階的抽象。C 風格的陣列通常很有用,但 Obj-C 程式設計師每天的主要工作使得它們需要 NSMutableArray 這樣一個可變的、可索引的容器。這裡,我們需要閱讀這篇部落格(http://ciechanowski.me/blog/2014/03/05/exposing-nsmutablearray/)。在這裡我們可以確定使用了環形緩衝區。正如你會猜測的,__NSArrayM用了環形緩衝區 (circular buffer)。這個資料結構相當簡單,只是比常規陣列或緩衝區複雜點。環形緩衝區的內容能在到達任意一端時繞向另一端。

環形緩衝區有一些非常酷的屬性。尤其是,除非緩衝區滿了,否則在任意一端插入或刪除均不會要求移動任何記憶體。我們來分析這個類如何充分利用環形緩衝區來使得自身比 C 陣列強大得多。我們在這裡知道了幾個有趣的東西:在刪除的時候不會清除指標。最有意思的一點,如果我們在中間進行插入或者刪除,只會移動最少的一邊的元素。

NSMutableArray的方法

正如 NSMutableArray Class Reference 的討論,每個 NSMutableArray 子類必須實現下面 7 個方法:

  • count
  • objectAtIndex:
  • insertObject:atIndex:
  • removeObjectAtIndex:
  • addObject:
  • removeLastObject
  • replaceObjectAtIndex:withObject:

毫不意外的是,__NSArrayM 履行了這個規定。然而,__NSArrayM 的所有實現方法列表相當短且不包含 21 個額外的在 NSMutableArray 標頭檔案列出來的方法。誰負責執行這些方法呢?

這證明它們只是 NSMutableArray 類自身的一部分。這會相當的方便:任何 NSMutableArray 的子類只須實現 7 個最基本的方法。所有其它高等級的抽象建立在它們的基礎之上。例如 - removeAllObjects 方法簡單地往回迭代,一個個地呼叫 - removeObjectAtIndex:。

遍歷陣列的n個方法

1.for 迴圈

for( inti = 0; i < array.count; ++i) { 

id object = array[i]; 

2.NSEnumerator

NSArray*anArray = /*...*/; 

NSEnumerator*enumerator = [anArray objectEnumerator]; 

idobject; 

while((object = [enumerator nextObject])!= nil){ 

3.forin

快速遍歷 

NSArray*anArray = /*...*/; 

for( idobject inanArray) { 

4.enumerateObjectsWithOptions:usingBlock:

通過block回撥,在子執行緒中遍歷,物件的回撥次序是亂序的,而且呼叫執行緒會等待該遍歷過程完成:

[array enumerateObjectsWithOptions: NSEnumerationConcurrent

usingBlock:^( id_Nonnull obj, NSUIntegeridx, BOOL* _Nonnull stop) { 

xxx 

}]; 

效能比較如圖

橫軸為遍歷的物件數目,縱軸為耗時,單位us.從圖中看出,在物件數目很小的時候,各種方式的效能差別微乎其微。隨著物件數目的增大, 效能差異才體現出來.其中for in的耗時一直都是最低的,當物件數高達100萬的時候,for in耗時也沒有超過5ms.

其次是for迴圈耗時較低.反而,直覺上應該非常快速的多執行緒遍歷方式卻是效能最差的。

我們來看一下陣列的內部結構:

NSArrayNSMutableArray都沒有定義例項變數,只是定義和實現了介面,且對內部資料操作的介面都是在各個子類中實現的.所以真正需要了解的是子類結構,瞭解了__NSArrayI就相當於瞭解NSArray,瞭解了__NSArrayM就相當於瞭解NSMutableArray.1. __NSArrayI __NSArrayI的結構定義為:

@interface __NSArrayI : NSArray
{
    NSUInteger _used;
    id _list[0];
}
@end

_used是陣列的元素個數,呼叫[array count]時,返回的就是_used的值。id _list[0]是陣列內部實際儲存物件的陣列,但為何定義為0長度呢?這裡有一篇關於0長度陣列的文章:http://blog.csdn.net/zhaqiwen/article/details/7904515 這裡我們可以把id _list[0]當作id *_list來用,即一個儲存id物件的buff. 由於__NSArrayI的不可變,所以_list一旦分配,釋放之前都不會再有移動刪除操作了,只有獲取物件一種操作.因此__NSArrayI的實現並不複雜.2. __NSSingleObjectArrayI __NSSingleObjectArrayI的結構定義為:

@interface __NSSingleObjectArrayI : NSArray
{
    id object;
}
@end

因為只有在"建立只包含一個物件的不可變陣列"時,才會得到__NSSingleObjectArrayI物件,所以其內部結構更加簡單,一個object足矣.3. __NSArrayM __NSArrayM的結構定義為:

@interface __NSArrayM : NSMutableArray
{
    NSUInteger _used;
    NSUInteger _offset;
    int _size:28;
    int _unused:4;
    uint32_t _mutations;
    id *_list;
}
@end

__NSArrayM稍微複雜一些,但是同樣的,它的內部物件陣列也是一塊連續記憶體id* _list,正如__NSArrayIid _list[0]一樣_used:當前物件數目_offset:實際物件陣列的起始偏移,這個欄位的用處稍後會討論_size:已分配的_list大小(能儲存的物件個數,不是位元組數)_mutations:修改標記,每次對__NSArrayM的修改操作都會使_mutations加1,“*** Collection <__NSArrayM: 0x1002076b0> was mutated while being enumerated.”這個異常就是通過對_mutations的識別來引發的

id *_list是個迴圈陣列.並且在增刪操作時會動態地重新分配以符合當前的儲存需求.以一個初始包含5個物件,總大小_size為6的_list為例:_offset = 0,_used = 5,_size=6

在末端追加3個物件後:_offset = 0,_used = 8,_size=8_list已重新分配

刪除物件A:_offset = 1,_used = 7,_size=8

刪除物件E:_offset = 2,_used = 6,_size=8 B,C往後移動了,E的空缺被填補

在末端追加兩個物件:_offset = 2,_used = 8,_size=8_list足夠儲存新加入的兩個物件,因此沒有重新分配,而是將兩個新物件儲存到了_list起始端 因此可見,__NSArrayM_list是個迴圈陣列,它的起始由_offset標識.  

遍歷的速度特點探究

1.for 迴圈&for in

這兩個速度是最快的,我們就以forin為例。forin遵從了NSFastEnumeration協議,它只有一個方法:

- ( NSUInteger)countByEnumeratingWithState: 

( NSFastEnumerationState*)state 

objects:( id*)stackbuffer 

count:( NSUInteger)len; 

它直接從C陣列中取物件。對於可變陣列來說,它最多隻需要兩次就可以獲取全部全速。如果陣列還沒有構成迴圈,那麼第一次就獲得了全部元素,跟不可變陣列一樣。但是如果陣列構成了迴圈,那麼就需要兩次,第一次獲取物件陣列的起始偏移到迴圈陣列末端的元素,第二次獲取存放在迴圈陣列起始處的剩餘元素。而for迴圈之所以慢一點,是因為for迴圈的時候每次都要呼叫objectAtIndex:假如我們遍歷的時候不需要獲取當前遍歷操作所針對的下標,我們就可以選擇forin。

2.block迴圈

這種迴圈雖然是最慢的,但是我們在遍歷的時候可以直接從block中獲取更多的資訊,並且可以修改塊的方法簽名,以免進行型別轉換操作。

for( NSString*key inaDictionary){ 

NSString*object = ( NSString*)aDictionary[key]; 

NSDictionary*aDictionary = /*...*/; 

[aDictionary enumerateKeysAndObjectsUsingBlock: 

^( NSString*key, NSString*obj, BOOL*stop){ 

}]; 

並且如果需要需要併發的時候,也可以方便的使用dispatch group。

另外還有一點:如果陣列的數量過多,除了block遍歷,其他的遍歷方法都需要新增autoreleasePool方法來優化。block不需要,因為系統在實現它的時候就已經實現了相關處理。