1. 程式人生 > >IOS學習 iOS中集合遍歷方法的比較和技巧

IOS學習 iOS中集合遍歷方法的比較和技巧

遍歷的目的是獲取集合中的某個物件或執行某個操作,所以能滿足這個條件的方法都可以作為備選:

經典for迴圈
for in (NSFastEnumeration),若不熟悉可以參考《nshipster介紹NSFastEnumeration的文章
makeObjectsPerformSelector
kvc集合運算子
enumerateObjectsUsingBlock
enumerateObjectsWithOptions(NSEnumerationConcurrent)
dispatch_apply

實驗

實驗條件

測試類如下:

  1. @interface Sark : NSObject  
  2. @property (nonatomic) NSInteger number;  
  3. - (void)doSomethingSlow; // sleep(0.01)
  4. @end 

實驗從兩個方面來評價:

1、分別使用有100個物件和1000000個物件的NSArray,只取物件,不執行操作,測試遍歷速度

2、使用有100個物件的NSArray遍歷執行doSomethingSlow方法,測試遍歷中多工執行速度

實驗使用CFAbsoluteTimeGetCurrent()記錄時間戳來計算執行時間,單位秒。

執行在iphone5真機(雙核cpu)

實驗資料

100物件遍歷操作:

  1. 經典for迴圈 - 0.001355  
  2. forin (NSFastEnumeration) - 0.002308  
  3. makeObjectsPerformSelector - 0.001120  
  4. kvc集合運算子(@sum.number) - 0.004272   
  5. enumerateObjectsUsingBlock - 0.001145  
  6. enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.001605  
  7. dispatch_apply(Concurrent) - 0.001380 

1000000物件遍歷操作:

  1. 經典for迴圈 - 1.246721  
  2. forin (NSFastEnumeration) - 0.025955  
  3. makeObjectsPerformSelector - 0.068234  
  4. kvc集合運算子(@sum.number) - 21.677246  
  5. enumerateObjectsUsingBlock - 0.586034  
  6. enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.722548  
  7. dispatch_apply(Concurrent) - 0.607100 

100物件遍歷執行一個很費時的操作:

  1. 經典for迴圈 - 1.106567  
  2. forin (NSFastEnumeration) - 1.102643  
  3. makeObjectsPerformSelector - 1.103965  
  4. kvc集合運算子(@sum.number) - N/A  
  5. enumerateObjectsUsingBlock - 1.104888  
  6. enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.554670  
  7. 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倒序遍歷,如:

  1. NSArray *strings = @[@"1", @"2", @"3"];  
  2. for (NSString *string in [strings reverseObjectEnumerator]) {  
  3.     NSLog(@"%@", string);  

這個方法只在迴圈第一次被呼叫,所以也不必擔心迴圈每次計算的問題。

同時,使用enumerateObjectsWithOptions:NSEnumerationReverse也可以實現倒序遍歷:

  1. [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) {  
  2.     [sark doSomething];  
  3. }]; 

使用block同時遍歷字典key,value

block版本的字典遍歷可以同時取key和value(forin只能取key再手動取value),如:

  1. NSDictionary *dict = @{@"a": @"1", @"b": @"2"};  
  2. [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {  
  3.     NSLog(@"key: %@, value: %@", key, obj);  
  4. }]; 

對於耗時且順序無關的遍歷,使用併發版本

  1. [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) {  
  2.     [sark doSomethingSlow];  
  3. }]; 

遍歷執行block會分配在多核cpu上執行(底層很可能就是gcd的併發queue),對於耗時的任務來說是很值得這麼做的,而且在以後cpu升級成更多核心後不用改程式碼也可以享受帶來的好處。同時,對於遍歷的外部是保持同步的(遍歷都完成後才繼續執行下一行),猜想內部大概是gcd的dispatch_group或者訊號量控制。

程式碼可讀性和效率的權衡

雖然說上面的測試結果表明,在集合內元素不多時,經典for迴圈的效率要比forin要高,但是從程式碼可讀性上來看,就遠不如forin看著更順暢;同樣的還有kvc的集合運算子,一些內建的操作以keypath的方式宣告,相比自己用for迴圈實現,一行程式碼就能搞定,清楚明瞭,還省去了重複工作;在framework中增加了集合遍歷的block支援後,對於需要index的遍歷再也不需要經典for迴圈的寫法了。

備註:NSSet可以直接取物件並不用遍歷(弱點:唯一值 優點:可以通過hish直接算出地址)(實驗:int,NSNumber,NSString都可以)