Cocoa編程的一個一般的任務是要去循環遍歷一個對象的集合 (例如,一個 NSArray, NSSet 或者是 NSDictionary). 這個看似簡單的問題有普遍數量的解決方案,它們中的許多不乏有對性能方面問題的細微考慮.objective-c 對於速度的追求首先,是一個免責聲明: 相比其它問題而言,一個 Objective-C 方法原始的速度是你在編程時最後才須要考慮的問題之一 – 區別就在於這個問題夠不上去同其它更加須要重點考慮的問題進行比較,好比說代碼的清晰度和可讀性.編程 但速度的次要性並不妨礙咱們去理解它. 你應該常常去了解一下性能方面的考慮將如何對你正在編寫的代碼產生影響,一邊在極少數發生問題的狀況下,你會知道如何下手.數組 還有,在循環的場景中,大多數時候不論是從可讀性或者是清晰度考慮,你選擇哪一種技術都沒什麼關係的, 因此你還不如選擇速度最快的那一種. 沒有必要選擇編碼速度比要求更慢的。併發 |
考慮到這一點,就有了以下的選擇:oop
經典的循環方式
這是循環遍歷一個數組的一個簡單熟悉的方式; 從性能方面考慮它也至關的差勁. 這段代碼最大的問題就是循環每進行一次咱們都會調用數組的計數方法. 數組的總數是不會改變的,所以每次都去調用一下這種作法是多餘的. 像這種代碼通常C編譯器通常都會優化掉, 可是 Objective-C 的動態語言特性意味着對這個方法的調用不會被自動優化掉. 所以,爲了提高性能,值得咱們在循環開始以前,將這個總數存到一個變量中,像這樣:性能
|
NSEnumeratorNSEnumerator 是循環遍歷集合的一種可選方式. 全部的集合都已一個或者更多個枚舉方法,每次它們被調用的時候都會返回一個NSEnumerator實體. 一個給定的 NSEnumerator 會包含一個指向集合中第一個對象的指針, 而且會有一個 nextObject 方法返回當前的對象並對指針進行增加. 你能夠重複調用它直到它返回nil,這代表已經到了集合的末尾了:測試
NSEnumerator 的性能能夠媲美原生的for循環, 但它更加實用,由於它對索引的概念進行了抽象,這意味着它應用在結構化數據上,好比鏈表,或者甚至是無窮序列和數據流,這些結構中的數據條數未知或者並無被定義.優化 |
快速枚舉快速枚舉是在 Objective-C 2.0 中做爲傳統的NSEnumerator的更便利(而且明顯更快速) 的替代方法而引入的. 它並無使得枚舉類過期由於其仍然被應用於注入反向枚舉, 或者是當你須要對集合進行變動操做 (以後會更多地提到) 這些場景中.編碼 快速枚舉添加了一個看起來像下面這樣子的新的枚舉方法:spa
若是你正在想着「那看起來並不怎麼舒服啊!」, 我不會怪你的. 可是新的方法順便帶來了一種新的循環語法, for…in 循環. 這是在幕後使用了新的枚舉方法, 而且重要的是在語法和性能上都比使用傳統的for循環或者 NSEnumerator 方法都更省心了:
|
枚舉塊隨着塊的誕生,Apple加入第四個基於塊語法的枚舉機制. 這無疑比快速枚舉更加的少見, 可是有一個優點就是對象和索引都會返回, 而其餘的枚舉方法只會返回對象. 枚舉塊的另一個關鍵特性就是可選擇型的併發枚舉 (在幾個併發的線程中枚舉對象). 這不是常常有用,取決於你在本身的循環中具體要作些什麼, 可是在你正有許多工做要作,而且你並不怎麼關心枚舉順序的場景下,它在多核處理器上可能會產生顯著的性能提升 (如今全部的 Mac和iOS設備都已經有了多核處理器). |
基準測試那麼這些方法疊加起來會如何呢, 性能會更加的好麼? 這裏有一個簡單的基準測試命令行應用,比較了使用多種不一樣方法枚舉一個數據的性能. 咱們已經在 ARC 關閉的狀況下運行了它,以排除任何干擾最終結果的隱藏在幕後的保留或者排除處理. 因爲是運行在一個很快的 Mac 機上面, 全部這些方法運行極快以致於咱們實際上不得不使用一個存有10,000,000 (一千萬) 對象的數組來測量結果. 若是你決定在一個 iPhone 進行測試, 最明智的作法是使用一個小得多的數量! 爲了編譯這段代碼:
下面展現出告終果:
|
leoxu
|
忽略掉時間的具體長短. 咱們感興趣的是它們同其它方法比較的相對大小. 若是咱們按順序排列它們,快的放前面,我會獲得了下面的結果:
For…in 是勝出者. 顯然他們將其稱爲快速枚舉是有緣由的! 併發枚舉看起來是比單線程的快一點點, 可是你不必對其作更多的解讀: 咱們這裏是在枚舉一個很是很是大型的對象數組,而對於小一些的數據併發執行的開銷遠多於其帶來的好處. 併發執行的主要是在當你的循環須要大量的執行時間時有優點. 若是你在本身的循環中有許多東西要運行,那就考慮試下並行枚舉,在你不關心枚舉順序的前提下 (可是請用行動的去權衡一下它是否變得更快樂,不要空手去揣度). |
其它集合類型Other Collection Types那麼其它的結合類型怎麼樣呢, 好比 NSSet 和 NSDictionary? NSSet 是無序的, 所以沒有按索引去取對象的概念.咱們也能夠進行一下基準測試:
結果同 NSArray 一致; for…in 再一次勝出了. NSDictionary怎麼樣了? NSDictionary 有一點不一樣由於咱們同時又一個鍵和值對象須要迭代. 在一個字典中單獨迭代鍵或者值是能夠的, 但典型的狀況下咱們二者都須要. 這裏咱們有一段適配於操做NSDictionary的基準測試代碼:
|
NSDictionary 填充起來比 NSArray 或者 NSSet 慢得多, 所以咱們把數據條數減小到了10,000 (一萬) 以免機器鎖住. 於是你應該忽略結果怎麼會比那些 NSArray 低那麼多,由於咱們使用的是更少對象的 1000 次循環:
沒有優化過的循環再這裏慢得很壯觀,由於每一次咱們都複製了鍵數組. 經過把鍵數組和總數存到變量中,咱們得到了更快的速度. 查找對象的消耗如今主宰了其它的因素,所以使用一個for循環, NSEnumerator 或者for…in 差異很小. 可是對於枚舉塊方法而言,它在一個方法中把鍵和值都返回了,因此如今變成了最快的選擇。 |
反轉齒輪基於咱們所見,若是全部其它的因素都同樣的話,在循環遍歷數組時你應該嘗試去使用for...in循環, 而遍歷字典時,則應該選擇枚舉塊. 也有一些場景下這樣的作法並不可能行得通,好比咱們須要回頭來進行枚舉,或者當咱們在遍歷時想要變動集合的狀況. 爲了回過頭來枚舉一個數據,咱們能夠調用reverseObjectEnumerator方法來得到一個NSEnumerator 以從尾至頭遍歷數組. NSEnumerator, 就像是 NSArray 它本身, 支持快速的枚舉協議. 那就意味着咱們仍然能夠在這種方式下使用 for…in, 而無速度和簡潔方面的損失:
(除非你異想天開, NSSet 或者 NSDictionary 是沒有等效的方法的, 而反向枚舉一個 NSSet 或者NSDictionary不管如何都沒啥意義, 由於鍵是無序的.) 若是你想使用枚舉塊的話, NSEnumerationReverse你能夠試試, 像這樣:
|
變動Mutation應用一樣的循環技術到變動中的集合上是可能的; 其性能也大體相同. 然而當你嘗試在循環數組或者字典的時候修改它們,你可能常常會面臨這樣的異常: '*** Collection XYZ was mutated while being enumerated.'
就像咱們優化了的for循環, 全部這些循環技術的性能取決於事先把數據總數存下來,這意味着若是你開始在循環中間加入或者去掉一個數據時,這個數據就不正確了. 可是在循環進行中加入,替換或者移除一條數據時常常想要作的事情. 那麼什麼纔是這個問題的解決之道呢? 咱們經典的for循環能夠工做得很好,由於它不依賴於駐留的總數常量; 咱們只須要記得,若是咱們添加或者移除了一條數據,就要增長或者減少索引. 但咱們已經瞭解到for循環並非一種速度快的解決方案. 咱們優化過的for循環則是一個合理的選擇, 只要咱們記得按需遞增或者遞減技術變量,還有索引. |
咱們仍然可使用for…in, 但前提是咱們首先建立了一個數組的拷貝. 這會起做用的,例如:
若是咱們對不一樣的技術進行基準測試(必要時把複製數組的開銷算在內,以便咱們能夠對原來數組內的數據進行變動), 咱們發現複製抵消了 for…in 循環以前所擁有的好處: $ For loop: 0.111422 $ Optimized for loop: 0.08967 $ Enumerator: 0.313182 $ For…in loop: 0.203722 $ Enumeration block: 0.436741 $ Concurrent enumeration block: 0.388509
在咱們遍歷一個數組時修改這個數組最快的計數,彷佛是須要使用一個優化了的for循環的. 對於一個 NSDictionary, 咱們不須要爲了使用NSEnumerator 或者快速枚舉而複製整個字典; 咱們能夠只去使用allKeys方法獲取到全部鍵的一個副本. 這都將能很好的運做起來:
|
然而一樣的技術在使用enumerateKeysAndObjectsUsingBlock方法時並不能起做用. 若是咱們循環遍歷一個字典進行基準測試, 按照須要對鍵或者對字典總體建立備份,咱們獲得了下面的結果: $ For loop: 2.24597 $ Optimized for loop: 0.00282001 $ Enumerator: 0.00508499 $ For…in loop: 0.000990987 $ Enumeration block: 0.00144804 $ Concurrent enumeration block: 0.00166804
這裏咱們能夠看到 for…in 循環是最快的一個. 那是由於在for...in循環中根據鍵取對象的開銷如今已經被在調用枚舉塊方法以前複製字典的開銷蓋過去了. |
當枚舉一個NSArray的時候:
當枚舉一個NSSet的時候:
當枚舉一個NSDictionary的時候:
這些方法可能不是最快的,但他們都是很是清晰易讀的。因此請記住,有時是在不寫乾淨的代碼,和快速的代碼之間作出選擇,你會發現,你能夠在兩個世界獲得最好的。 |