惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
罗磊的独立博客
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
The Register - Security
The Register - Security
IT之家
IT之家
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Help Net Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
NISL@THU
NISL@THU
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
B
Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
S
Security Affairs
小众软件
小众软件
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
A
Arctic Wolf
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

博客园 - chaoguo1234

如何自定义 UIScrollView 的减速系数 Plist 二进制格式 UIImageView 设置图片不生效的原因排查 Any metadata 的内存布局 Mach-O 文件结构 Block Copy 的内存布局详解 __block 变量内存布局详解 Block 内存布局详解 XNU Inside: iOS 模拟器 WebKit Insie: WebKit 调试(二) NSMutableDictionary 的内存布局 WebKit Inside: GPU 进程调试 WebKit Inside: 渲染树 WebKit Inside: px 与 pt WebKit Inside: CSS 的匹配原理 dyld: 神秘的 __dso_handle 一文搞懂 ARM 64 系列: ADD(立即数版) 一文搞懂 ARM 64: AUTIBSP 一文搞懂 ARM 64 系列: PACISB XCode 编译 PAG 源码 一文搞懂 ARM 64 系列: 一文搞懂 ARM 64 系列: 函数调用传参与返回值 一文搞懂 ARM 64 系列: 寄存器
NSDictionary 内存布局
chaoguo1234 · 2025-06-02 · via 博客园 - chaoguo1234

NSDictionaryiOS开发中经常用到的数据结构。

熟悉NSDictionary的内部实现,有助于我们更好的使用它们。

同时,在遇到相关崩溃,也能帮助我们更好的分析问题。

1 类簇

非可变字典由NSDictionary表示。

可变字典由NSMutableDictionary表示。

按照苹果官方文档的说法,NSDictionaryNSMutableDictionary都是类簇。

也就是说,NSDictionaryNSMutableDictionary只是暴露的公共接口,具体实现由内部众多私有子类完成。

image

2 类图

image

3 NSDictionary

下面介绍各个非可变字典的内存布局。

3.1 __NSDictionary0

__NSDictioanry0里面没有任何元素。

NSDictionary *dict = @{};

NSDictionary *dict = [NSdictionary dictionary];

上面代码都会创建一个__NSDictionary0

(lldb) p dict
(__NSDictionary0 *) 0x00000001e3dd2390 0 key/value pairs

3.2 NSConstantDictionary

如果字典初始化时key-value对都是字符串常量,那么就会得到一个NSConstantDictionary

NSDictionary *dict = @{
  @"kaaa": @"aaa",
  @"kbbb": @"bbb",
  @"kccc": @"ccc",
  @"kddd": @"ddd",
};

上面代码会创建一个NSConstantDictionary

(lldb) p dict
(NSConstantDictionary *) 0x00000001021b87c8 4 key/value pairs

如果key不全是字符串,也不会得到NSConstantDictionary:

NSDictionary *dict = @{
        @1: @"aaa",
        @2: @"bbb",
        @3: @"ccc",
        @4: @"ddd",
    };

上面代码会得到一个__NSDictionaryI:

(lldb) p dict
(__NSDictionaryI *) 0x0000600002c0af80 4 key/value pairs

3.2.1 内存布局

NSConstantDictionary的内存布局如下图所示:

image

isa指向对应的类对象。

options在调试时只遇到过值为1的情形,表示字典的key全是字符串。

当调用-[NSDictionary objectForKey:]方法时,如果参数不是字符串,不会处理:

  -[NSConstantDictionary objectForKey:]:
  ...
  // 1. x21 中存储方法参数
  0x180430b60 <+120>: mov    x0, x21
  // 2. w23 存储的计时 options 的值
  0x180430b64 <+124>: tbnz   w23, #0x1, 0x180430b8c    ; <+164>
  // 3. 判断参数是否是字符串
  0x180430b68 <+128>: bl     0x1804cf7ac               ; _NSIsNSString
  ...

代码注释1,寄存器x21存储方法参数,传递给寄存器x0,作为下面函数_NSIsNSString的参数。

代码注释2,寄存器w23存储options的值,options1,才会调用下面的函数_NSIsNSString方法。

代码注释3,调用_NSIsNSString方法对参数进行校验。

count存储字典中key-value的个数

keys是一个指针,指向字典中key所在的数组。

values是一个指针,指向字典中value所在的数组。

3.2.2 objectForKey:

使用objectForKey:方法读取一个key对应的value时:

1 使用二分法从keys数组中找到对应key所在的索引;

2values数组中根据索引返回对应的value值。

  -[NSConstantDictionary objectForKey:]:
  ...
  // 1. 调用二分法寻找参数在 keys 数组中的地址
  0x180430c58 <+368>: bl     0x180547f18               ; symbol stub for: bsearch
  0x180430c5c <+372>: cbz    x0, 0x180430c6c           ; <+388>
  // 2. 计算参数在 keys 数组中的索引
  0x180430c60 <+376>: sub    x8, x0, x19
  // 3. 获取 value 在 values 数组中地址
  0x180430c64 <+380>: add    x22, x22, x8
  // 4. 获取 value 值
  0x180430c68 <+384>: ldr    x0, [x22]
  ...

代码注释1,调用二分法bsearch获取参数keykeys数组中所在地址,存储到x0

代码注释2x19指向keys数组首地址,这里计算出参数keykeys数组中的偏移量,也就是对应索引。

代码注释3x22指向values数组首地址,这里计算出valuevalues数组中的地址。

代码注释4,从values数组中加载出value值,存储到x0

image

3.3 __NSSingleEntryDictionaryI

如果字典中只有一个key-value对,就会得到__NSSingleEntryDictionaryI

 NSDictionary *dict = @{
        @5: @"555",
 };

上面代码会创建一个__NSSingleEntryDictionaryI:

(lldb) p dict
(__NSDictionaryI *) 0x0000600002c0af80 4 key/value pairs

但是,如果key是字符串,得到的还是NSConstantDictionary:

NSDictionary *dict = @{
        @"5": @"555",
};

上面代码会创建一个NSConstantDictionary:

(lldb) p dict
(NSConstantDictionary *) 0x000000010445c7d8 1 key/value pair

3.3.1 内存布局

__NSSingleEntryDictionaryI的内存局部如下:

image

isa指向对应类对象。

key是一个指针,指向对应的key

value是一个指针,指向对应的value值。

3.3.2 objectForKey:

__NSSingleEntryDictionaryI调用objectForKey:比较简单:

1 比较参数是否和存储的key值相等;

2 如果相等,就将存储的value返回。

3.4 __NSDictionaryI

大多数情况下,创建的NSDictionary对象,对应的类都是__NSDictionaryI

3.4.1 初始化

通常我们会使用下面的函数初始化一个NSDictionary对象:

NSDictionary *dict = @{
    @"kaaa": @"aaa",
    @"kbbb": @"bbb",
    @"kccc": @"ccc",
    @"kddd": @"ddd",
};

NSDictionary *dictI = [NSDictionary dictionaryWithDictionary:dict];

上面函数会创建一个__NSDictionaryI对象:

(lldb) p dictI
(__NSDictionaryI *) 0x0000600002c07f80 4 key/value pairs

下面我们来看一下+[NSDictionary dictionaryWithDictionary:]方法的初始化过程。

CoreFoundation`+[NSDictionary dictionaryWithDictionary:]:
->  ...
    0x1804b81a4 <+12>: mov    x19, x2
    // 1. 调用 objc_alloc 方法
    0x1804b81a8 <+16>: bl     0x1805488cc               ; symbol stub for: objc_alloc
    0x1804b81ac <+20>: mov    x2, x19
    0x1804b81b0 <+24>: mov    w3, #0x0                  ; =0 
    // 2. 调用 initWithDictionary:copyItems: 方法
    0x1804b81b4 <+28>: bl     0x180757aa0               ; objc_msgSend$initWithDictionary:copyItems:
    ...

代码注释1,调用objc_alloc为一个NSDictionary对象的内存空间。

代码注释2,调用initWithDictionary:copyItems:方法初始化第1步分配的内存空间。

但是当断点到initWithDictionary:copyItems:方法时,发现调用的是-[__NSPlaceholderDictionary initWithDictionary:compyItems:]方法,而不是期望的-[__NSDictionaryI initWithDictionary:copyItems:]方法。

CoreFoundation`-[__NSPlaceholderDictionary initWithDictionary:copyItems:]:
->  0x180528b2c <+0>:   sub    sp, sp, #0x60
    0x180528b30 <+4>:   stp    x24, x23, [sp, #0x20]
    0x180528b34 <+8>:   stp    x22, x21, [sp, #0x30]
    0x180528b38 <+12>:  stp    x20, x19, [sp, #0x40]
    0x180528b3c <+16>:  stp    x29, x30, [sp, #0x50]

那就说明,方法objc_alloc分配了一个__NSPlaceholderDictionary对象。

从上面的类图可以知道,__NSPlaceholderDictionary继承自NSMutableDictionary

非可变字典从可变字典初始化而来,出乎意料之外

下面就来看下objc_alloc的实现。

objc_alloc函数源码位于objc4中的NSObject.mm文件中。

但是我们还是从汇编角度来看一下它的实现。

libobjc.A.dylib`objc_alloc:
    ...
    // 1. 获取 isa 指针
    0x1800917dc <+4>:  ldr    x8, [x0]
    // 2. 掩码运算,剔除 isa 指针中的多余值
    0x1800917e0 <+8>:  and    x8, x8, #0x7ffffffffffff8
    // 3. 加载 AWZ 标志位
    0x1800917e4 <+12>: ldrh   w8, [x8, #0x1e]
    // 4. 判断是否没有设置 AWZ 标志
    0x1800917e8 <+16>: tbz    w8, #0xe, 0x1800917f4     ; <+28>
    // 5. 有 AWZ 标志位,就跳转执行 _objc_rootAllocWithZone 函数
    0x1800917ec <+20>: b      0x180086eec               ; _objc_rootAllocWithZone
    0x1800917f0 <+24>: ret    
    0x1800917f4 <+28>: adrp   x8, 482527
    // 6. 如果没有设置了 AWZ 标志,执行 allocWithZone: 方法
    0x1800917f8 <+32>: add    x1, x8, #0x6e0
    0x1800917fc <+36>: b      0x18006b400               ; objc_msgSend

代码注释1,获取isa指针。

由于我们调用objc_alloc传入的是NSDictionary.class对象,所以这里的isa指针指向NSDictionary.class的元类。

代码注释2,对isa指针做掩码运算,剔除不相干的位。

众所周知,iOS中的isa并不是所有的bit都是类对象指针,有些bit用作了其他用处。

iOS 12又引入了PAC指针验证机制,isa各个bit的使用有了变化。

下面是objc4源码中,对isa指针的最新定义:

// isa.h
...
#   elif __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                           
        uintptr_t nonpointer        : 1;  // 此标志为 1,表明 isa 指针并不是纯粹的类指针                            
        uintptr_t has_assoc         : 1;  // 是否有关联对象             
        uintptr_t weakly_referenced : 1;  // 是否有弱引用             
        uintptr_t shiftcls_and_sig  : 52;  // 真正的类指针             
        uintptr_t has_sidetable_rc  : 1;  // 是否启用了 sidetable 来引用计数                  
        uintptr_t extra_rc          : 8 // 优先使用 8 bit 进行引用计数

从定义中可以看到,isa指针中,只有52bit用于真正的类指针。

因此,isa指针的掩码为0x7ffffffffffff8,刚好521

代码注释3,加载NSDictionary的元类中的AWZ标志。

AWZ就是AllocWithZone的简写。

如果设置了AWZ标志,就说明这个类用默认的alloc或者allocWithZone:方法。

如果不设置AWZ标志,那就说明这个类对于alloc或者allocWithZone:方法有自己的实现。

我们可以看到在objc4源码中有对应的注释:

// objc-runtime-new.h
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#   define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)

这个标志为0,说明该类自定义了alloc或者allocWithZone:方法。

那这个标志存在什么地方法呢?

objc4源码可知,这个标志存在元类对象的flags属性中:

image

这个flags属性偏移元类对象首地址0x1e个字节。

代码注释4,判断是否没有设置AWZ标志。

从第3步介绍可知,AWZ标志设置在第14位,tbz指令查看flags的第14位是否为0

代码注释5,如果设置了AWZ标志,那么使用使用默认的alloc或者allocWithZone:方法。

代码注释6,如果没有设置AWZ标志,那么就说明NSDictionary有自定义的alloc或者allocWithZone:方法。

x1寄存器存储着objc_msgsend要调用的方法名,打印可知,这个方法正是alloc:

(lldb) po (char *)$x1
"alloc"

最终,方法会调用到+[NSDictionary allocWithZone:]方法。

下面来看一下+[NSDictionary allocWithZone:]的实现。

CoreFoundation`+[NSDictionary allocWithZone:]:
    ...
    0x1804b6d04 <+28>:  adrp   x8, 407836
    0x1804b6d08 <+32>:  ldr    x8, [x8, #0x600]
    // 1. 比较当前类对象是否是 NSDictionary
->  0x1804b6d0c <+36>:  cmp    x8, x0
    0x1804b6d10 <+40>:  b.eq   0x1804b6d64               ; <+124>
    0x1804b6d14 <+44>:  adrp   x8, 407836
    0x1804b6d18 <+48>:  ldr    x8, [x8, #0x608]
    // 2. 比较当前类对象是否是 NSMutableDictionary
    0x1804b6d1c <+52>:  cmp    x8, x0
    0x1804b6d20 <+56>:  b.eq   0x1804b6d88               ; <+160>
    ...
    // 3. 当前类是 NSDictionary,将执行 __NSDictionaryImmutablePlaceholder 方法
    0x1804b6d84 <+156>: b      0x180528728               ; __NSDictionaryImmutablePlaceholder
    ...
    // 4. 当前类是 NSMutableDictionary,将执行 __NSDictionaryMutablePlaceholder 方法
    0x1804b6da8 <+192>: b      0x180528734               ; __NSDictionaryMutablePlaceholder

代码注释1,寄存器x8存储NSDictionary的类地址,寄存器x0存储当前类地址。

这里比较当前类地址是否是NSDictionary类。

如果比较成功,就会跳转执行__NSDictionaryImmutablePlaceholder 方法。

代码注释2,寄存器x8存储NSMutableDictionary的类地址。

这里比较当前类地址是否是NSMutableDictionary类。

如果比较成功,就会跳转执行__NSDictionaryMutablePlaceholder方法。

由于我们现在创建非可变字典,因此,代码最终会执行__NSDictionaryImmutablePlaceholder方法。

__NSDictionaryImmutablePlaceholder方法很简单,直接返回一个__NSPlaceholderDictionary对象:

CoreFoundation`__NSDictionaryImmutablePlaceholder:
    0x180528728 <+0>: adrp   x0, 407690
    0x18052872c <+4>: add    x0, x0, #0x338            ; ___immutablePlaceholderDictionary
->  0x180528730 <+8>: ret 

打印返回的对象:

(lldb) po [$x0 class]
__NSPlaceholderDictionary

顺便看一下__NSDictionaryMutablePlaceholder方法:

CoreFoundation`__NSDictionaryMutablePlaceholder:
    0x180528734 <+0>: adrp   x0, 407690
    0x180528738 <+4>: add    x0, x0, #0x348            ; ___mutablePlaceholderDictionary
->  0x18052873c <+8>: ret

方法也很简单,也是直接返回一个__NSPlaceholderDictionary对象:

(lldb) po [$x0 class]
__NSPlaceholderDictionary

__NSPlaceholderDictionary对象的创建流程我们已经清楚了。

接下来,继续看-[__NSPlaceholderDictionary initWithDictionary:copyItems:]方法。

汇编代码不看了,直接上伪代码:

@interface __NSPlaceholderDictionary
...
@end

@implementation

- (instancetype)initWithDictionary:(NSDictionary *)dict copyItems:(BOOL)shouldCopy {
  if (dict.class != __NSDictionaryI.class && dict.class != __NSDictionaryM.class && dict.class != __NSFrozenDictionaryM.class) {
  return [super initWithDictionary:dict copyItems:shouldCopy];
  }
  
  if (self == ___mutablePlaceholderDictionary) {
    return [dict mutableCopyWithZone:0];
  }
  
  if (self == ___immutablePlaceholderDictionary) {
    return [dict copyWithZone:0];
  }
  
}
@end

__NSDictionaryM__NSFrozenDictionaryM在介绍可变字典时会涉及。

字典的拷贝在介绍完可变与非可变字典后会涉及。

由于本次例子中,我们使用的是一个NSConstantDictionary进行初始化,因此会调用到-[super initWithDictionary:copyItems:]方法。

__NSPlaceholderDictionarysuper中,NSDictionary实现了这个方法。

-[NSDictionary initWithDictionary:copyItems:]不看汇编了,伪代码如下:

@interface NSDictionary
...
@end

@implementation

- (instancetype)initWithDictionary:(NSDictionary *)dict copyItems:(BOOL)shouldCopy {
  NSInteger count = dict.count;
  if (count >= 2^60) {
    // 创建的字典 key-value 对不能超过 2^60
    error "attempt to create a temporary id buffer which is too large or with a negative count (%lu) -- possibly data is corrupt"
  }
  
  NSObject *keys = nil;
  NSObject *objects = nil;
  if (count <= 0x100) {
    // key-value 对数量 <= 256,在栈上分配空间
    NSObject * keysArr[count];
    NSObject * objectsArr[count];
    keys = keysArr;
    objects = objectsArr;
  } else {
   // key-value 对数量 > 256,在堆上分配空间
   keys = _CFCreateArrayStorage(count, 0);
   objects = _CFCreateArrayStorage(count, 0);
  }
  
  // 读取参数 dict 的 keys 和 objects 到分配的数组中
  [dict getObjects:objects keys:keys count:count];
  
  if (count != 0 && shouldCopy) {
    // 拷贝 key-value 对
    for (NSInteger i = 0; i < count; i++) {
      NSObject *key = keys[i];
      keys[i] = [key copyWithZone:nil];
    }
    
    for (NSInteger i = 0; i < count; i++) {
      NSObject *object = objects[i];
      objects[i] = [object copyWithZone:nil];
    }
  }
  
  return [self initWithObjects:objects forKeys:keys count:count]; 
}

从上面伪代码可以看到,最终会调用NADictionaryinitWithObjects:forKeys:count:方法完成初始化。

initWithObjects:forKeys:count:正是NSDictionarydesignated initializer方法。

下面就来看下这个方法。

由于此时的self真正的类型为__NSPlaceholderDictionary,所以此时真正调用的方法为-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]

下面我们就来看这个方法的伪代码:

// -[__NSPlaceholderDictionary initWithObjects:forKeys:count]

@interface __NSPlaceholderDictionary

...

@end

@implementation __NSPlaceholderDictionary

- (instancetype)initWithObjects:(ObjectType const[])objects forKeys:(ObjectTpye const[])keys count:(NSUInteger)count {
  if (keys == nil && count == 0) {
    goto label;
  }
  
  if (keys == nil && count != 0) {
    // 报错
    error "pointer to objects array is NULL but length is {count}";
  }
  
  if (keys != nil && count == 0 {
    goto label;
  }
  
  if (keys != nil && count != 0) {
    // 检测 keys 数组里的值是否有 nil
    for (NSInteger i = 0; i < count; i++) {
      ObjectType key = keys[i];
      if (key == nil) {
        // 报错
        error "attempt to insert nil object from objects{[i]}";
      }
    }
  }
  
  if (objects == nil && count == 0) {
    goto label;
  }
  
  if (objects == nil && count != 0) {
    // 报错
    error "pointer to objects array is NULL but length is {count}";
  }
  
  if (objects != nil && count == 0) {
    goto label;
  }
  
  if (objects != nil && count != 0) {
    // 检测 objects 数组里是否有 nil
    for (NSInteger i = 0; i < count; i++) {
      ObjectType object = objects[i];
      if (object == nil) {
        error "attempt to insert nil object from objects{[i]}";
      }
    }
  }
  
  label:
  if (self == ___immutablePlaceholderDictionary) {
    if (count == 0) {
      // 创建 __NSDictionary0
      return __NSDictionary0__();
    }
    
    if (count == 1) {
      // 创建 __NSSingleEntryDictionaryI
      return __NSSingleEntryDictionaryI_new(keys[0], objects[0], 1);
    }
    
    // 创建 __NSDictionaryI
    return __NSDictionaryI_new(keys, objects, 0, count, 1);
  } else if (self == ___mutablePlaceholderDictionary) {
    // 创建 __NSDictionaryM
    return __NSDictionaryM_new(keys, objecs, count, 3);
  }
  
  error "创建出错"
}

上面伪代码中,___immutablePlaceholderDictionary___mutablePlaceholderDictionary在前面介绍alloc方法时提到过。

这里重点看下__NSDictionryI_news方法。

__NSDictionaryI_news内部首先根据count值,遍历一个全局数组__NSDictionaryCapacities

__NSDictionaryCapacities总共有64项,每一项代表字典的capacity:

0x1803cc548 <+72>:  adrp   x8, 451
0x1803cc54c <+76>:  add    x8, x8, #0xc88            ; __NSDictionaryCapacities

Xcodelldb查看其内容为:

(lldb) x/64g $x8
0x18058fc88: 0x0000000000000000 0x0000000000000003
0x18058fc98: 0x0000000000000006 0x000000000000000b
0x18058fca8: 0x0000000000000013 0x0000000000000020
0x18058fcb8: 0x0000000000000034 0x0000000000000055
...
0x18058fe78: 0xc1d7fb9980000000 0xc2625e72e7800000

从上面的输出可以看到:

0项的值为0

1项的值为3

63项的值为0xc2625e72e7800000,已经非常大了。

遍历__NSDictionaryCapacity数组的目的,是为了找到一个索引,这个索引对应的capacity大于或者等于count

对应的伪代码为:

BOOL found = NO;
NSInteger index = 0;
for (; index < 64; index++) {
  if (__NSDictionaryCapacity[i] >= count) {
    found = YES;
    break;
  }
}

if (!found) {
  error "不能创建 NSDictionary";
}

如果遍历了全部的64想,仍然没有满足条件的索引,那么程序就会crash

需要注意的是,__NSDictionaryCapacity中存储的capacity,并不是要创建的字典的大小。

要创建的字典的大小,存储在全局变量__NSDictionarySizes中:

0x1803cc56c <+108>: adrp   x8, 451
0x1803cc570 <+112>: add    x8, x8, #0xb40            ; __NSDictionarySizes

Xcodelldb中查看其内容为:

(lldb) x/64g $x8
0x18058fb40: 0x0000000000000000 0x0000000000000003
0x18058fb50: 0x0000000000000007 0x000000000000000d
0x18058fb60: 0x0000000000000017 0x0000000000000029
0x18058fb70: 0x0000000000000047 0x000000000000007f
0x18058fb80: 0x00000000000000bf 0x00000000000000fb
...

从输出可以看到,除了第0项和第1项之外,其他各项的值与__NSDictionaryCapacity中的值都不相等。

通过上面遍历__NSDictionaryCapacity数组查找到的索引,就可以获取到要创建字典的大小:


NSUInteger size = __NSDictionarySizes[index];

按照道理,直接遍历__NSDictionarySizes也能达到效果。

至于为什么要分成2个数组__NSDictionaryCapacity__NSDictionarySizes,暂时还不清楚原因。

有了要创建字典的大小,接下来就会创建对应的__NSDictionaryI对象:


___NSDictionaryI *dictI = __CFAllocateObject(__NSDictionaryI.class, size * 8 * 2);

上面代码中使用size * 8 * 2的原因是:

size代表key-value对的个数;

每一个key或者value占用8字节;

因此,一个key-value对占用16字节。

创建出的__NSDictionaryI对象,此时还没有存储任何的key-value对。

其内存布局此时为:

image

从内存布局可以看到,key-value对将直接存储在对象当中。

在存储key-value之前,还有一些其他属性需要存储在__NSDictionaryI对象中。

image

如上图所示:

8字节的高6 bit存储这个字典对象size的索引,6 bit最多可以存储64项。

8字节的第7 bit存储__NSDictionaryI._copyKey标志,但是现在暂时不知道它的作用。

8字节剩余的57 bit存储实际的key-value对个数,初始值为count值。

这里有一个问题。

前面-[NSDictionary initWithDictionary:copyItems:]方法内部会对count值进行判断:

if (count >= 2^60) {
    // 创建的字典 key-value 对不能超过 2^60
    error "attempt to create a temporary id buffer which is too large or with a negative count (%lu) -- possibly data is corrupt"
  }

可以看到,count的值最多可以占用60 bit

但是这里只使用57 bit来存储count的值,不知道是不是AppleBUG

设置好这些属性,接下来就要遍历keysobjects数组,通过一个栈block__NSDictionaryI对象填充key-value对:

for (NSInteger i = 0; i < count; i++) {
  ObjectType key = keys[i];
  ObjectTpye object = objects[i];
  ____NSDictionaryI_new_block_invoke(key, value);
}

____NSDictionaryI_new_block_invoke内部,首先对key调用hash函数获取器哈希值:

NSUInteger hashValue = [key hash];

计算出哈希值后,对字典的size进行取余,得到的结果作为__NSDictionaryI对象中,key-value对数组的索引:

NSUInteger index = hashValue % size;

__NSDictionaryI对象中的key-value对数组记作__NSDictionaryI._list

有了index索引值,就可以从__NSDictionaryI._list数组中取出对应的值:

ObjectType oldKey = __NSDictionaryI._list[index];

如果oldKeynil,说明这个位置之前没有值,那么当前的key-value对可以安全的存储到这个位置:

image

需要注意的是,写入的时对 key 进行了 copy

[key copyWithZone:nil];

因此,字典中的key必须实现copy协议。

如果oldKey不为nil,说明这个位置已经被占用了,发生了hash冲突。

这时,需要分情形处理。

如果oldKeykey是同一个对象,或者他们的isEqual方法相等:


if (oldKey == key || [oldKey isEqual:key]) {
  ...
}

那么,当前的key-value对不会被写入,会被丢弃,同时__NSDictionaryI._used会减1

image

如果oldKeykey不是同一个对象,同时,isEqual方法也不相等,那么就会从当前索引开始,遍历整个__NSDictionaryI._list数组。

如果遍历的过程中,找到了空位,那么就写入key-value对。

如果遍历的过程中,出现了上面oldKeykey相等的情形,那么就丢弃当前的key-value对,同时__NSDictionaryI._used1

由于字典的size总是大于或者等于count,因此不会出现遍历整个__NSDictionaryI._list数组,也找不到空位的情形。

image

3.4.2 内存布局

__NSDictionaryI对象完整的内存布局如下:

image

3.4.3 objectForKey:

-[__NSDictionaryI objectForKey:]方法首先调用参数keyhash方法:

NSUInteger hashValue = [key hash];

和初始化过程一样,获取哈希值目的是为了得到__NSDictionaryI._list数组中的索引:

NSUInteger index = hashValue % size;

那此时size是如何得到的呢?

上面__NSDictionaryI对象的内存布局可以知道,size的索引存储在第8字节上。

获取到这个值,就可以从__NSDictionarySizes数组中,取得size值。

获取到index之后,就可以从__NSDictionaryI._list数组中的值:

ObjectType candidateKey = __NSDictionaryI._list[index];

如果candidateKeynil,说明这个位置根本没有值,那么直接返回nil

如果candidateKey不为nil,那么就看candidateKey与参数key是否是同一个对象,或者两者的isEqual方法相等:

if (candidateKey == key || [candidateKey isEqual:key]) {
  ...
}

这种情况下,就是找到了目标key-value对,直接将对应的value值返回。

如果candidateKey与参数key既不是同一个对象,它们的isEqual方法也不相等,那么就从当前的index处开始遍历整个__NSDictionaryI._list数组。

这个过程和初始化过程有点类似:

image

遍历过程中,如果有candidateKey与参数key是同一个对象,或者isEqual方法相等,那么就找到了目标key-value对,直接返回value值。

如果遍历了整个数组,还是没有发现目标key-value对,就返回nil

可以看到,如果哈希冲突比较严重,objectForKey:并不能O(1)时间返回目标值,可能需要O(size)的时间。