At least there are special circumstances for which this unreliability kicks in.
Comparing [a hash] and [b hash] of two different NSString is safe when:
the strings' length is shorter or equal to 96 characters.
[a length] is different to [b length].
the concatinated first, middle, and last 32 characters of a differ to the concatinated components of b.
Otherwise every difference between the first and the middle 32 chars, as well as every difference between the middle and the last 32 characters, are not used while producing the [NSString hash] value.
[NSString hash] 这个方法对 <=96 个字符的字符串是安全的,如果比 96 个字符长,会大大增加碰撞的概率:
#define HashEverythingLimit 96
#define HashNextFourUniChars(accessStart, accessEnd, pointer) \
{result = result * 67503105 + (accessStart 0 accessEnd) * 16974593 + (accessStart 1 accessEnd) * 66049 + (accessStart 2 accessEnd) * 257 + (accessStart 3 accessEnd); pointer += 4;}
#define HashNextUniChar(accessStart, accessEnd, pointer) \
{result = result * 257 + (accessStart 0 accessEnd); pointer++;}
CF_INLINE CFHashCode __CFStrHashCharacters(const UniChar *uContents, CFIndex len, CFIndex actualLen) {
CFHashCode result = actualLen;
// ****X 这里HashEverythingLimit = 96
if (len <= HashEverythingLimit) {
// ****X 若字符串长度在96以内,对所有的字符做hash运算得到一个结果
const UniChar *end4 = uContents + (len & ~3);
const UniChar *end = uContents + len;
while (uContents < end4) HashNextFourUniChars(uContents[, ], uContents); // First count in fours
while (uContents < end) HashNextUniChar(uContents[, ], uContents); // Then for the last <4 chars, count in ones...
} else {
// ****X 若字符串长度超过96
const UniChar *contents, *end;
// ****X 取前32个字符做hash运算
contents = uContents;
end = contents + 32;
while (contents < end) HashNextFourUniChars(contents[, ], contents);
// ****X 取中间32个字符做hash运算
contents = uContents + (len >> 1) - 16;
end = contents + 32;
while (contents < end) HashNextFourUniChars(contents[, ], contents);
// ****X 取最后32个字符做hash运算
end = uContents + len;
contents = end - 32;
while (contents < end) HashNextFourUniChars(contents[, ], contents);
}
return result + (result << (actualLen & 31));
}
如果长度大于 96,只有前、中、后 32 个字符做了哈希运算,也就是说在这些字符相同的情况下,其他任意位置的字符发生改变,Hash 值都不会变。在工程中使用字符串的 hash 方法作为数据是否发生改变的依据,发现方法返回的 hash 值为一个很大的负数,这是因为数值大小超出了数据类型能够表达的范围。可以使用 md5 算法代替系统的 hash 算法。
十二、Hash 在 iOS 中的应用分析
① 关联对象 AssociatedObject 的底层实现采用的数据存储结构(Hash 表 + Hash 表)
关联对象采用的是 HashMap 嵌套 HashMap 的结构存储数据的,简单来说就是根据对象从第一个 HashMap 中取出存储对象所有关联对象的第二个 HashMap,然后根据属性名从第二个 HashMap 中取出属性对应的值和策略。设计关联对象的初衷:通过传入对象 + 属性名字,就可以找到属性值,方案设计实现好后,查找一个对象的关联对象的基本步骤:
已知条件一:对象,因此引出第一个 HashMap(AssociationsHashMap),用一个能唯一代表对象的值作为 key,用存储对象的所有关联对象的结构(名字:值+策略)作为 value;
已知条件二:属性名字,因此引出第二个 HashMap(ObjectAssociationMap),用属性名字作为key,用属性名字对应的结构体(值+策略)作为 value。
② weak 底层实现采用的数据存储结构(Hash 表 + 数组)
weak 采用的是一个全局的 HashMap 嵌套数组的结构存储数据的,销毁对象(weak 指针指向的对象)的时候,根据对象从 HashMap 中找到存放所有指向该对象的 weak 指针的数组,然后将数组中的所有元素都置为 nil。weak 的最大特点就是在对象销毁时,自动置 nil,减少访问野指针的风险,这也是设计 weak 的初衷。方案设计实现好后,weak 指针置 nil 的基本步骤:
对象 dealloc 的时候,从全局的 HashMap 中,根据一个唯一代表对象的值作为 key,找到存储所有指向该对象的 weak 指针的数组;
将数组中的所有元素都置为 nil。 深入了解请参考:iOS之深入解析weak关键字的底层原理。
③ KVO 底层实现采用的数据存储结构(Hash 表 + 数组)
一个对象可以被 n 个对象观察,一对象的 n 个属性又可以分别被 n 个对象观察。深入了解请参考:iOS之深入解析KVO的底层原理。
④ iOS App 签名的原理(MD5 + 哈希表 + 非对称加密 RSA)
一致性哈希算法 + 非对称加解密算法:
数字签名:传递数据时会将原始的数据和数字签名一起发送,对方拿到数据后,通过同样的 Hash 算法得到原始数据的 Hash 的值,然后通过非对称加密,将数字签名中的校验 Hash 值解密出来,最后对比两个 Hash 值是否一致。
代码签名:代码签名就是对可执行文件或脚本进行数字签名,用来确认软件在签名后未被修改或损坏的措施。它的原理和数字签名类似,只不过把签名的不是数据,而是代码。 深入了解请参考:iOS逆向之深入解析App签名的双向验证机制和原理。
⑤ 对象的引用计数存储的位置(Hash 表)
if 对象支持TaggedPointer {
return 直接将对象的指针值作为引用计数返回
}
else if 设备是64位环境 && Objective-C2.0 {
return 对象isa指针的一部分空间(bits_extra_rc)
}
else {
return hash表
}
深入了解请参考:
iOS之深入解析内存管理的引用计数retainCount的底层原理;
iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理。
⑥ NSDictionary 的实现原理(Hash 表)
NSMapTable 实现:
NSDictionary 是使用 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 存放到链表中,遍历链表的时间。
上面的结论和对应的解释似乎很合理?下面我们继续分析。 _CFDictionary 封装:– NSDictionary 是对 _CFDictionary 的封装,解决哈希冲突使用的是开放定址线性探测法:
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 使用开放定址线性探测法解决哈希冲突的原理: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 在使用过程中总是会很快的被释放,不会长期占用内存。 NSDictionary 的存储过程:
通过方法 - (void)setObject:(id)anObject forKey:(id)aKey; 可以看出 key 必须遵循 NSCopy 协议,也就是说 NSDictionary 的 key 是 copy 一份新的,而 value 是浅拷贝的(如果想深拷贝可以使用 NSMapTable)。
key 还必须要继承 NSObject,并且重写 -(NSUInteger)hash; 和 -(BOOL)isEqual:(id)object; 两个方法:
第一个函数用于计算 hash 值;
第二个函数用来判断当哈希值相同的时候 value 是否相同(解决哈希冲突)。
⑦ Runloop 与线程的存储关系
线程和 Runloop 之间是一一(子线程可以没有)对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 Runloop,如果你不主动获取,那它一直都不会有。Runloop 的创建是发生在第一次获取时,Runloop 的销毁是发生在线程结束时,只能在一个线程的内部获取其 Runloop(主线程除外)。Runloop 保存在一个全局的可变字典中,key 是 pthread_t,value 是 cfrunloopref,即 Hash 表。深入了解请参考:iOS之深入解析Runloop的底层原理。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)