在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是CoreFoundation中的,和Foundation中的NSArray相對應,他們是Toll-Free Bridged的。經過閱讀 ibireme的這篇博客,咱們能夠知道,CFArray最開始是使用雙端隊列實現的,可是由於性能問題,後來發生了改變,由於沒有開源代碼,ibireme只能經過測試來猜想它可能換成圓形緩衝區來實現了(可是如今能夠肯定仍是雙端隊列)。
任何典型的程序員都知道 C 數組的原理。能夠歸結爲一段能被方便讀寫的連續內存空間。數組和指針並不相同 (詳見 Expert C Programming 或 這篇文章),不能說:一塊被 malloc 過的內存空間等同於一個數組 (一種被濫用了的說法)。
使用一段線性內存空間的一個最明顯的缺點是,在下標 0 處插入一個元素時,須要移動其它全部的元素,即 memmove 的原理:
一樣地,假如想要保持相同的內存指針做爲首個元素的地址,移除第一個元素須要進行相同的動做: 當數組很是大時,這樣很快會成爲問題。顯而易見,直接指針存取在數組的世界裏一定不是最高級的抽象。C 風格的數組一般頗有用,但 Obj-C 程序員天天的主要工做使得它們須要 NSMutableArray 這樣一個可變的、可索引的容器。 這裏,咱們須要閱讀 這篇博客。在這裏咱們能夠肯定使用了環形緩衝區。 正如你會猜想的, __NSArrayM 用了 環形緩衝區 (circular buffer)。這個數據結構至關簡單,只是比常規數組或緩衝區複雜點。環形緩衝區的內容能在到達任意一端時繞向另外一端。環形緩衝區有一些很是酷的屬性。尤爲是,除非緩衝區滿了,不然在任意一端插入或刪除均不會要求移動任何內存。咱們來分析這個類如何充分利用環形緩衝區來使得自身比 C 數組強大得多。
咱們在這裏知道了幾個有趣的東西: 在刪除的時候不會清除指針。 最有意思的一點,若是咱們在中間進行插入或者刪除,只會移動最少的一邊的元素。
絕不意外的是,__NSArrayM 履行了這個規定。然而,__NSArrayM 的全部實現方法列表至關短且不包含 21 個額外的在 NSMutableArray 頭文件列出來的方法。誰負責執行這些方法呢?
這證實它們只是 NSMutableArray 類自身的一部分。這會至關的方便:任何 NSMutableArray 的子類只須實現 7 個最基本的方法。全部其它高等級的抽象創建在它們的基礎之上。例如 - removeAllObjects 方法簡單地往回迭代,一個個地調用 - removeObjectAtIndex:。
for (int i = 0; i < array.count; ++i) {
id object = array[i];
}
複製代碼
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while((object = [enumerator nextObject])!= nil){
}
複製代碼
快速遍歷
NSArray *anArray = /*...*/;
for (id object in anArray) {
}
複製代碼
經過block回調,在子線程中遍歷,對象的回調次序是亂序的,並且調用線程會等待該遍歷過程完成:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
複製代碼
性能比較如圖
橫軸爲遍歷的對象數目,縱軸爲耗時,單位us. 從圖中看出,在對象數目很小的時候,各類方式的性能差異微乎其微。隨着對象數目的增大, 性能差別才體現出來. 其中for in的耗時一直都是最低的,當對象數高達100萬的時候,for in耗時也沒有超過5ms.@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
,正如__NSArrayI
的id _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
在末端追加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
起始端
這兩個速度是最快的,咱們就以forin爲例。forin聽從了NSFastEnumeration
協議,它只有一個方法:
- (NSUInteger)countByEnumeratingWithState:
(NSFastEnumerationState *)state
objects:(id *)stackbuffer
count:(NSUInteger)len;
複製代碼
它直接從C數組中取對象。對於可變數組來講,它最多隻須要兩次就能夠獲取所有全速。若是數組尚未構成循環,那麼第一次就得到了所有元素,跟不可變數組同樣。可是若是數組構成了循環,那麼就須要兩次,第一次獲取對象數組的起始偏移到循環數組末端的元素,第二次獲取存放在循環數組起始處的剩餘元素。
而for循環之因此慢一點,是由於for循環的時候每次都要調用objectAtIndex:
假如咱們遍歷的時候不須要獲取當前遍歷操做所針對的下標,咱們就能夠選擇forin。
這種循環雖然是最慢的,可是咱們在遍歷的時候能夠直接從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 數組遍歷的性能及原理