CFDictionary
和 NSDictionary
是 Apple 框架中两套并行存在的“字典容器类型”,它们底层其实是同一个对象 CFBasicHash,只是提供了不同的接口层给 C 和 Objective-C/Swift 使用。
Toll-Free Bridging(无缝桥接)
下面举一个例子来证明 CFDictionary
和 NSDictionary
是可以相互转换、强转的。
NSDictionary *dict = @{@"name": @"Zhang"};
CFDictionaryRef cfDict = (__bridge CFDictionaryRef)dict;
// 仍然可以调用 Core Foundation 的 API
CFIndex count = CFDictionaryGetCount(cfDict);
断点后输出下图
图中可以看出 NSDictionary(类簇)的实际类型是 __NSSingleEntryDictionaryI
和 CFDictionaryRef
完全对应一个内存地址,所以两者完全可以 Toll-Free Bridging(无缝桥接),转换之后就可以调用 Core Foundation 的 API。
GNUStep 的底层实现
目前网上看到的 CFDictionary 的底层实现如下
struct __CFDictionary {
CFRuntimeBase _base;
CFIndex _count; /* number of values */
CFIndex _capacity; /* maximum number of values */
CFIndex _bucketsNum; /* number of slots */
uintptr_t _marker;
void *_context; /* private */
CFIndex _deletes;
CFOptionFlags _xflags; /* bits **for** GC */
const void **_keys; /* can be NULL **if** not allocated yet */
const void **_values; /* can be NULL **if** not allocated yet */
};
大家都知道,字典里的键值对 key-value 是一一对应的关系,从数据结构可以看出,key 和 value 是分别存储在两个不同的数组里,这里面是如何对 key、value 进行绑定的呢?
首先 key 利用 hash 函数算出 hash 值,然后对数组的长度取模,得到数组下标的位置,同样将这个地址对应到 values 数组的下标,就匹配到相应的 value。 注意到上面的这句话,要保证一点,就是 keys 和 values 这两个数组的长度要一致。
但是这个数据结构只是 GNUStep 的实现方式,并不等于苹果的实现方式。
苹果的底层实现方式
参照苹果最新源码(CF-1153.18),我们可以一探究竟,源码中并没有关于 CFDictionary
的直接定义位置,但是我们依然可以从 CFDictionary
的创建函数 ****** __CFDictionaryCreateGeneric**中发现蛛丝马迹。****
static CFBasicHashRef __CFDictionaryCreateGeneric(CFAllocatorRef allocator, const CFHashKeyCallBacks *keyCallBacks, const CFHashValueCallBacks *valueCallBacks, Boolean useValueCB) {
// 设置哈希表的初始标志,采用线性哈希方式
CFOptionFlags flags = kCFBasicHashLinearHashing; // kCFBasicHashExponentialHashing
// 根据是否为字典或Bag类型,设置是否有Key和Count
flags |= (CFDictionary ? kCFBasicHashHasKeys : 0) | (CFBag ? kCFBasicHashHasCounts : 0);
// 如果分配器支持垃圾回收(GC),则需要特殊处理回调和标志
if (CF_IS_COLLECTABLE_ALLOCATOR(allocator)) { // 下面的代码主要是为了历史兼容GC的标志设置
...
// 检查key和value的回调是否为标准回调或为空
if ((NULL == keyCallBacks || 0 == keyCallBacks->version) && (!useValueCB || NULL == valueCallBacks || 0 == valueCallBacks->version)) {
...
// 如果所有回调都是标准的,则设置set_cb和std_cb
if (keyRetainStd && keyReleaseStd && keyEquateStd && keyHashStd && keyDescribeStd && valueRetainStd && valueReleaseStd && valueEquateStd && valueDescribeStd) {
set_cb = true;
// 如果所有回调都不是NULL,则std_cb为true
if (!(keyRetainNull || keyReleaseNull || keyEquateNull || keyHashNull || keyDescribeNull || valueRetainNull || valueReleaseNull || valueEquateNull || valueDescribeNull)) {
std_cb = true;
} else {
// 仅设置这些变量以兼容历史GC逻辑
key_retain = keyCallBacks ? keyCallBacks->retain : NULL;
key_release = keyCallBacks ? keyCallBacks->release : NULL;
if (useValueCB) {
value_retain = valueCallBacks ? valueCallBacks->retain : NULL;
value_release = valueCallBacks ? valueCallBacks->release : NULL;
} else {
value_retain = key_retain;
value_release = key_release;
}
}
}
}
// ... 省略部分安全检查和回调指针初始化 ...
}
// 创建底层哈希表对象
CFBasicHashRef ht = CFBasicHashCreate(allocator, flags, &callbacks);
return ht;
}
可以看出,通过一些安全监测和回调指针初始化之后,CFDictionary 底层是由 CFBasicHashRef 实现的,CFBasicHashRef 是 struct __CFBasicHash * 的别名。******
探索 CFBasicHash 初始化
CFBasicHashCreate 的实现如下
CF_PRIVATE CFBasicHashRef CFBasicHashCreate(CFAllocatorRef allocator, CFOptionFlags flags, const CFBasicHashCallbacks *cb) {
size_t size = sizeof(struct __CFBasicHash) - sizeof(CFRuntimeBase);
if (flags & kCFBasicHashHasKeys) size += sizeof(CFBasicHashValue *); // keys
if (flags & kCFBasicHashHasCounts) size += sizeof(void *); // counts
if (flags & kCFBasicHashHasHashCache) size += sizeof(uintptr_t *); // hashes
CFBasicHashRef ht = (CFBasicHashRef)_CFRuntimeCreateInstance(allocator, CFBasicHashGetTypeID(), size, NULL);
if (NULL == ht) return NULL;
ht->bits.finalized = 0;
ht->bits.hash_style = (flags >> 13) & 0x3;
ht->bits.fast_grow = (flags & kCFBasicHashAggressiveGrowth) ? 1 : 0;
ht->bits.counts_width = 0;
ht->bits.strong_values = (flags & kCFBasicHashStrongValues) ? 1 : 0;
ht->bits.strong_keys = (flags & kCFBasicHashStrongKeys) ? 1 : 0;
ht->bits.weak_values = (flags & kCFBasicHashWeakValues) ? 1 : 0;
ht->bits.weak_keys = (flags & kCFBasicHashWeakKeys) ? 1 : 0;
// ... 省略部分安全检查和回调指针初始化 ...
uint64_t offset = 1;
ht->bits.keys_offset = (flags & kCFBasicHashHasKeys) ? offset++ : 0;
ht->bits.counts_offset = (flags & kCFBasicHashHasCounts) ? offset++ : 0;
ht->bits.hashes_offset = (flags & kCFBasicHashHasHashCache) ? offset++ : 0;
for (CFIndex idx = 0; idx < offset; idx++) {
ht->pointers[idx] = NULL;
}
return ht;
}
源码中先计算了结构体所需内存大小 size
-
先计算 ****** __**CFBasicHash**** 结构体本身(不含 CoreFoundation 头部 CFRuntimeBase)的大小。****
-
如果需要存储 key/count/hashCache,则为 pointers 数组分配更多空间(每种类型多一个指针)。
然后分配内存创建对象
-
调用 _CFRuntimeCreateInstance 分配内存并初始化 CoreFoundation 对象头部。**
-
返回的 ht 是一个指向 __CFBasicHash 结构体的指针。****
-
如果分配失败,直接返回 NULL。
最后计算并设置各数据块在 pointers 数组中的偏移
-
pointers[0] 总是用于 value 数组。
-
如果有 key/count/hashCache,则依次分配在 pointers[1]、pointers[2]、pointers[3] 等。
-
没有的部分 offset 就是 0,相关指针不会被用到。
解开 CFBaiscHash 的神秘面纱
struct __CFBasicHash {
CFRuntimeBase base; // CoreFoundation对象的基础头部,包含类型信息和引用计数等
struct { // 192 bits
uint16_t mutations; // 哈希表变更次数(用于检测并发修改)
uint8_t hash_style:2; // 哈希算法风格(如线性、指数等)
uint8_t keys_offset:1; // key数组在pointers中的偏移(有key时为1,否则为0)
uint8_t counts_offset:2; // 计数数组在pointers中的偏移(有计数时为2,否则为0)
uint8_t counts_width:2; // 计数单元宽度(如8/16/32/64位)
uint8_t hashes_offset:2; // 哈希缓存数组在pointers中的偏移
uint8_t strong_values:1; // 是否对value使用强引用
uint8_t strong_keys:1; // 是否对key使用强引用
uint8_t weak_values:1; // 是否对value使用弱引用
uint8_t weak_keys:1; // 是否对key使用弱引用
uint8_t int_values:1; // value是否为整数类型
uint8_t int_keys:1; // key是否为整数类型
uint8_t indirect_keys:1; // 是否为间接key(如value中包含key)
uint32_t used_buckets; // 已使用的桶数量
uint64_t deleted:16; // 已删除的桶数量
uint64_t num_buckets_idx:8; // 桶数量的索引(实际桶数量=查表获得)
uint64_t __kret:10; // key的retain回调在全局回调表中的索引
uint64_t __vret:10; // value的retain回调在全局回调表中的索引
uint64_t __krel:10; // key的release回调在全局回调表中的索引
uint64_t __vrel:10; // value的release回调在全局回调表中的索引
uint64_t __:1; // 保留位
uint64_t null_rc:1; // 是否禁用引用计数
uint64_t fast_grow:1; // 是否启用快速扩容
uint64_t finalized:1; // 是否已析构
uint64_t __kdes:10; // key的描述回调在全局回调表中的索引
uint64_t __vdes:10; // value的描述回调在全局回调表中的索引
uint64_t __kequ:10; // key的比较回调在全局回调表中的索引
uint64_t __vequ:10; // value的比较回调在全局回调表中的索引
uint64_t __khas:10; // key的哈希回调在全局回调表中的索引
uint64_t __kget:10; // key的间接获取回调在全局回调表中的索引
} bits; // 哈希表的元数据和状态位
void *pointers[1]; // 指向实际数据区(value、key、count、hashes等)的指针数组
};
最重要的字段就是 pointers 指针数组
-
pointers[0] 指向 value(值)数组
-
pointers[1](如果有 key)指向 key(键)数组
可以看出 value 数组和 key 数组是单独的数组,这跟其他语言的键值对实现方式(如下)都不太一样,至于原因,我猜想可能 NSSet 的底层实现也是基于 CFBasicHash,出于兼容性的考虑。
struct Bucket{
Key key;
Value value;
};
那如何实现键值对的一一对应呢? 可以参考上述 GunStep 中的实现,这里详细讲下。
1. 索引一致性
-
同一个索引 i,pointers[0][i] 存储第 i 个 value,pointers[1][i] 存储第 i 个 key。
-
也就是说,key 和 value 在各自数组中的索引是完全一致的。
2. 查找/插入流程
-
当插入一个键值对时,哈希算法会确定一个桶索引 i。
-
key 存到 pointers[1][i],value 存到 pointers[0][i]。
-
查找时,先用 key 计算哈希,找到索引 i,然后比较 pointers[1][i] 是否等于目标 key,若相等则 pointers[0][i] 就是对应的 value。
3.结构示意
索引 i | pointers[1][i] (key) | pointers[0][i] (value) |
---|---|---|
0 | key0 | value0 |
1 | key1 | value1 |
2 | key2 | value2 |
... | ... | ... |
参照: