關於NSArray的二三事

在iOS開發中,咱們在很是很是多的地方用到了數組。而關於數組,有不少須要注意和優化的細節,須要咱們潛入到下面,去了解。
如下,是我長時間工做學習中積攢下來的碎片,積攢了足夠多了,就應該拿出來亮一亮了。html

讀書讀出來的問題

前段日子我爲了學習英語,閱讀《Effective Objective-C 2.0》的原版的時候,我發現了以前沒怎麼注意到的一段話:ios

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 = [NSArray alloc];
    NSArray *arr1 = [[NSArray alloc] init];
    NSArray *arr2 = [[NSArray alloc] initWithObjects:@0, nil];
    NSArray *arr3 = [[NSArray alloc] initWithObjects:@0, @1, nil];
    NSArray *arr4 = [[NSArray alloc] 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 = [NSMutableArray alloc];
    NSMutableArray *mArr1 = [[NSMutableArray alloc] init];
    NSMutableArray *mArr2 = [[NSMutableArray alloc] initWithObjects:@0, nil];
    NSMutableArray *mArr3 = [[NSMutableArray alloc] 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));
複製代碼

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

2018-02-25 09:09:15.628381+0800 NSArrayTest[44716:5228210] placeholder: __NSPlaceholderArray
2018-02-25 09:09:15.628749+0800 NSArrayTest[44716:5228210] arr1: __NSArray0
2018-02-25 09:09:15.629535+0800 NSArrayTest[44716:5228210] arr2: __NSSingleObjectArrayI
2018-02-25 09:09:15.630635+0800 NSArrayTest[44716:5228210] arr3: __NSArrayI
2018-02-25 09:09:15.630789+0800 NSArrayTest[44716:5228210] arr4: __NSArrayI
2018-02-25 09:09:15.630993+0800 NSArrayTest[44716:5228210] mPlaceholder: __NSPlaceholderArray
2018-02-25 09:09:15.631095+0800 NSArrayTest[44716:5228210] mArr1: __NSArrayM
2018-02-25 09:09:15.631954+0800 NSArrayTest[44716:5228210] mArr2: __NSArrayM
2018-02-25 09:09:15.632702+0800 NSArrayTest[44716:5228210] mArr3: __NSArrayM
複製代碼

清晰易懂,咱們能夠看到,無論建立的事可變仍是不可變的數組,在alloc以後獲得的類都是 __NSPlaceholderArray 。而當咱們init一個不可變的空數組以後,獲得的是 __NSArray0;若是有且只有一個元素,那就是 __NSSingleObjectArrayI;有多個元素的,叫作 __NSArrayI;init出來一個可變數組的話,都是 __NSArrayM數據結構

咱們看到__NSPlaceholderArray的名字就知道它是用來佔位的。 那它是什麼呢?咱們繼續寫幾行代碼:

NSArray *placeholder1 = [NSArray alloc];
    NSArray *placeholder2 = [NSArray alloc];
    NSLog(@"placeholder1: %p", placeholder1);
    NSLog(@"placeholder2: %p", placeholder2);
複製代碼

打印出來的結果頗有意思多線程

2018-02-25 09:41:45.097431+0800 NSArrayTest[45228:5277101] placeholder1: 0x604000005d90
2018-02-25 09:41:45.097713+0800 NSArrayTest[45228:5277101] placeholder2: 0x604000005d90
複製代碼

這兩個內存地址是同樣的,咱們能夠猜想,這裏是生成了一個單例,在執行init以後就被新的實例給更換掉了。該類內部只有一個isa指針,除此以外沒有別的東西。
因爲蘋果沒有公開此處的源碼,我查閱了別的相似的開源以及資料,獲得以下的結論:併發

一、當元素爲空時,返回的是__NSArray0的單例;
二、當元素僅有一個時,返回的是__NSSingleObjectArrayI的實例;
三、當元素大於一個的時候,返回的是__NSArrayI的實例;
四、網上的資料,大多未說起__NSSingleObjectArrayI,多是後面新增的,理由大概仍是爲了效率,在此不深究。app

爲了區別可變和不可變的狀況,在init的時候,會根據是NSArray 仍是NSMutableArray來建立immutablePlaceholder和mutablePlaceholder,它們都是__NSPlaceholderArray類型的。函數

建立數組

在上面的多種建立數組的方法裏,都是最後調用了initWithObjects:count:函數。

@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property (readonly) NSUInteger count;
- (ObjectType)objectAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObjects:(const ObjectType _Nonnull [_Nullable])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)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 數組的原理。能夠歸結爲一段能被方便讀寫的連續內存空間。數組和指針並不相同 (詳見 Expert C Programming這篇文章),不能說:一塊被 malloc 過的內存空間等同於一個數組 (一種被濫用了的說法)。

使用一段線性內存空間的一個最明顯的缺點是,在下標 0 處插入一個元素時,須要移動其它全部的元素,即 memmove 的原理:

一樣地,假如想要保持相同的內存指針做爲首個元素的地址,移除第一個元素須要進行相同的動做:
當數組很是大時,這樣很快會成爲問題。顯而易見,直接指針存取在數組的世界裏一定不是最高級的抽象。C 風格的數組一般頗有用,但 Obj-C 程序員天天的主要工做使得它們須要 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 (int i = 0;  i < array.count; ++i) {
       id object = array[i];
  }
複製代碼

2.NSEnumerator

NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while((object = [enumerator nextObject])!= nil){

}
複製代碼

3.forin

快速遍歷

NSArray *anArray = /*...*/;
for (id object in anArray) {

  }
複製代碼

4.enumerateObjectsWithOptions:usingBlock:

經過block回調,在子線程中遍歷,對象的回調次序是亂序的,並且調用線程會等待該遍歷過程完成:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];
複製代碼

性能比較如圖

橫軸爲遍歷的對象數目,縱軸爲耗時,單位us. 從圖中看出,在對象數目很小的時候,各類方式的性能差異微乎其微。隨着對象數目的增大, 性能差別才體現出來. 其中for in的耗時一直都是最低的,當對象數高達100萬的時候,for in耗時也沒有超過5ms.
其次是for循環耗時較低.
反而,直覺上應該很是快速的多線程遍歷方式倒是性能最差的。
咱們來看一下內部結構:
1. __NSArrayI __NSArrayI的結構定義爲:

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

複製代碼

_used是數組的元素個數,調用[array count]時,返回的就是_used的值。
這裏咱們能夠把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 id *_list是個循環數組.而且在增刪操做時會動態地從新分配以符合當前的存儲需求.

咱們在上面說過,__NSArrayM 用了環形緩衝區 (circular buffer)。 而且在增刪操做時會動態地從新分配以符合當前的存儲需求.以一個初始包含5個對象,總大小_size爲6的_list爲例: _offset = 0,_used = 5,_size=6

image

在末端追加3個對象後: _offset = 0,_used = 8,_size=8 _list已從新分配

image

刪除對象A: _offset = 1,_used = 7,_size=8

image

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

image

在末端追加兩個對象: _offset = 2,_used = 8,_size=8 _list足夠存儲新加入的兩個對象,所以沒有從新分配,而是將兩個新對象存儲到了_list起始端

image

遍歷的速度特色探究

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 in aDictionary){
    NSString *object = (NSString *)aDictionary[key];
}
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:
  ^(NSString *key,NSString *obj,BOOL *stop){
  
  }];

複製代碼

而且若是須要須要併發的時候,也能夠方便的使用dispatch group。 另外還有一點:若是數組的數量過多,除了block遍歷,其餘的遍歷方法都須要添加autoreleasePool方法來優化。block不須要,由於系統在實現它的時候就已經實現了相關處理。

參考文獻

Effective Objective-C 2.0:編寫高質量iOS與OS X代碼的52個有效方法
NSMutableArray Class Reference
CFArray 的歷史淵源及實現原理
Objective-C 數組遍歷的性能及原理

相關文章
相關標籤/搜索