在iOS编程中,经常需要列举collection中的元素,当前的Objective-C中有如下几种方法:
- 标准的c语言循环
- NSEnumerator
- fast enumeration
- block enumeration
性能对比
接下来以NSSet为例测试一下4种方法的性能。代码如下:
#import <Foundation/Foundation.h>
#define magicNum 1000000
int main(int argc, const char * argv[]) {
NSMutableSet *mutableSet = [NSMutableSet setWithCapacity:0];
for (int i=1; i<=magicNum; i++) {
NSNumber *temp= [NSNumber numberWithInteger:i];
[mutableSet addObject:temp];
}
NSSet *set = [mutableSet copy];
CFAbsoluteTime startTime1 =CFAbsoluteTimeGetCurrent();
NSArray *tempArr = [set allObjects];
for (int i=0; i < tempArr.count; i++) {
if (![tempArr[i] isEqualToNumber:[NSNumber numberWithInteger:magicNum]]) {
continue;
}
NSLog(@"%@",tempArr[i]);
break;
}
CFAbsoluteTime linkTime1 = (CFAbsoluteTimeGetCurrent() - startTime1);
NSLog(@"Linked1 in %f ms", linkTime1 *1000.0);
CFAbsoluteTime startTime2 =CFAbsoluteTimeGetCurrent();
NSEnumerator *enumerator = [set objectEnumerator];
id obj;
while (obj = [enumerator nextObject]) {
if (![obj isEqualToNumber:[NSNumber numberWithInteger:magicNum]]) {
continue;
}
NSLog(@"%@",obj);
break;
}
CFAbsoluteTime linkTime2 = (CFAbsoluteTimeGetCurrent() - startTime2);
NSLog(@"Linked2 in %f ms", linkTime2 *1000.0);
CFAbsoluteTime startTime3 =CFAbsoluteTimeGetCurrent();
for (NSNumber *number in set) {
if (![number isEqualToNumber:[NSNumber numberWithInteger:magicNum]]) {
continue;
}
NSLog(@"%@",number);
break;
}
CFAbsoluteTime linkTime3 = (CFAbsoluteTimeGetCurrent() - startTime3);
NSLog(@"Linked3 in %f ms", linkTime3 *1000.0);
CFAbsoluteTime startTime4 =CFAbsoluteTimeGetCurrent();
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
if ([obj isEqualToNumber:[NSNumber numberWithInteger:magicNum]]) {
NSLog(@"%@",obj);
*stop = YES;
}
}];
CFAbsoluteTime linkTime4 = (CFAbsoluteTimeGetCurrent() - startTime4);
NSLog(@"Linked4 in %f ms", linkTime4 *1000.0);
return 0;
}
通过测试发现,fast enumeration耗时最少,block enumeration耗时最多。标准c语言循环耗时也比较多,而且对于无序集合,需要先copy到一个数组中,增加了额外开销。而且容易产生off-by-one errors。block enumeration虽然耗时比较多,但是对于需要用到block的地方,这个方法比较便捷。
2017-12-17 19:58:34.006923+0800 FastEnumerationDemo[39073:785059] 1000000
2017-12-17 19:58:34.007785+0800 FastEnumerationDemo[39073:785059] Linked1 in 77.924013 ms
2017-12-17 19:58:34.069683+0800 FastEnumerationDemo[39073:785059] 1000000
2017-12-17 19:58:34.069718+0800 FastEnumerationDemo[39073:785059] Linked2 in 61.906993 ms
2017-12-17 19:58:34.125608+0800 FastEnumerationDemo[39073:785059] 1000000
2017-12-17 19:58:34.125817+0800 FastEnumerationDemo[39073:785059] Linked3 in 56.064010 ms
2017-12-17 19:58:34.210177+0800 FastEnumerationDemo[39073:785059] 1000000
2017-12-17 19:58:34.210214+0800 FastEnumerationDemo[39073:785059] Linked4 in 84.289968 ms
为什么fast enumeration性能最优?###
Objective-C 2.0引入了快速遍历的功能,它为for循环开设了in关键字。只要实现了 NSFastEnumeration protocol的类都能够快速遍历,包括NSDictionary, NSArray, NSSet等。
2.1
NSFastEnumeration只包含了一个方法:
/**
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
@param buffer A C array of objects over which the sender is to iterate.
@param len The maximum number of objects to return in stackbuf.
@discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
@return The number of objects returned in stackbuf. Returns 0 when the iteration is finished.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
NSFastEnumerationState结构体如下:
typedef struct {
unsigned long state;
id *itemsPtr;
unsigned long *mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;
这个方法就是返回一系列C数组,以供调用者进行遍历。C数组需要提供一个数组的指针和数组的长度。数组的长度就是这个函数的返回值,数组的指针就是NSFastEnumerationState中的itemsPtr。
当然,有些数据结构不是连续内存的,这时需要把它拷贝到一个C数组上,就是buffer参数,长度由len决定。
如果集合在遍历的过程中被修改的话,NSFastEnumeration 就会抛出异常。而这个功能就是通过 mutationsPtr 字段来实现的,它指向一个这样的值,这个值在集合被修改时会发现改变。因此,我们就可以利用它来判断集合在遍历的过程中是否被修改。
fast enumeration内部实现
用clang命令将代码重写为C++代码。
clang rewrite-objc main.m
代码如下:
{
NSNumber * number;
struct __objcFastEnumerationState enumState = { 0 };
id __rw_items[16];
id l_collection = (id) set;
_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) {
unsigned long startMutations = *enumState.mutationsPtr;
do {
unsigned long counter = 0;
do {
if (startMutations != *enumState.mutationsPtr)
objc_enumerationMutation(l_collection);
number = (NSNumber *)enumState.itemsPtr[counter++]; {
if (!((BOOL (*)(id, SEL, NSNumber *))(void *)objc_msgSend)((id)number, sel_registerName("isEqualToNumber:"), ((NSNumber *(*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)objc_getClass("NSNumber"), sel_registerName("numberWithInteger:"), (NSInteger)1000000))) {
goto __continue_label_1;
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0c_dkz7xg955s35wk657wvq8y8w0000gn_T_main_af2091_mi_4,number);
goto __break_label_1;
};
__continue_label_1: ;
} 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)));
number = ((NSNumber *)0);
__break_label_1: ;
}
else
number = ((NSNumber *)0);
}
可以看到,里面用到了两个do-while循环。外层负责调用 countByEnumeratingWithState:objects:count:方法,获取C数组。内层则负责遍历C数组。在内层遍历中,使用指针的算术运算获取相应的集合元素,这是快速枚举之所以高效的关键所在。
number = (NSNumber *)enumState.itemsPtr[counter++]
总结
本文首先分析了各种集合遍历的性能,然后对fast enumeration做了性能的内部剖析。
参考
1.Mike Ash has a fantastic blog post
2.Objective-C Fast Enumeration 的实现原理
3.NSFastEnumeration / NSEnumerator