浅谈 iOS 字典 NSDictionary 的底层实现原理

0 阅读7分钟

CFDictionaryNSDictionary 是 Apple 框架中两套并行存在的“字典容器类型”,它们底层其实是同一个对象 CFBasicHash,只是提供了不同的接口层给 CObjective-C/Swift 使用。

Toll-Free Bridging(无缝桥接)

下面举一个例子来证明 CFDictionaryNSDictionary 是可以相互转换、强转的。


NSDictionary *dict = @{@"name": @"Zhang"};

CFDictionaryRef cfDict = (__bridge CFDictionaryRef)dict;
  

// 仍然可以调用 Core Foundation 的 API

CFIndex count = CFDictionaryGetCount(cfDict);

断点后输出下图

GNqZb70M1olAw3xkh7RcdGBknnc.png

图中可以看出 NSDictionary(类簇)的实际类型是 __NSSingleEntryDictionaryICFDictionaryRef 完全对应一个内存地址,所以两者完全可以 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.结构示意

索引 ipointers[1][i] (key)pointers[0][i] (value)
0key0value0
1key1value1
2key2value2
.........

参照:

笔记-集合 NSSet、字典 NSDictionary 的底层实现原理