卷首語
歡迎來到 objc.io 第七期霉晕!
這個月,我們選擇了 Foundation 框架作為我們的主題腿准。
Foundation 框架的起源杂穷,可追溯到NeXTSTEP的時代, 到現(xiàn)在大概有 25 年的歷史了,涵蓋了很多的內(nèi)容猜敢。
本期不會描述太多關(guān)于 Foundation 框架的細節(jié)姑荷,而是重點會選取幾個話題來進行討論。我們選擇了對所有 Objective-C 開發(fā)者都有用的一些話題:基礎集合類(collection classes)缩擂、KVC(key-value coding) 和 KVO(key-value observing)鼠冕、值對象(value objects)和 值格式處理器(value formatters)。我們也挑選了一篇通用的關(guān)于通訊模式的文章胯盯,文章討論了在 Foundation 框架中發(fā)現(xiàn)的一些模式懈费,也涉及到了一個比較特別的話題:NSLinguisticTagger.
非常感謝Peter Steinberger,Klaas Pieter Annema和Oliver Mason博脑,他們?yōu)檫@一期貢獻了很多憎乙。也很感謝為這個社區(qū)出力的所有人票罐,沒有他們就沒有 objc.io∨⒈撸看看我們的貢獻者的名單该押,我就明白我所說的了。
說到支持阵谚,我們上個月上線了objc.io Newsstand應用蚕礼,那時候我們說,或者你可以捐贈支持我們梢什。后續(xù)的反響很令人欣慰奠蹬。我們并不是說需要拿錢去買什么很酷的車子,捐贈所得的錢會用于維持這個網(wǎng)站的日常運作绳矩,如果有余錢罩润,我們會把余錢捐給那些和 objc.io 的理念相似的社區(qū)協(xié)作項目上。
為了信息的透明翼馆,我們需要說明割以,現(xiàn)在大部分的錢被用于主機、設計工作和內(nèi)容編輯上应媚。另外严沥,我們每個月會抽出一小部分錢,存著留待日后用于 objc.io 的改善上中姜。盡管也花費了不少式矫,現(xiàn)在還是有不少的余錢铐拐。
除了上面的提到的方庭,我們還決定要把錢用在有意義的地方(where it makes a difference)跨扮。從這個月開始,我們會把額外的錢捐贈給一個慈善計劃携龟。我們會在下個月公布更多的消息兔跌。
從柏林發(fā)來最誠摯的祝福!
Chris, Daniel 和 Florian。
基礎集合類
NSArray, NSSet, NSOrderedSet 和 NSDictionary
基礎集合類是每一個 Mac/iOS 應用的基本組成部分峡蟋。在本文中坟桅,我們將對”老類” (NSArray,NSSet)和”新類” (NSMapTable,NSHashTable,NSPointerArray) 進行一個深入的研究,探索每一個的效率細節(jié)蕊蝗,并討論其使用場景仅乓。
作者提示:本文包含一些參照結(jié)果,但它們并不意味著絕對精確蓬戚,也沒有進行均差分析及多次的測試夸楣。這些結(jié)果的目的是給出運行時統(tǒng)計,來幫助我們認識到通常來說用什么會更快。所有的測試基于 iPhone 5s裕偿,使用 Xcode 5.1b1 和 iOS 7.1b1 的 64 位程序洞慎。編譯選項設置為 -Ofast 的發(fā)布構(gòu)建。Vectorize loops 和 unroll loops (默認設置) 均設置為關(guān)閉嘿棘。
大 O 符號,算法復雜度計量
首先旭绒,我們需要一些理論知識鸟妙。效率通常用大 O 符號描述。它定義了一個函數(shù)的極限特征挥吵,通常被用于描繪其算法效率重父。O 定義了函數(shù)增長率的上限。不同量級的差異非常巨大忽匈,可以看看通常使用的 O 符號的量級以及它們所對應需要的操作數(shù)的關(guān)系房午。
例如,如果用算法復雜度為 O(n^2)的算法對一個有 50 個元素的數(shù)組排序丹允,需要 2,500 步的操作郭厌。而且,還有內(nèi)部的系統(tǒng)開銷和方法調(diào)用 — 所以是 250 0個操作的時間常量雕蔽。 O(1)是理想的復雜度折柠,代表著恒定的時間。好的排序算法通常需要 O(n*log n) 的時間批狐。
可變性
大多數(shù)的集合類存在兩個版本:可變和不可變(默認)扇售。這和其他大多數(shù)的框架有非常大的不同,一開始會讓人覺得有一點奇怪嚣艇。然而其他的框架現(xiàn)在也應用了這一特性:就在幾個月前承冰,.NET公布了作為官方擴展的不可變集合。
最大的好處是什么食零?線程安全困乒。不可變的集合完全是線程安全的,可以同時在多個線程中迭代慌洪,避免各種轉(zhuǎn)變時出現(xiàn)異常的風險顶燕。你的 API絕不應該暴露一個可變的集合。
當然從不可變到可變?nèi)缓笤倩貋硎菚幸欢ù鷥r的 — 對象必須被拷貝兩次冈爹,所有集合內(nèi)的對象將被 retain/release涌攻。有時在內(nèi)部使用一個可變的集合,而在訪問時返回一個不可變的對象副本會更高效频伤。
與其他框架不同的是恳谎,蘋果沒有提供一個線程安全的可變集合,NSCache是例外,但它真的算不上是集合類因痛,因為它不是一個通用的容器婚苹。大多數(shù)時候,你不會需要在集合層級的同步特性鸵膏。想象一段代碼膊升,作用是檢查字典中一個 key 是否存在,并根據(jù)檢查結(jié)果決定設置一個新的 key 或者返回某些值 — 你通常需要把多個操作歸類谭企,這時線程安全的可變集合并不能對你有所幫助廓译。
其實也有一些同步的,線程安全的可以使用的可變集合案例债查,它們往往只需要用幾行代碼非区,通過子類和組合的方法建立,比如這個NSDictionary或這個NSArray盹廷。
需要注意的是征绸,一些較新的集合類,如NSHashTable俄占,NSMapTable和NSPointerArray默認就是可變的管怠,它們并沒有對應的不可變的類。它們用于類的內(nèi)部使用颠放,你基本應該不會能找到需要它們的不可變版本的應用場景排惨。
NSArray
NSArray作為一個存儲對象的有序集合,可能是被使用最多的集合類碰凶。這也是為什么它有自己的比原來的[NSArray arrayWithObjects:..., nil]簡短得多的快速語法糖符號@[...]暮芭。NSArray實現(xiàn)了objectAtIndexedSubscript:,因為我們可以使用類 C 的語法array[0]來代替原來的[array objectAtIndex:0]欲低。
性能特征
關(guān)于NSArray的內(nèi)容比你想象的要多的多辕宏。基于存儲對象的多少砾莱,它使用各種內(nèi)部的變體瑞筐。最有趣的部分是蘋果對于個別的對象訪問并不保證 O(1) 的訪問時間 — 正如你在CFArray.h CoreFoundation 頭文件中的關(guān)于算法復雜度的注解中可以讀到的:
對于 array 中值的訪問時間,不管是在現(xiàn)在還是將來腊瑟,我們保證在任何一種實現(xiàn)下最壞情況是 O(lg N)聚假。但是通常來說它會是 O(1) (常數(shù)時間)。線性搜索操作很可能在最壞情況下的復雜度為 O(N*lg N)闰非,但通常來說上限會更小一些膘格。插入和刪除操作耗時通常和數(shù)組中的值的數(shù)量成線性關(guān)系。但在某些實現(xiàn)的最壞情況下會是 O(N*lg N) 财松。在數(shù)組中瘪贱,沒有對于性能上特別有優(yōu)勢的數(shù)據(jù)位置纱控,也就是說,為了更快地訪問到元素而將其設為在較低的 index 上菜秦,或者在較高的 index 上進行插入和刪除甜害,或者類似的一些做法,是沒有必要的球昨。
在測量的時候尔店,NSArray產(chǎn)生了一些有趣的額外的性能特征。在數(shù)組的開頭和結(jié)尾插入/刪除元素通常是一個 O(1)操作褪尝,而隨機的插入/刪除通常是 O(N) 的闹获。
有用的方法
NSArray的大多數(shù)方法使用isEqual:來檢查對象間的關(guān)系(例如containsObject:中)。有一個特別的方法indexOfObjectIdenticalTo:用來檢查指針相等河哑,如果你確保在同一個集合中搜索,那么這個方法可以很大的提升搜索速度龟虎。 在 iOS 7 中璃谨,我們最終得到了與lastObject對應的公開的firstObject方法,對于空數(shù)組鲤妥,這兩個方法都會返回nil— 而常規(guī)的訪問方法會拋出一個NSRangeException異常佳吞。
關(guān)于構(gòu)造(可變)數(shù)組有一個漂亮的細節(jié)可以節(jié)省代碼量。如果你通過一個可能為 nil 的數(shù)組創(chuàng)建一個可變數(shù)組棉安,通常會這么寫:
或者通過更簡潔的三元運算符:
NSMutableArray*mutableObjects = [array mutableCopy] ?: [NSMutableArrayarray];
更好的解決方案是使用arrayWithArray:底扳,即使原數(shù)組為nil,該方法也會返回一個數(shù)組對象:
NSMutableArray*mutableObjects = [NSMutableArrayarrayWithArray:array];
這兩個操作在效率上幾乎相等贡耽。使用copy會快一點點衷模,不過話說回來,這不太可能是你應用的瓶頸所在蒲赂。提醒:不要使用[@[] mutableCopy]阱冶。經(jīng)典的[NSMutableArray array]可讀性更好。
逆序一個數(shù)組非常簡單:array.reverseObjectEnumerator.allObjects滥嘴。我們使用系統(tǒng)提供的reverseObjectEnumerator木蹬,每一個NSEnumerator都實現(xiàn)了allObjects,該方法返回一個新數(shù)組若皱。雖然沒有原生的randomObjectEnumerator方法镊叁,你可以寫一個自定義的打亂數(shù)組順序的枚舉器或者使用一些出色的開源代碼。
數(shù)組排序
有很多各種各樣的方法來對一個數(shù)組排序走触。如果數(shù)組存儲的是字符串對象晦譬,sortedArrayUsingSelector:是第一選擇:
如果想更可控,可以使用基于函數(shù)指針的排序方法:
蘋果增加了一個方法來加速使用sortedArrayHint的排序饺汹。
hinted sort 方式在你有一個已排序的大數(shù)組 (N 個元素) 并且只改變其中一小部分(P 個添加和刪除蛔添,這里 P遠小于 N)時,會非常有效。你可以重用原來的排序結(jié)果迎瞧,然后在 N 個老項目和 P 個新項目進行一個概念上的歸并排序夸溶。為了得到合適的 hint,你應該在原來的數(shù)組排序后使用 sortedArrayHint 來在你需要的時候(比如在數(shù)組改變后想重新排序時)保證持有它凶硅。
因為block的引入缝裁,也出現(xiàn)了一些基于block的排序方法:
性能上來說,不同的方法間并沒有太多的不同足绅。有趣的是捷绑,基于 selector 的方式是最快的。
:
Sorting 1000000 elements. selector: 4947.90[ms] function: 5618.93[ms] block: 5082.98[ms].
二分查找
NSArray從 iOS 4 / Snow Leopard 開始內(nèi)置了二分查找
這是個簡單的衡量二分查找有多快的數(shù)據(jù):
Timeto searchfor1000entries within1000000objects.Linear:54130.38[ms].Binary:7.62[ms]
作為比較沈贝,查找NSOrderedSet中的指定索引花費 0.23 毫秒 — 就算和二分查找相比也又快了 30 多倍。
記住排序的開銷也是昂貴的勋乾。蘋果使用復雜度為 O(n*log n) 的歸并排序宋下,所以如果你執(zhí)行一次indexOfObject:的話,就沒有必要使用二分查找了市俊。
通過指定NSBinarySearchingInsertionIndex杨凑,你可以獲得正確的插入索引,以確保在插入元素后仍然可以保證數(shù)組的順序摆昧。
枚舉和總覽
作為測試撩满,我們來看一個普通的使用場景。從一個數(shù)組中過濾出一些元素組成另一個數(shù)組绅你。這些測試都包括了枚舉的方法以及使用 API 進行過濾的方式:
indexesOfObjectsWithOptions:passingTest:必須每次都執(zhí)行一次 block 因此比傳統(tǒng)的使用NSFastEnumeration技術(shù)的基于 for 循環(huán)的枚舉要稍微低效一些伺帘。但是如果開啟了并發(fā)枚舉,那么前者的速度則會大大的超過后者幾乎 2 倍忌锯。iPhone 5s 是雙核的伪嫁,所以這說得通。這里并沒有體現(xiàn)出來的是NSEnumerationConcurrent只對大量的對象有意義偶垮,如果你的集合中的對象數(shù)量很少张咳,用哪個方法就真的無關(guān)緊要帝洪。甚至NSEnumerationConcurrent上額外的線程管理實際上會使結(jié)果變得更慢。
最大的輸家是filteredArrayUsingPredicate:脚猾。NSPredicate需要在這里提及是因為葱峡,人們可以寫出非常復雜的表達式,尤其是用不基于 block 的變體龙助。使用 Core Data 的用戶應該會很熟悉砰奕。
為了比較的完整,我們也加入了NSEnumerator作為比較 — 雖然沒有任何理由再使用它了提鸟。然而它竟出人意料的快(至少還是比基于NSPredicate的過濾要快)军援,它的運行時消耗無疑比快速枚舉更多 — 現(xiàn)在它只用于向后兼容。甚至沒有優(yōu)化過的objectAtIndex:都要更快些称勋。
NSFastEnumeration
在OSX 10.5和iOS的最初版本中胸哥,蘋果增加了NSFastEnumeration。在此之前赡鲜,只有每次返回一個元素的NSEnumeration烘嘱,每次迭代都有運行時開銷。而快速枚舉蝗蛙,蘋果通過countByEnumeratingWithState:objects:count:返回一個數(shù)據(jù)塊。該數(shù)據(jù)塊被解析成id類型的 C 數(shù)組醉鳖。這就是更快的速度的原因捡硅;迭代一個 C 數(shù)組要快得多,而且可以被編譯器更深一步的優(yōu)化盗棵。手動的實現(xiàn)快速枚舉是十分難辦的壮韭,所以蘋果的FastEnumerationSample是一個不錯的開始,還有一篇Mike Ash 的文章也很不錯纹因。
應該用arrayWithCapacity:嗎?
初始化NSArray的時候喷屋,可以選擇指定數(shù)組的預期大小。在檢測的時候瞭恰,結(jié)果是在效率上沒有差別 — 至少在統(tǒng)計誤差范圍內(nèi)的測量的時間幾乎相等屯曹。有消息透漏說實際上蘋果根本沒有使用這個參數(shù)。然而使用arrayWithCapacity:仍然好處惊畏,它可以作為一種隱性的文檔來幫助你理解代碼:
Adding 10.000.000 elements to NSArray. no count 1067.35[ms] with count: 1083.13[ms].
子類化注意事項
很少有理由去子類化基礎集合類恶耽。大多數(shù)時候,使用 CoreFoundation 級別的類并且自定義回調(diào)函數(shù)定制自定義行為是更好的解決方案颜启。 創(chuàng)建一個大小寫不敏感的字典偷俭,一種方法是子類化NSDictionary并且自定義訪問方法,使其將字符串始終變?yōu)樾?或大寫)缰盏,并對排序也做類似的修改涌萤。更快更好的解決方案是提供一組不同的CFDictionaryKeyCallBacks集淹遵,你可以提供自定義的hash和isEqual:回調(diào)。你可以在這里找到一個例子负溪。這種方法的優(yōu)美之處應該歸功于toll-free 橋接)透揣,它仍然是一個簡單的字典,因此可以被任何使用NSDictionary作為參數(shù)的API接受笙以。
子類作用的一個例子是有序字典的用例淌实。.NET 提供了一個SortedDictionary,Java 有TreeMap猖腕,C++ 有std::map拆祈。雖然你可以使用 C++ 的 STL 容器,但卻無法使它自動的retain/release倘感,這會讓使用起來笨拙得多放坏。因為NSDictionary是一個類簇,所以子類化跟人們想象的相比非常不同老玛。這已經(jīng)超過了本文的討論范疇淤年,這里有一個真實的有序字典的例子。
NSDictionary
一個字典存儲任意的對象鍵值對蜡豹。 由于歷史原因麸粮,初始化方法[NSDictionary dictionaryWithObjectsAndKeys:object, key, nil]使用了相反的值到鍵的順序,而新的快捷語法則從 key 開始镜廉,@{key : value, ...}弄诲。
NSDictionary中的鍵是被拷貝的并且需要是不變的。如果在一個鍵在被用于在字典中放入一個值后被改變的話娇唯,那么這個值就會變得無法獲取了齐遵。一個有趣的細節(jié)是,在NSDictionary中鍵是被 copy 的塔插,但是在使用一個 toll-free 橋接的CFDictionary時卻只會被 retain梗摇。CoreFoundation 類沒有通用的拷貝對象的方法,因此這時拷貝是不可能的(*)想许。這只適用于你使用CFDictionarySetValue()的時候伶授。如果你是通過setObject:forKey來使用一個 toll-free 橋接的CFDictionary的話,蘋果會為其增加額外處理邏輯伸刃,使得鍵被拷貝蒸甜。但是反過來這個結(jié)論則不成立 — 使用已經(jīng)轉(zhuǎn)換為CFDictionary的NSDictionary對象韧衣,并用對其使用CFDictionarySetValue()方法,還是會導致調(diào)用回setObject:forKey并對鍵進行拷貝。
(*)其實有一個現(xiàn)成的鍵的回調(diào)函數(shù)kCFCopyStringDictionaryKeyCallBacks可以拷貝字符串澜掩,因為對于 ObjC對象來說随常,CFStringCreateCopy()會調(diào)用[NSObject copy],我們可以巧妙使用這個回調(diào)來創(chuàng)建一個能進行鍵拷貝的CFDictionary。
性能特征
蘋果在定義字典的計算復雜度時顯得相當?shù)驼{(diào)亮蒋。唯一的信息可以在CFDictionary的頭文件中找到:
對于字典中值的訪問時間,不管是在現(xiàn)在還是將來妆毕,我們保證在任何一種實現(xiàn)下最壞情況是 O(N)慎玖。但通常來說它會是 O(1) (常數(shù)時間)。插入和刪除操作一般來說也會是常數(shù)時間笛粘,但是在某些實現(xiàn)中最壞情況將為 O(N*N)趁怔。通過鍵來訪問值將比直接訪問值要快(如果你有這樣的操作要做的話)。對于同樣數(shù)目的值薪前,字典需要花費比數(shù)組多得多的內(nèi)存空間润努。
跟數(shù)組相似的,字典根據(jù)尺寸的不同使用不同的實現(xiàn)示括,并在其中無縫切換铺浇。
枚舉和總覽
過濾字典有幾個不同的方法:
(*)使用getObjects:andKeys:時需要注意。在上面的代碼例子中垛膝,我們使用了可變長度數(shù)組這一 C99 特性(通常鳍侣,數(shù)組的數(shù)量需要是一個固定值)。這將在棧上分配內(nèi)存吼拥,雖然更方便一點倚聚,但卻有其限制。上面的代碼在元素數(shù)量很多的時候會崩潰掉凿可,所以我們使用基于malloc/calloc的分配 (和free) 以確保安全秉沼。
為什么這次NSFastEnumeration這么慢?迭代字典通常需要鍵和值兩者矿酵,快速枚舉只能枚舉鍵,我們必須每次都自己獲取值矗积。使用基于 block 的enumerateKeysAndObjectsUsingBlock:更高效全肮,因為兩者都可以更高效的被提前獲取。
這次測試的勝利者又是通過keysOfEntriesWithOptions:passingTest:和objectsForKeys:notFoundMarker:的并發(fā)迭代棘捣。代碼稍微多了一點辜腺,但是可以用 category 進行漂亮的封裝。
應該用 dictionaryWithCapacity: 嗎?
到現(xiàn)在你應該已經(jīng)知道該如何測試了乍恐,簡單的回答是不评疗,count參數(shù)沒有改變?nèi)魏问虑?
Adding 10000000 elements to NSDictionary. no count 10786.60[ms] with count: 10798.40[ms].
排序
關(guān)于字典排序沒有太多可說的。你只能將鍵數(shù)組排序為一個新對象茵烈,因此你可以使用任何正規(guī)的NSArray的排序方法:
共享鍵
從 iOS 6 和 OS X 10.8 開始百匆,新建的字典可以使用一個預先生成好的鍵集,使用sharedKeySetForKeys:從一個數(shù)組中創(chuàng)建鍵集呜投,然后用dictionaryWithSharedKeySet:創(chuàng)建字典加匈。共享鍵集會復用對象存璃,以節(jié)省內(nèi)存。根據(jù)Foundation Release Notes雕拼,sharedKeySetForKeys:中會計算一個最小完美哈希纵东,這個哈希值可以取代字典查找過程中探索循環(huán)的需要,因此使鍵的訪問更快啥寇。
雖然在我們有限的測試中沒有法線蘋果在NSJSONSerialization中使用這個特性偎球,但毫無疑問,在處理 JSON 的解析工作時這個特性可以發(fā)揮得淋漓盡致辑甜。(使用共享鍵集創(chuàng)建的字典是NSSharedKeyDictionary的子類衰絮;通常的字典是__NSDictionaryI/__NSDictionaryM,I / M 表明可變性栈戳;可變和不可變的的字典在 toll-free 橋接后對應的都是_NSCFDictionary類岂傲。)
有趣的細節(jié):共享鍵字典始終是可變的,即使對它們執(zhí)行了”copy”命令后也是子檀。這個行為文檔中并沒有說明镊掖,但很容易被測試:
NSSet
NSSet和它的可變變體NSMutableSet是無序?qū)ο蠹稀z查一個對象是否存在通常是一個 O(1) 的操作褂痰,使得比NSArray快很多亩进。NSSet只在被使用的哈希方法平衡的情況下能高效的工作;如果所有的對象都在同一個哈纤跬幔筐內(nèi)归薛,NSSet在查找對象是否存在時并不比NSArray快多少。
NSSet還有變體NSCountedSet匪蝙,以及非 toll-free 計數(shù)變體CFBag/CFMutableBag主籍。
NSSet會 retain 它其中的對象,但是根據(jù) set 的規(guī)定逛球,對象應該是不可變的千元。添加一個對象到 set 中隨后改變它會導致一些奇怪的問題并破壞 set 的狀態(tài)。
NSSet的方法比NSArray少的多颤绕。沒有排序方法幸海,但有一些方便的枚舉方法。重要的方法有allObjects奥务,將對象轉(zhuǎn)化為NSArray物独,anyObject則返回任意的對象,如果 set 為空氯葬,則返回 nil挡篓。
Set 操作
NSMutableSet有幾個很強大的方法,例如intersectSet:帚称,minusSet:和unionSet:瞻凤。
應該用setWithCapacity:嗎?
我們再一次測試當創(chuàng)建 set 時給定容量大小是否會有顯著的速度差異:
Adding 1.000.000 elements to NSSet. no count 2928.49[ms] with count: 2947.52[ms].
在統(tǒng)計誤差范圍內(nèi)憨攒,結(jié)果沒有顯著差異。有一份證據(jù)表明至少在上一個 runtime 版本中阀参,有很多的性能上的影響肝集。
NSSet 性能特征
這個檢測非常符合我們的預期:NSSet在每一個被添加的對象上執(zhí)行hash和isEqual:方法并管理一系列哈希值,所以在添加元素時耗費了更多的時間蛛壳。set的隨機訪問比較難以測試杏瞻,因為這里執(zhí)行的都是anyObject。
這里沒有必要包含containsObject:的測試衙荐,set 要快幾個數(shù)量級捞挥,畢竟這是它的特點。
NSOrderedSet
NSOrderedSet在 iOS 5 和 Mac OS X 10.7 中第一次被引入忧吟,除了 Core Data砌函,幾乎沒有直接使用它的 API×镒澹看上去它綜合了NSArray和NSSet兩者的好處讹俊,對象查找,對象唯一性煌抒,和快速隨機訪問仍劈。
NSOrderedSet有著優(yōu)秀的 API 方法,使得它可以很便利的與其他 set 或者有序 set 對象合作寡壮。合并贩疙,交集,差集况既,就像NSSet支持的那樣这溅。它有NSArray中除了比較陳舊的基于函數(shù)的排序方法和二分查找以外的大多數(shù)排序方法。畢竟containsObject:非嘲羧裕快芍躏,所以沒有必要再用二分查找了。
NSOrderedSet的array和set方法分別返回一個NSArray和NSSet降狠,這些對象表面上是不可變的對象,但實際上在 NSOrderedSet 更新的時候庇楞,它們也會更新自己榜配。如果你在不同線程上使用這些對象并發(fā)生了詭異異常的時候,知道這一點是非常有好處的吕晌。本質(zhì)上蛋褥,這些類使用的是__NSOrderedSetSetProxy和__NSOrderedSetArrayProxy。
附注:如果你想知道為什么NSOrderedSet不是NSSet的子類睛驳,NSHipster 上有一篇非常好的文章解釋了可變/不可變類簇的缺點烙心。
NSOrderedSet 性能特征
這個測試在每一個集合類中添加自定義字符串膜廊,隨后隨機訪問它們。
NSOrderedSet比NSSet和NSArray占用更多的內(nèi)存淫茵,因為它需要同時維護哈希值和索引爪瓜。
NSHashTable
NSHashTable效仿了NSSet,但在對象/內(nèi)存處理時更加的靈活匙瘪∶可以通過自定義CFSet的回調(diào)獲得NSHashTable的一些特性,哈希表可以保持對對象的弱引用并在對象被銷毀之后正確的將其移除丹喻,有時候如果手動在 NSSet 中添加的話薄货,想做到這個是挺惡心的一件事。它是默認可變的 — 并且這個類沒有相應的不可變版本碍论。
NSHashTable有 ObjC 和原始的 C API谅猾,C API 可以用來存儲任意對象。蘋果在 10.5 Leopard 系統(tǒng)中引入了這個類鳍悠,但是 iOS 的話直到最近的 iOS 6 中才被加入税娜。足夠有趣的是它們只移植了 ObjC API;更多強大的 C API 沒有包括在 iOS 中贼涩。
NSHashTable可以通過initWithPointerFunctions:capacity:進行大量的設置 — 我們只選取使用預先定義的hashTableWithOptions:這一最普遍的使用場景巧涧。其中最有用的選項有利用weakObjectsHashTable來使用其自身的構(gòu)造函數(shù)。
NSPointerFunctions
這些指針函數(shù)可以被用在NSHashTable遥倦,NSMapTable和NSPointerArray中谤绳,定義了對存儲在這個集合中的對象的獲取和保留行為。這里只介紹最有用的選項袒哥。完整列表參見NSPointerFunctions.h缩筛。
有兩組選項步淹。內(nèi)存選項決定了內(nèi)存管理垦写,個性化定義了哈希和相等。
NSPointerFunctionsStrongMemory創(chuàng)建了一個r etain/release 對象的集合热芹,非常像常規(guī)的NSSet或NSArray却紧。
NSPointerFunctionsWeakMemory使用和__weak等價的方式來存儲對象并自動移除被銷毀的對象桐臊。
NSPointerFunctionsCopyIn在對象被加入到集合前拷貝它們。
NSPointerFunctionsObjectPersonality使用對象的hash和isEqual:(默認)晓殊。
NSPointerFunctionsObjectPointerPersonality對于isEqual:和hash使用直接的指針比較断凶。
NSHashTable 性能特征
如果你只是需要NSSet的特性,請堅持使用NSSet巫俺。NSHashTable在添加對象時花費了將近2倍的時間认烁,但是其他方面的效率卻非常相近。
NSMapTable
NSMapTable和NSHashTable相似,但是效仿的是NSDictionary却嗡。因此舶沛,我們可以通過mapTableWithKeyOptions:valueOptions:分別控制鍵和值的對象獲取/保留行為。存儲弱引用是NSMapTable最有用的特性窗价,這里有4個方便的構(gòu)造函數(shù):
strongToStrongObjectsMapTable
weakToStrongObjectsMapTable
strongToWeakObjectsMapTable
weakToWeakObjectsMapTable
注意如庭,除了使用NSPointerFunctionsCopyIn,任何的默認行為都會 retain (或弱引用)鍵對象而不會拷貝它舌镶,這與CFDictionary的行為相同而與NSDictionary不同柱彻。當你需要一個字典,它的鍵沒有實現(xiàn)NSCopying協(xié)議的時候(比如像UIView)餐胀,這會非常有用哟楷。
如果你好奇為什么蘋果”忘記”為NSMapTable增加下標,你現(xiàn)在知道了否灾。下標訪問需要一個id作為 key卖擅,對NSMapTable來說這不是強制的。如果不通過一個非法的 API 協(xié)議或者移除NSCopying協(xié)議來削弱全局下標墨技,是沒有辦法給它增加下標的惩阶。
你可以通過dictionaryRepresentation把內(nèi)容轉(zhuǎn)換為普通的NSDictionary。不像NSOrderedSet扣汪,這個方法返回的是一個常規(guī)的字典而不是一個代理断楷。
NSMapTable 性能特征
NSMapTable只比NSDictionary略微慢一點。如果你需要一個不 retain 鍵的字典崭别,放棄CFDictionary而使用它吧冬筒。
NSPointerArray
NSPointerArray類是一個稀疏數(shù)組,工作起來與NSMutableArray相似茅主,但可以存儲NULL值舞痰,并且count方法會反應這些空點【饕Γ可以用NSPointerFunctions對其進行各種設置响牛,也有應對常見的使用場景的快捷構(gòu)造函數(shù)strongObjectsPointerArray和weakObjectsPointerArray。
在能使用insertPointer:atIndex:之前赫段,我們需要通過直接設置count屬性來申請空間呀打,否則會產(chǎn)生一個異常。另一種選擇是使用addPointer:糯笙,這個方法可以自動根據(jù)需要增加數(shù)組的大小贬丛。
你可以通過allObjects將一個NSPointerArray轉(zhuǎn)換成常規(guī)的NSArray。這時所有的NULL值會被去掉炬丸,只有真正存在的對象被加入到數(shù)組 — 因此數(shù)組的對象索引很有可能會跟指針數(shù)組的不同。注意:如果向指針數(shù)組中存入任何非對象的東西,試圖執(zhí)行allObjects都會造成EXC_BAD_ACCESS崩潰稠炬,因為它會一個一個地去 retain ”對象”焕阿。
從調(diào)試的角度講,NSPointerArray沒有受到太多歡迎首启。description方法只是簡單的返回了暮屡。為了得到所有的對象需要執(zhí)行[pointerArray allObjects],當然毅桃,如果存在NULL的話會改變索引褒纲。
NSPointerArray 性能特征
在性能方面,NSPointerArray真的非常非常慢钥飞,所以當你打算在一個很大的數(shù)據(jù)集合上使用它的時候一定要三思莺掠。在本測試中我們比較了使用NSNull作為空標記的NSMutableArray,而對NSPointerArray我們用NSPointerFunctionsStrongMemory來進行設置 (這樣對象會被適當?shù)?retain)读宙。在一個有 10,000 個元素的數(shù)組中彻秆,我們每隔十個插入一個字符串 ”Entry %d”。此測試包括了用NSNull.null填充NSMutableArray的總時間结闸。對于NSPointerArray唇兑,我們使用setCount:來代替:
注意NSPointerArray需要的時間比NSMutableArray多了超過* 250 倍(!)* 。這非常奇怪和意外桦锄。跟蹤內(nèi)存是比較困難的扎附,所以按理說NSPointerArray會更高效才對。不過由于我們使用的是同一個NSNull來標記空對象结耀,所以除了指針也沒有什么更多的消耗留夜。
NSCache
NSCache是一個非常奇怪的集合。在 iOS 4 / Snow Leopard 中加入饼记,默認為可變并且線程安全的香伴。這使它很適合緩存那些創(chuàng)建起來代價高昂的對象。它自動對內(nèi)存警告做出反應并基于可設置的”成本”清理自己具则。與NSDictionary相比即纲,鍵是被 retain 而不是被 copy 的。
NSCache的回收方法是不確定的博肋,在文檔中也沒有說明低斋。向里面放一些類似圖片那樣超大的對象并不是一個好主意,有可能它在能回收之前就更快地把你的 cache 給填滿了匪凡。(這是在PSPDFKit中很多跟內(nèi)存有關(guān)的 crash 的原因膊畴,在使用自定義的基于 LRU 的鏈表緩存的代碼之前,我們起初使用了NSCache存儲事先渲染的圖片病游。)
可以對NSCache進行設置唇跨,這樣它就能自動回收那些實現(xiàn)了NSDiscardableContent協(xié)議的對象稠通。實現(xiàn)了該屬性的一個比較常用的類是同時間加入的NSPurgeableData,但是在 OS X 10.9 之前买猖,它是非完全線程安全的 (也沒有信息表明這個變化也影響到了 iOS改橘,或者說在 iOS 7 中被修復了)。
NSCache 性能
那么相比起NSMutableDictionary來說玉控,NSCache表現(xiàn)如何呢飞主?加入的線程安全必然會帶來一些消耗。處于好奇高诺,我也加入了一個自定義的線程安全的字典的子類 (PSPDFThreadSafeMutableDictionary)碌识,它通過OSSpinLock實現(xiàn)同步的訪問。
NSCache表現(xiàn)的相當好虱而,隨機訪問跟我們自定義的線程安全字典一樣快筏餐。如我們預料的,添加更慢一些薛窥,因為NSCache要多維護一個決定何時回收對象的成本系數(shù)胖烛。就這一點來看這不是一個非常公平的比較。有趣的是诅迷,在模擬器上運行效率要慢了幾乎 10 倍佩番。無論對 32 或 64 位的系統(tǒng)都是這樣。而且看起來這個類已經(jīng)在 iOS 7 中優(yōu)化過罢杉,或者是受益于 64 位 runtime 環(huán)境趟畏。當在老的設備上測試時,使用NSCache的性能消耗就明顯得多滩租。
iOS 6(32 bit) 和 iOS 7(64 bit) 的區(qū)別也很明顯赋秀,因為 64 位運行時使用標簽指針 (tagged pointer),因此我們的@(idx)boxing 要更為高效律想。
NSIndexSet
有些使用場景下NSIndexSet(和它的可變變體猎莲,NSMutableIndexSet) 真的非常出色,對它的使用貫穿在 Foundation 中技即。它可以用一種非常高效的方法存儲一組無符號整數(shù)的集合著洼,尤其是如果只是一個或少量范圍的時候。正如 set 這個名字已經(jīng)暗示的那樣而叼,每一個NSUInteger要么在索引 set 中身笤,要么不在。如果你需要存儲任意非唯一的數(shù)的時候葵陵,最好使用NSArray液荸。
下面是如何把一個整數(shù)數(shù)組轉(zhuǎn)換為NSIndexSet:
如果不使用block,從索引set中拿到所有的索引有點麻煩脱篙,getIndexes:maxCount:inIndexRange:是最快的方法娇钱,其次是使用firstIndex并迭代直到indexGreaterThanIndex:返回NSNotFound伤柄。隨著 block 的到來,使用NSIndexSet工作變得方便的多:
NSIndexSet性能
Core Foundation 中沒有和NSIndexSet相當?shù)念愇穆ВO果也沒有對性能做出任何承諾响迂。NSIndexSet和NSSet之間的比較也相對的不公平,因為常規(guī)的 set 需要對數(shù)字進行包裝细疚。為了緩解這個影響,這里的測試準備了實現(xiàn)包裝好的NSUintegers川梅,并且在兩個循環(huán)中都會執(zhí)行unsignedIntegerValue:
我們看到在一百萬左右對象的時候疯兼,NSIndexSet開始變得比NSSet慢,但只是因為新的運行時和標簽指針贫途。在 iOS 6 上運行相同的測試表明吧彪,甚至在更高數(shù)量級實體的條件下,NSIndexSet更快丢早。實際上姨裸,在大多數(shù)應用中,你不會添加太多的整數(shù)到索引 set 中怨酝。還有一點這里沒有測試傀缩,就是NSIndexSet跟NSSet比無疑有更好的內(nèi)存優(yōu)化。
結(jié)論
本文提供了一些真實的測試农猬,使你在使用基礎集合類的時候做出有根據(jù)的選擇赡艰。除了上面討論的類,還有一些不常用但確實有用的類斤葱,尤其是NSCountedSet慷垮,CFBag,CFTree揍堕,CFBitVector和CFBinaryHeap料身。