NSDictionary实现原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NSDictionary实现原理相关的知识,希望对你有一定的参考价值。

参考技术A NSDictionary(字典)是使用hash表来实现key和value之间的映射和存储的

方法:- (void)setObject:(id)anObject forKey:(id)aKey;

Objective-C中的字典NSDictionary底层其实是一个哈希表

散列表 ( Hash table ,也叫 哈希表 ),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个 映射函数 叫做 散列函数 ,存放记录的数组叫做 散列表 。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。

哈希表还有一个重要的属性: 负载因子(load factor) ,它用来衡量哈希表的空/满程度,一定程度上也可以体现查询的效率,计算公式为:

负载因子 = 总键值对数 / 箱子个数

负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将 自动扩容 。

哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。

基于以上总结,哈希表的两个问题:

NSDictionary实现原理-ios哈希hash和isEqual

NSDictionary实现原理-ios哈希hash和isEqual

 
OC中自定义类的NSCopying实现的注意事项(isEqual & hash实现)
 
iOS开发 之 不要告诉我你真的懂isEqual与hash!

       NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。数据在hash表中分布的越均匀,其访问效率越高。而在Objective-C中,通常都是利用NSString 来作为键值,其内部使用的hash函数也是通过使用 NSString对象作为键值来保证数据的各个节点在hash表中均匀分布。

见NSDictionary中最常用的一个方法原型:

 

[objc] view plain copy
 
  1. - (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;  

 

从这个方法中可以知道, 要作为 Key 值,必须遵循 NSCopying 协议。这是因为在NSDictionary内部,会对 aKey 对象 copy 一份新的。而  anObject 对象在其内部是作为强引用(retain或strong)。
 

既然知道了作为 key 值,必须遵循 NSCopying 协议,说明除了 NSString 对象之外,我们还可以使用其他类型对象来作为 NSDictionary 的 key值。不过这还不够,作为 key 值,该类型还必须继承于 NSObject 并且要重载一下两个方法:

 

[objc] view plain copy
 
  1. - (NSUInteger)hash;  
 
[objc] view plain copy
 
  1. - (BOOL)isEqual:(id)object;  
 
 
其中,hash 方法是用来计算该对象的 hash 值,最终的 hash 值决定了该对象在 hash 表中存储的位置。我们重写hash方法是因为每向NSDictionary和NSSet中存入一个key-value,字典会先利用即将插入的key的hash和字典中已经存在的所有的key.hash进行比较,最终来决定是新增一个key,还是覆盖原有的key。 但是仅仅使用key.hash比较,有时会出现2个对象hash相同的情况,这时候就需要调用isEqual 方法来最终裁定,2个key对象是否相同。
 
 
 

在OC中,如果自定义类,则要考虑赋值、持久化保存、保存到其它容器中等各种情况的对象复制和比较,下面是一个比较全面的自定义例子,在此仅作记录:

自定义类:

KeyValuePairs.h:

 

[objc] view plain copy
 
  1. #import <Foundation/Foundation.h>  
  2.   
  3. @interface KeyValuePairs: NSObject <NSCopying>  
  4. @property (nonatomic,strong)NSString *identifier;  
  5. @property (nonatomic,strong)NSString *name;  
  6.   
  7. @end  
KeyValuePairs.m:

 

[objc] view plain copy
 
  1. #import "KeyValuePairs.h"  
  2.   
  3. @implementation KeyValuePairs  
  4.   
  5. - (id)copyWithZone:(NSZone *)zone  
  6. {  
  7.     KeyValuePairs *kvp = [[[self class] allocWithZone:zone] init];  
  8.     kvp.identifier = self.identifier;  
  9.     kvp.name = self.name;  
  10.     return kvp;  
  11. }  
  12.   
  13. - (BOOL)isEqualToKeyValuePairs:(KeyValuePairs *)kvp{  
  14.     if (!kvp) {  
  15.         return NO;  
  16.     }  
  17.     BOOL haveEqualName = (!self.name && !kvp.name) || [self.name isEqualToString:kvp.name];  
  18.     BOOL haveEqualIdentifier = (!self.identifier && !kvp.identifier) || [self.identifier isEqualToString:kvp.identifier];  
  19.       
  20.     return haveEqualName && haveEqualIdentifier;  
  21. }  
  22.   
  23. #pragma mark -NSObject  
  24. -(BOOL)isEqual:(id)object{  
  25.     if (self == object) {  
  26.         return YES;  
  27.     }  
  28.     if (![object isKindOfClass:[KeyValuePairs class]]) {  
  29.         return NO;  
  30.     }  
  31.     return [self isEqualToKeyValuePairs:(KeyValuePairs *)object];  
  32. }  
  33.   
  34. - (NSUInteger)hash {  
  35.     return [self.name hash] ^ [self.identifier hash];  
  36. }  
  37.   
  38. @end  

 

 

测试:

 

[objc] view plain copy
 
  1. NSMutableDictionary *namesWillUpdateDic = [[NSMutableDictionary alloc] init];  
  2. NSMutableArray *names = [[NSMutableArray alloc] init];  
  3. for (int i = 0; i<1000; i++) {  
  4.     NSString *name = [NSString stringWithFormat:@"%d_zhangsan",i];  
  5.     NSString *identifier = [NSString stringWithFormat:@"%d_identifier",i];  
  6.     NSString *strObj = [NSString stringWithFormat:@"%d_strObj",i];  
  7.     KeyValuePairs *kvp = [[KeyValuePairs alloc] init];  
  8.     kvp.identifier = identifier;  
  9.     kvp.name = name;  
  10.     [namesWillUpdateDic setObject:strObj forKey:kvp];  
  11.     [names addObject:kvp];  
  12. }  
  13.   
  14. for (int j = 0; j<1000; j++) {  
  15.     int index = arc4random()%1000;  
  16.     KeyValuePairs *kvp = [names objectAtIndex:index];  
  17.     NSString *strObj = [namesWillUpdateDic objectForKey:kvp];  
  18.     NSString *msg = [NSString stringWithFormat:@"index:%d,identifier:%@,email:%@,strObj:%@",index,kvp.identifier,kvp.name,strObj];  
  19.     NSLog(@"%@",msg);  
  20. }  
 
 
 
注意:
1、自定义类为什么一定要实现NSCopying协议呢?这是因为通过key-value把2个对象加入到字典中,字典会对key进行copy一份的操作,而对value对象进行retain操作,如果自定义的类不实现copy协议,那么就不能作为字典的key对象使用。
2、如果自定义类不重写isEqual则默认使用内存地址比较两个对象,可能会出现意想不到的结果
3、isEqual和hash方法要同时重写,否则isEqual方法判断将不正确
 
技术分享
 
 

以上是关于NSDictionary实现原理的主要内容,如果未能解决你的问题,请参考以下文章

NSDictionary底层实现原理

iOS 字典的实现原理

AVAudioRecorder 记录空白文件

我用 NSDictionary & JSON 序列化 & NSUserDefaults 错误地实现了啥?

具有多个日期条目(每年大约 800 多个)的核心数据或 NSDictionary?啥是最容易实现的?

NSDictionary+JSON - iOS