數組的遍歷,這個話題貌似沒什么好探究的,該怎么遍歷就怎么遍歷唄!但是如果要回答這些問題:
OC數組有哪幾種遍歷方式?
哪種方式效率最高?為什么?
各種遍歷方式的內部實現是怎么樣的?
NS(Mutable)Array的內部結構是怎么樣的?
我覺得還是需要探究一下.
一.OC數組的類體系
當我們創建一個NSArray對象時,實際上得到的是NSArray的子類__NSArrayI
對象.同樣的,我們創建NSMutableArray
對象,得到的同樣是其子類__NSArrayM
對象.有趣的是,當我們創建只有一個對象的NSArray
時,得到的是__NSSingleObjectArrayI
類對象.
__NSArrayI
和__NSArrayM
,__NSSingleObjectArrayI
為框架隱藏的類.
OC數組的類體系如下:
通過NSArray和NSMutableArray接口,返回的卻是子類對象,怎么做到的?
先介紹另一個私有類:
__NSPlaceholderArray
,和兩個此類的全局變量___immutablePlaceholderArray
,___mutablePlaceholderArray
。__NSPlaceholderArray
從類命名上看,它只是用來占位的,具體怎么占位法稍后討論,有個重要特點是,__NSPlaceholderArray
實現了和NSArray
,NSMutableArray
一摸一樣的初始化方法,如initWithObjects:count:
,initWithCapacity:
等.
介紹完__NSPlaceholderArray
后,這個機制可以總結為以下兩個大步驟:
(1).NSArray重寫了+ (id)allocWithZone:(struct _NSZone *)zone
方法,在方法內部,如果調用類為NSArray
則直接返回全局變量___immutablePlaceholderArray
,如果調用類為NSMUtableArray
則直接返回全局變量___mutablePlaceholderArray
。
也就是調用[NSArray alloc]
或者[NSMUtableArray alloc]
得到的僅僅是兩個占位指針,類型為__NSPlaceholderArray
.
(2).在調用了alloc
的基礎上,不論是NSArray
或NSMutableArray
都必定要繼續調用某個initXXX
方法,而實際上調用的是__NSPlaceholderArray
的initXXX
.在這個initXXX
方法內部,如果self == ___immutablePlaceholderArray
就會重新構造并返回__NSArrayI
對象,如果self == ___mutablePlaceholderArray
就會重新構造并返回_NSArrayM
對象.
總結來說,對于NSArray
和NSMutableArray
,alloc
時拿到的僅僅是個占位對象,init
后才得到真實的子類對象.
接下來清點一下幾種遍歷方式:
二.OC數組遍歷的幾種方式
1.for 循環
for (NSUInteger i = 0; i < array.count; ++i) {
object = array[i];
}
array[i]
會被編譯器轉化為對- (ObjectType)objectAtIndexedSubscript:(NSUInteger)index
的調用,此方法內部調用的就是- (ObjectType)objectAtIndex:(NSUInteger)index
方法.
2.for in
for (id obj in array) {
xxx
}
文章稍后會討論到for in的內部實現
3.enumerateObjectsUsingBlock
通過block回調順序遍歷:
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
4.enumerateObjectsWithOptions:usingBlock:
通過block回調,在子線程中遍歷,對象的回調次序是亂序的,而且調用線程會等待該遍歷過程完成:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
通過block回調,在主線程中逆序遍歷:
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
5.objectEnumerator/reverseObjectEnumerator
通過Enumerator順序遍歷:
NSEnumerator *enumerator = array.objectEnumerator;
while((object = enumerator.nextObject)) {
xxx
}
通過ReverseEnumerator逆序遍歷:
NSEnumerator *enumerator = array.reverseObjectEnumerator;
while((object = enumerator.nextObject)) {
xxx
}
6.enumerateObjectsAtIndexes:options:usingBlock:
通過block回調,在子線程中對指定IndexSet遍歷,對象的回調次序是亂序的,而且調用線程會等待該遍歷過程完成:
[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
通過block回調,在主線程中對指定IndexSet逆序遍歷:
[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
三.性能比較
以100為步長,構造對象數目在0-100萬之間的NSArray
, 分別用上述的遍歷方式進行遍歷并計時(單位us),而且在每一次遍歷中,僅僅只是得到對象,沒有其他任何輸入輸出,計算之類的干擾操作。每種遍歷方式采集得1萬組數據,得到如下的性能對比結果:
橫軸為遍歷的對象數目,縱軸為耗時,單位us.
從圖中看出,在對象數目很小的時候,各種方式的性能差別微乎其微。隨著對象數目的增大, 性能差異才體現出來.
其中for in
的耗時一直都是最低的,當對象數高達100萬的時候,for in
耗時也沒有超過5ms.
其次是for循環耗時較低.
反而,直覺上應該非??焖俚亩嗑€程遍歷方式:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
xxx
}];
卻是性能最差的。
enumerateObjectsUsingBlock :
和reverseObjectEnumerator
的遍歷性能非常相近.
為什么會有這樣的結果,文章稍后會從各種遍歷的內部實現來分析原因。
四.OC數組的內部結構
NSArray
和NSMutableArray
都沒有定義實例變量,只是定義和實現了接口,且對內部數據操作的接口都是在各個子類中實現的.所以真正需要了解的是子類結構,了解了__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
,正如__NSArrayI
的id _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 in就是基于快速枚舉實現的,但是先不討論for in,先認識一個協議:NSFastEnumeration
,它的定義在Foundation
框架的NSFastEnumeration .h
頭文件中:
@protocol NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
@end
NSFastEnumerationState
定義:
typedef struct {
unsigned long state;
id __unsafe_unretained _Nullable * _Nullable itemsPtr;
unsigned long * _Nullable mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;
看了這些定義和蘋果文檔,我也不知道究竟怎么用這個方法,它怎么就叫快速枚舉了呢,除非知道它的實現細節,否則用的時候疑惑太多了.因此我們就先不管怎么用,而是來看看它的實現細節.
__NSArrayI
,__NSArrayM
,__NSSingleObjectArrayI
都實現了NSFastEnumeration
協議.
(1) __NSArrayI的實現:
根據匯編反寫可以得到:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
if (!buffer && len > 0) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
if (len >= 0x40000000) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
static const unsigned long mu = 0x01000000;
if (state->state == 0) {
state->mutationsPtr = μ
state->state = ~0;
state->itemsPtr = _list;
return _used;
}
return 0;
}
可見在__NSArrayI
對這個方法的實現中,主要做的事就是把__NSArrayI
的內部數組_list
賦給state->itemsPtr
,并返回_used
即數組大小.state->mutationsPtr
指向一個局部靜態變量,state->state
看起來是一個標志,如果再次用同一個state
調用這個方法就直接返回0了.
至于傳入的buffer
,len
僅僅只是用來判斷了一下參數合理性。
看來有點明白快速枚舉的意思了,這一下就把全部對象獲取到了,而且在一個c數組里,之后要獲得哪個位置的對象都可以快速尋址到,調用方通過state->itemsPtr
來訪問這個數組,通過返回值來確定數組里對象數目.
例如遍歷一個NSArray
可以這樣:
NSFastEnumerationState state = {0};
NSArray *array = @[@1,@2,@3];
id buffer[2];
//buffer 實際上內部沒有用上,但還是得傳, 2表示我期望得到2個對象,實際上返回的是全部對象數3
NSUInteger n = [array countByEnumeratingWithState:&state objects:buffer count:2];
for (NSUInteger i=0; i<n; ++i) {
NSLog(@"%@", (__bridge NSNumber *)state.itemsPtr[i]);
}
看來之所以叫快速遍歷,是因為這種方式直接從c數組里取對象,不用調用別的方法,所以快速.
__NSSingleObjectArrayI
的實現也猜得出了,在此就不貼代碼了.我們來看看__NSArrayM
是怎么實現這個協議的.
(2) __NSArrayM的實現:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
if (!buffer && len > 0) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
if (len >= 0x40000000) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
if (state->state != ~0) {
if (state->state == 0) {
state->mutationsPtr = &_mutations;
//找到_list中元素起始的位置
state->itemsPtr = _list + _offset;
if (_offset + _used <= _size) {
//必定沒有剩余元素
//標示遍歷完成
state->state = ~0;
return _used;
}
else {
//有剩余元素(_list是個循環數組,剩余元素在_list從起始位置開始存儲)
//state->state存放剩余元素數目
state->state = _offset + _used - _size;
//返回本次得到的元素數目 (總數 - 剩余)
return _used - state->state;
}
}
else {
//得到剩余元素指針
state->itemsPtr = _list;
unsigned long left = state->state;
//標示遍歷完成了
state->state = ~0;
return left;
}
}
return 0;
}
從實現看出,對于__NSArrayM
,用快速枚舉的方式最多只要兩次就可以獲取全部元素. 如果_list
還沒有構成循環,那么第一次就獲得了全部元素,跟__NSArrayI
一樣。但是如果_list
構成了循環,那么就需要兩次,第一次獲取_offset
到_list
末端的元素,第二次獲取存放在_list
起始處的剩余元素.
2.for in的實現
如前面性能比較一節提到的,for in的性能是最好的,可以猜測for in基于應該就是剛剛討論的快速枚舉。
如下代碼:
NSArray *arr = @[@1,@2,@3];
for (id obj in arr) {
NSLog(@"obj = %@",obj);
}
通過clang -rewrite-objc main.m
命令看看編譯器把for in變成了什么:
//NSArray *arr = @[@1,@2,@3];
NSArray *arr = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 3)).arr, 3U);
{
//for (id obj in arr) obj的定義
id obj;
//NSFastEnumerationState
struct __objcFastEnumerationState enumState = { 0 };
//buffer
id __rw_items[16];
id l_collection = (id) arr;
//第一次遍歷,調用countByEnumeratingWithState:objects:count:快速枚舉方法
_WIN_NSUInteger limit =
((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
if (limit) {
//保存初次得到的enumState.mutationsPtr的值
unsigned long startMutations = *enumState.mutationsPtr;
do {
unsigned long counter = 0;
do {
//在獲取enumState.itemsPtr中每個元素前,都檢查一遍enumState.mutationsPtr所指標志是否改變,改變則拋出異常
//對__NSArrayI,enumState.mutationsPtr指向一個靜態局部變量,永遠也不會拋異常
//對__NSArrayM,enumState.mutationsPtr指向_mutations變量, 每次增刪操作后,_mutations會+1
if (startMutations != *enumState.mutationsPtr)
objc_enumerationMutation(l_collection);
//獲取每一個obj
obj = (id)enumState.itemsPtr[counter++]; {
//NSLog(@"obj = %@",obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rg_wm9xjmyn1kz01_pph_34xcqc0000gn_T_main_c95c5d_mi_8,obj);
};
__continue_label_2: ;
} while (counter < limit);
//再一次遍歷,獲取剩余元素
} while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
//遍歷完成
obj = ((id)0);
__break_label_2: ;
}
//沒有元素,空數組
else
obj = ((id)0);
}
可見,for in就是基于快速枚舉實現的,編譯器將for in轉化為兩層循環,外層調用快速枚舉方法批量獲取元素,內層通過c數組取得一批元素中的每一個,并且在每次獲取元素前,檢查是否對數組對象進行了變更操作,如果是,則拋出異常.
3.enumerateObjectsUsingBlock:
該方法在NSArray
中實現,所有子類對象調用的都是這個實現
- (void)enumerateObjectsUsingBlock:(void ( ^)(id obj, NSUInteger idx, BOOL *stop))block {
if (!block) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[NSArray enumerateObjectsUsingBlock:]");
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
[self enumerateObjectsWithOptions:0 usingBlock:block];
}
內部直接以option = 0
調用了enumerateObjectsWithOptions: usingBlock:
4. enumerateObjectsWithOptions: usingBlock:
(1)__NSArrayI
的實現
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id _Nonnull, NSUInteger, BOOL * _Nonnull))block {
if (!block) {
CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[__NSArrayI enumerateObjectsWithOptions:usingBlock:]");
CFAutorelease(errorString);
[[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
}
__block BOOL stoped = NO;
void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
if(!stoped) {
@autoreleasepool {
block(_list[idx],idx,&stoped);
}
}
};
if (opts == NSEnumerationConcurrent) {
dispatch_apply(_used, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), enumBlock);
}
else if(opts == NSEnumerationReverse) {
for (NSUInteger idx = _used - 1; idx != (NSUInteger)-1 && !stoped; idx--) {
enumBlock(idx);
}
}
//opts == 0
else {
if(_used > 0) {
for (NSUInteger idx = 0; idx != _used - 1 && !stoped; idx++) {
enumBlock(idx);
}
}
}
}
(1)__NSArrayM
的實現
__NSArrayM
的實現唯一不同的是enumBlock
void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
if(!stoped) {
@autoreleasepool {
NSUInteger idx_ok = _offset + idx;
//idx對應元素在_list起始處(循環部分)
if (idx_ok >= _size) {
idx_ok -= _size;
}
block(_list[idx_ok],idx,&stoped);
}
}
};
5.objectEnumerator/reverseObjectEnumerator
通過array.objectEnumerator
得到的是一個__NSFastEnumerationEnumerator
私有類對象,在這個enumerator
對象上每次調用- (id)nextObject
時,實際上內部每次都調用的是array
的快速枚舉方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len
只不過每次只獲取并返回一個元素.
而通過array.reverseObjectEnumerator
得到的是一個__NSArrayReverseEnumerator
私有類對象,在這個enumerator
對象上每次調用- (id)nextObject
時,內部直接調用是:objectAtIndex:
來返回對象.
6.enumerateObjectsAtIndexes:options:usingBlock:
由于時間關系后面再貼了.
6.總結
到此,應該可以回答文章開頭提到的幾個問題了.
關于性能的差異:
for in
之所以快,是因為它基于快速枚舉,對NSArray
只要一次快速枚舉調用就可以獲取到包含全部元素的c數組,對NSMUtableArray
最多兩次就可以全部獲取。
for
之所以比 for in
稍慢,僅僅是因為它函數調用開銷的問題,相對于for in
直接從c數組取每個元素的方式,for
靠的是每次調用objectAtIndex:
。
而NSEnumerationConcurrent+Block
的方式耗時最大,我認為是因為它采用了多線程,就這個方法來講,多線程的優勢并不在于遍歷有多快,而是在于它的回調在各個子線程,如果有遍歷+分別耗時計算
的場景,這個方法應該是最適合的,只是此處只測遍歷速度,它光啟動分發管理線程就耗時不少,所以性能落后了.
希望通過此文能對你有幫助.