在 iOS開發中 隨處可見 Hash 的身影,難道咱們很差奇嗎?html
下圖只是列出了部分知識點(Hash
在iOS中的應用分析整理) ios
摘自知乎的一句話:git
算法、數據結構、通訊協議、文件系統、驅動等,雖然本身不寫那些東西,可是
瞭解其原理
對於排錯
、優化本身的代碼
有很大幫助,就比如雖然你不設計製造汽車,但若是你瞭解發動機、變速器、安全氣囊等幾項原理,對於你駕車如何省油
、延長使用壽命
、保證自身安全
有很大好處學而不思則罔、思而不學則殆,開發人員就是個隨波而進的行業,不管什麼時候何地,保持學習的深度和廣度對於自身發展是很重要的,誰都不想60歲退休了還停留在增刪查改的層面。github
詳細的原理能夠查閱其餘資料,這裏只介紹一下實現中使用的基本數據結構。關聯對象採用的是
HashMap
嵌套HashMap
的結構存儲數據的,簡單來講就是根據對象從第一個HashMap
中取出存儲對象全部關聯對象的第二個HashMap
,而後根據屬性名從第二個HashMap
中取出屬性對應的值和策略。算法
設計關聯對象的初衷是,經過傳入 對象 + 屬性名字 ,就能夠找到屬性值。方案設計實現好後,查找一個對象的關聯對象的基本步驟:swift
- - 一、 已知條件一:對象 ,所以引出第一個
HashMap
(AssociationsHashMap
),用一個能惟一表明對象的值做爲key
,用存儲對象的全部關聯對象的結構(名字:值+策略)做爲value
- - 二、 已知條件二:屬性名字 ,所以引出第二個
HashMap
(ObjectAssociationMap
),用屬性名字做爲key
,用屬性名字對應的結構體(值+策略)做爲value
一樣詳細的原理能夠查閱其餘資料,這裏只介紹一下實現中使用的基本數據結構。
weak
採用的是一個全局的HashMap
嵌套數組的結構存儲數據的,銷燬對象(weak指針
指向的對象)的時候,根據對象從HashMap
中找到存放全部指向該對象的weak指針
的數組,而後將數組中的全部元素(weak指針
)都置爲nil。bash
weak
的最大特色就是在對象銷燬的時候,自動置nil
,減小訪問野指針的風險
,這也是設計weak
的初衷。方案設計實現好後,weak
指針置nil
的基本步驟:數據結構
- 一、對象
dealloc
的時候,從全局的HashMap
中,根據一個惟一表明對象的值做爲key
,找到存儲全部指向該對象的weak
指針的數組- 二、將數組中的全部元素都置爲
nil
蘋果對於weak的實現其實相似於通知的實現,指明誰(
weak指針
)要監聽誰(賦值對象
)什麼事件(dealloc操做
)執行什麼操做(置nil
)。
iOS 底層解析weak的實現原理(包含weak對象的初始化,引用,釋放的分析) weak實現原理
比較複雜,一個對象能夠被n個對象觀察,一對象的n個屬性又能夠分別被n個對象觀察。
詳細參考: GNUstep KVC/KVO探索(二):KVO的內部實現
一句話:
一致性哈希算法
+非對稱加解密算法
詳細參考: iOS App 簽名的原理
具體參考 :蘋果iOS系統源碼思考:對象的引用計數存儲在哪裏?--從runtime源碼獲得的啓示
if 對象支持TaggedPointer {
return 直接將對象的指針值做爲引用計數返回
}
else if 設備是64位環境 && Objective-C2.0 {
return 對象isa指針的一部分空間(bits_extra_rc)
}
else {
return hash表
}
複製代碼
線程和
RunLoop
之間是一一(子線程能夠沒有)對應的,其關係是保存在一個全局的Dictionary
裏。線程剛建立時並無RunLoop
,若是你不主動獲取,那它一直都不會有。RunLoop
的建立是發生在第一次獲取時,RunLoop
的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其RunLoop
(主線程除外)。
解釋完Hash表後,下面簡單解釋下
哈希表(
hash table
,也叫散列表),是根據鍵(key
)直接訪問訪問在內存儲存位置的數據結構。 哈希表本質是一個數組,數組中的每個元素成爲一個箱子,箱子中存放的是鍵值對。根據下標index
從數組中取value
。關鍵是如何獲取index
,這就須要一個固定的函數(哈希函數),將key
轉換成index
。不論哈希函數設計的如何完美,均可能出現不一樣的key
通過hash
處理後獲得相同的hash
值,這時候就須要處理哈希衝突。
優勢 :哈希表能夠提供快速的操做。
缺點 :哈希表一般是基於數組的,數組建立後難於擴展。 也沒有一種簡便的方法能夠以任何一種順序〔例如從小到大)遍歷表中的數據項。
綜上,若是不須要有序遍歷數據,井且能夠提早預測數據量的大小。那麼哈希表在速度和易用性方面是無與倫比的。
- 一、使用哈希函數將被查找的鍵映射(轉換)爲數組的索引,理想狀況下(hash函數設計合理)不一樣的鍵映射的數組下標也不一樣,全部的查找時間複雜度爲O(1)。可是實際狀況下不是這樣的,因此哈希查找的第二步就是處理哈希衝突。
- 二、處理哈希碰撞衝突。處理方法有不少,好比拉鍊法、線性探測法。
- 一、使用hash函數根據key獲得哈希值h
- 二、若是箱子的個數爲n,那麼值應該存放在底(h%n)個箱子中。h%n的值範圍爲[0, n-1]。
- 三、若是該箱子非空(已經存放了一個值)即不一樣的key獲得了相同的h產生了哈希衝突,此時須要使用拉鍊法或者開放定址線性探測法解決衝突。
hash("張三") = 23;
hash("李四") = 30;
hash("王五") = 23;
複製代碼
哈希查找第一步就是使用哈希函數將鍵映射成索引。這種映射函數就是哈希函數。若是咱們有一個保存0-M數組,那麼咱們就須要一個可以將任意鍵轉換爲該數組範圍內的索引(0~M-1)的哈希函數。哈希函數須要易於計算而且可以均勻分佈全部鍵。好比舉個簡單的例子,使用手機號碼後三位就比前三位做爲key更好,由於前三位手機號碼的重複率很高。再好比使用身份證號碼出生年月位數要比使用前幾位數要更好。
在實際中,咱們的鍵並不都是數字,有多是字符串,還有多是幾個值的組合等,因此咱們須要實現本身的哈希函數。
- - 一、直接尋址法
- - 二、數字分析法
- - 三、平方取中法
- - 四、摺疊法
- - 五、隨機數法
- - 六、除留餘數法
要想設計一個優秀的哈希算法並不容易,根據經驗,總結了須要知足的幾點要求:
- 從哈希值不能反向推導出原始數據(因此哈希算法也叫單向哈希算法);
- 對輸入數據很是敏感,哪怕原始數據只修改了一個 Bit,最後獲得的哈希值也大不相同;
- 散列衝突的機率要很小,對於不一樣的原始數據,哈希值相同的機率很是小;
- 哈希算法的執行效率要儘可能高效,針對較長的文本,也能快速地計算出哈希值。
負載因子是哈希表的一個重要屬性,用來衡量哈希表的空/滿程度,必定程度也能夠提現查詢的效率。負載因子越大,意味着哈希表越滿,越容易致使衝突,性能也就越低。因此當負載因子大於某個常數(通常是0.75)時,哈希表將自動擴容。哈希表擴容時,通常會建立兩倍於原來的數組長度。所以即便key的哈希值沒有變化,對數組個數取餘的結果會隨着數組個數的擴容發生變化,所以鍵值對的位置都有可能發生變化,這個過程也成爲重哈希(
rehash
)。
哈希表擴容 在數組比較多的時候,須要從新哈希並移動數據,性能影響較大。
哈希表擴容 雖然可以使負載因子下降,但並不老是能有效提升哈希表的查詢性能。好比哈希函數設計的不合理,致使全部的
key
計算出的哈希值都相同,那麼即便擴容他們的位置仍是在同一條鏈表上,變成了線性表,性能極低,查詢的時候時間複雜度就變成了O(n)
。
簡單來講就是 數組 + 鏈表 。將鍵經過hash函數映射爲大小爲M的數組的下標索引,數組的每個元素指向一個鏈表,鏈表中的每個結點存儲着hash出來的索引值爲結點下標的鍵值對。
Java 8
解決哈希衝突採用的就是拉鍊法。在處理哈希函數設計不合理致使鏈表很長時(鏈表長度超過8
切換爲紅黑樹,小於6
從新退化爲鏈表)。將鏈表切換爲紅黑樹可以保證插入和查找的效率,缺點是當哈希表比較大時,哈希表擴容會致使瞬時效率下降。
Redis
解決哈希衝突採用的也是拉鍊法。經過增量式擴容解決了Java 8
中的瞬時擴容致使的瞬時效率下降的缺點,同時拉鍊法的實現方式(新插入的鍵值對放在鏈表頭部)帶來了兩個好處:
- - 1、頭插法能夠節省插入耗時。若是插到尾部,則須要時間複雜度爲
O(n)
的操做找到鏈表尾部,或者須要額外的內存地址來保存尾部鏈表的位置。- - 2、頭插法能夠節省查找耗時。對於一個數據系統來講,最新插入的數據每每可能頻繁的被查詢。
使用兩個大小爲N的數組(一個存放keys,另外一個存放values)。使用數組中的空位解決碰撞,當碰撞發生時(即一個鍵的hash值對應數組的下標被兩外一個鍵佔用)直接將下標索引加一(
index += 1
),這樣會出現三種結果:
- - 一、未命中(數組下標中的值爲空,沒有佔用)。
keys[index] = key
,values[index] = value
。- - 二、命中(數組下標中的值不爲空,佔用)。
keys[index] == key
,values[index] == value
。- - 三、命中(數組下標中的值不爲空,佔用)。
keys[index] != key
,繼續index += 1
,直到遇到結果1或2中止。
與開放定址線性探測發相比,拉鍊法有以下幾個優勢:
- - ①、拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
- - ②、因爲拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
- - ③、開放定址線性探測發爲減小衝突,要求裝填因子α較小,故當結點規模較大時會浪費不少空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增長的指針域可忽略不計,所以節省空間;
- ④、在用拉鍊法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。而對開放定址線性探測發構造的散列表,刪除結點不能簡單地將被刪結 點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。這是由於各類開放定址線性探測發中,空地址單元(即開放地址)都是查找失敗的條件。所以在用開放定址線性探測發處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點。
指針須要額外的空間,故當結點規模較小時,開放定址線性探測發較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址線性探測發中的衝突,從而提升平均查找速度。
- - 一、容易產生堆積問題;
- - 二、不適於大規模的數據存儲;
- - 三、散列函數的設計對衝突會有很大的影響;
- - 四、插入時可能會出現屢次衝突的現象,刪除的元素是多個衝突元素中的一個,須要對後面的元素做處理,實現較複雜;
- - 五、結點規模很大時會浪費不少空間;
Hash表的
平均查找長度
包括查找成功時的平均查找長度和查找失敗時的平均查找長度。查找成功時的平均查找長度=表中每一個元素查找成功時的比較次數之和/表中元素個數;
查找不成功時的平均查找長度至關於在表中查找元素不成功時的平均比較次數,能夠理解爲向表中插入某個元素,該元素在每一個位置都有可能,而後計算出在每一個位置可以插入時須要比較的次數,再除以表長即爲查找不成功時的平均查找長度。
例子:
給定一組數據
{32,14,23,01,42,20,45,27,55,24,10,53}
,假設散列表的長度爲13(最接近n的質數),散列函數爲H(k) = k%13
。分別畫出
用 線性探測法 和 拉鍊法 解決衝突時構造的哈希表,並求出在等機率下狀況,這兩種方法查找成功和查找不成功的平均查找長度。
ASL = (1*6+2*4+3*1+4*1)/12 = 7/4
複製代碼
查找不成功時的平均查找長度:
ASL = (4+2+2+1+2+1)/13
複製代碼
查找成功時查找次數=插入元素時的比較次數,查找成功的平均查找長度:
ASL = (1+2+1+4+3+1+1+1+3+9+1+1+3)/12=2.5
複製代碼
查找不成功時的查找次數=第n個位置不成功時的比較次數爲,第n個位置到第1個沒有數據位置的距離:如第0個位置取值爲1,第1個位置取值爲2.查找不成功時的平均查找長度:
ASL = (1+2+3+4+5+6+7+8+9+10+11+12)/ 13 = 91/13
複製代碼
NSMapTable
實現的,採用拉鍊法
解決哈希衝突typedef struct {
NSMapTable *table;
NSInteger i;
struct _NSMapNode *j;
} NSMapEnumerator;
複製代碼
上述結構體描述了遍歷一個
NSMapTable
時的一個指針對象,其中包含table
對象自身的指針,計數值,和節點指針。
typedef struct {
NSUInteger (*hash)(NSMapTable *table,const void *);
BOOL (*isEqual)(NSMapTable *table,const void *,const void *);
void (*retain)(NSMapTable *table,const void *);
void (*release)(NSMapTable *table,void *);
NSString *(*describe)(NSMapTable *table,const void *);
const void *notAKeyMarker;
} NSMapTableKeyCallBacks;
複製代碼
上述結構體中存放的是幾個函數指針,用於計算
key
的hash
值,判斷key
是否相等,retain
,release
操做。
typedef struct {
void (*retain)(NSMapTable *table,const void *);
void (*release)(NSMapTable *table,void *);
NSString *(*describe)(NSMapTable *table, const void *);
} NSMapTableValueCallBacks;
複製代碼
上述存放的三個函數指針,定義在對
NSMapTable
插入一對key-value
時,對value
對象的操做。
@interface NSMapTable : NSObject {
NSMapTableKeyCallBacks *keyCallBacks;
NSMapTableValueCallBacks *valueCallBacks;
NSUInteger count;
NSUInteger nBuckets;
struct _NSMapNode **buckets;
}
複製代碼
從上面的結構真的能看出
NSMapTable
是一個 哈希表 + 鏈表 的數據結構嗎?在NSMapTbale
中插入或者刪除一個對象的尋找時間 = O(1) + O(m) ,m爲最壞時可能爲n。O(1) :對
key
進行hash
獲得bucket
的位置O(m) :不一樣的
key
獲得相同的hash
值的value
存放到鏈表中,遍歷鏈表的時間
上面的結論和對應的解釋彷佛很合理?看看下面的分析再說也不遲!
開放定址線性探測法
struct __CFDictionary {
CFRuntimeBase _base;
CFIndex _count;
CFIndex _capacity;
CFIndex _bucketsNum;
uintptr_t _marker;
void *_context;
CFIndex _deletes;
CFOptionFlags _xflags;
const void **_keys;
const void **_values;
};
複製代碼
從上面的數據結構能夠看出
NSDictionary
內部使用了兩個指針數組
分別保存keys
和values
。採用的是連續方式存儲鍵值對。拉鍊法會將key
和value
包裝成一個結果存儲(鏈表結點),而Dictionary
的結構擁有keys
和values
兩個數組(開放尋址線性探測法解決哈希衝突的用的就是兩個數組),說明兩個數據是被分開存儲的,不像是拉鍊法。
能夠看到,
NSDictionary
設置的key
和value
,key
值會根據特定的hash函數算出創建的空桶數組,keys
和values
一樣多,而後存儲數據的時候,根據hash函數
算出來的值,找到對應的index
下標,若是下標已有數據,開放定址法後移動插入,若是空桶數組到達數據閥值
,這個時候就會把空桶數組擴容
,而後從新哈希插入。這樣把一些不連續的
key-value
值插入到了能創建起關係的hash
表中,當咱們查找的時候,key
根據哈希>值算出來,而後根據索引,直接index
訪問hash
表keys
和hash
表values
,這樣查詢速度就能夠和連續線性存儲的數據同樣接近O(1)
了,只是佔用空間有點大,性能就很強悍。若是刪除的時候,也會根據_maker標記邏輯上的刪除,除非
NSDictionary
(NSDictionary
本體的hash
值就是count
)內存被移除。
NSDictionary
之因此採用這種設計, 其一出於查詢性能的考慮; 其二NSDictionary
在使用過程當中老是會很快的被釋放,不會長期佔用內存;
解決哈希衝突的拉鍊法和開放定址線性探測法,Apple都是用了。具體使用哪種是根據存儲數據的生命週期和特性決定的。
@synchronized
使用的是拉鍊法。拉鍊法多用於存儲的數據是通用類型,可以被反覆利用,就像@synchronized存儲的是鎖是一種無關業務的實現結構,程序運行時多個對象使用同一個鎖的機率至關高,有效的節省了內存。weak對象
associatedObject
採用的是開放定址線性探測法。開放定址線性探測法用於存儲的數據是臨時的,用完儘快釋放,就像associatedObject,weak。具體參考我寫的另外一篇博客:iOS筆記:進一步認識 ==、isEqual、hash
- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;
能夠看出key必須遵循NSCopy
協議,也就是說NSDictionary
的key是copy一份新的,而value是淺拷貝的(若是想深拷貝可使用NSMapTable)。NSObject
,而且重寫-(NSUInteger)hash;
和-(BOOL)isEqual:(id)object;
兩個方法。第一個函數用於計算hash值,第二個函數用來判斷當哈希值相同的時候value是否相同(解決哈希衝突)。NSDictionary和NSMutableArray底層原理(哈希表和環形緩衝區)