IOS學習 iOS中集合遍歷方法的比較和技巧
遍歷的目的是獲取集合中的某個物件或執行某個操作,所以能滿足這個條件的方法都可以作為備選:
經典for迴圈
for in (NSFastEnumeration),若不熟悉可以參考《nshipster介紹NSFastEnumeration的文章》
makeObjectsPerformSelector
kvc集合運算子
enumerateObjectsUsingBlock
enumerateObjectsWithOptions(NSEnumerationConcurrent)
dispatch_apply
實驗
實驗條件
測試類如下:
- @interface Sark : NSObject
- @property (nonatomic) NSInteger number;
- - (void)doSomethingSlow; // sleep(0.01)
- @end
實驗從兩個方面來評價:
1、分別使用有100個物件和1000000個物件的NSArray,只取物件,不執行操作,測試遍歷速度
2、使用有100個物件的NSArray遍歷執行doSomethingSlow方法,測試遍歷中多工執行速度
實驗使用CFAbsoluteTimeGetCurrent()記錄時間戳來計算執行時間,單位秒。
執行在iphone5真機(雙核cpu)
實驗資料
100物件遍歷操作:
- 經典for迴圈 - 0.001355
- forin (NSFastEnumeration) - 0.002308
- makeObjectsPerformSelector - 0.001120
- kvc集合運算子(@sum.number) - 0.004272
- enumerateObjectsUsingBlock - 0.001145
- enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.001605
- dispatch_apply(Concurrent) - 0.001380
1000000物件遍歷操作:
- 經典for迴圈 - 1.246721
- forin (NSFastEnumeration) - 0.025955
- makeObjectsPerformSelector - 0.068234
- kvc集合運算子(@sum.number) - 21.677246
- enumerateObjectsUsingBlock - 0.586034
- enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.722548
- dispatch_apply(Concurrent) - 0.607100
100物件遍歷執行一個很費時的操作:
- 經典for迴圈 - 1.106567
- forin (NSFastEnumeration) - 1.102643
- makeObjectsPerformSelector - 1.103965
- kvc集合運算子(@sum.number) - N/A
- enumerateObjectsUsingBlock - 1.104888
- enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.554670
- dispatch_apply(Concurrent) - 0.554858
值得注意的
1. 對於集合中物件數很多的情況下,for in (NSFastEnumeration)的遍歷速度非常之快,但小規模的遍歷並不明顯(還沒普通for迴圈快)
2. 使用kvc集合運算子運算很大規模的集合時,效率明顯下降(100萬的陣列離譜的21秒多),同時佔用了大量記憶體和cpu
3. enumerateObjectsWithOptions(NSEnumerationConcurrent)和dispatch_apply(Concurrent)的遍歷執行可以利用到多核cpu的優勢(實驗中在雙核cpu上效率基本上x2)
遍歷實踐Tips
倒序遍歷
NSArray和NSOrderedSet都支援使用reverseObjectEnumerator倒序遍歷,如:
- NSArray *strings = @[@"1", @"2", @"3"];
- for (NSString *string in [strings reverseObjectEnumerator]) {
- NSLog(@"%@", string);
- }
這個方法只在迴圈第一次被呼叫,所以也不必擔心迴圈每次計算的問題。
同時,使用enumerateObjectsWithOptions:NSEnumerationReverse也可以實現倒序遍歷:
- [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) {
- [sark doSomething];
- }];
使用block同時遍歷字典key,value
block版本的字典遍歷可以同時取key和value(forin只能取key再手動取value),如:
- NSDictionary *dict = @{@"a": @"1", @"b": @"2"};
- [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
- NSLog(@"key: %@, value: %@", key, obj);
- }];
對於耗時且順序無關的遍歷,使用併發版本
- [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) {
- [sark doSomethingSlow];
- }];
遍歷執行block會分配在多核cpu上執行(底層很可能就是gcd的併發queue),對於耗時的任務來說是很值得這麼做的,而且在以後cpu升級成更多核心後不用改程式碼也可以享受帶來的好處。同時,對於遍歷的外部是保持同步的(遍歷都完成後才繼續執行下一行),猜想內部大概是gcd的dispatch_group或者訊號量控制。
程式碼可讀性和效率的權衡
雖然說上面的測試結果表明,在集合內元素不多時,經典for迴圈的效率要比forin要高,但是從程式碼可讀性上來看,就遠不如forin看著更順暢;同樣的還有kvc的集合運算子,一些內建的操作以keypath的方式宣告,相比自己用for迴圈實現,一行程式碼就能搞定,清楚明瞭,還省去了重複工作;在framework中增加了集合遍歷的block支援後,對於需要index的遍歷再也不需要經典for迴圈的寫法了。
備註:NSSet可以直接取物件並不用遍歷(弱點:唯一值 優點:可以通過hish直接算出地址)(實驗:int,NSNumber,NSString都可以)