iOS之集合遍历

在iOS编程中,经常需要列举collection中的元素,当前的Objective-C中有如下几种方法:

  1. 标准的c语言循环
  2. NSEnumerator
  3. fast enumeration
  4. 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.NSFast​Enumeration / NSEnumerator

Author: MrHook
Link: https://bigjar.github.io/2018/01/29/iOS%E4%B9%8B%E9%9B%86%E5%90%88%E9%81%8D%E5%8E%86/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.