NSDictionary
是iOS
开发中经常用到的数据结构。
熟悉NSDictionary
的内部实现,有助于我们更好的使用它们。
同时,在遇到相关崩溃,也能帮助我们更好的分析问题。
1 类簇
非可变字典由NSDictionary
表示。
可变字典由NSMutableDictionary
表示。
按照苹果官方文档的说法,NSDictionary
和NSMutableDictionary
都是类簇。
也就是说,NSDictionary
和NSMutableDictionary
只是暴露的公共接口,具体实现由内部众多私有子类完成。
2 类图
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
的内存布局如下图所示:
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
的值,options
为1
,才会调用下面的函数_NSIsNSString
方法。
代码注释3
,调用_NSIsNSString
方法对参数进行校验。
count
存储字典中key-value
的个数
keys
是一个指针,指向字典中key
所在的数组。
values
是一个指针,指向字典中value
所在的数组。
3.2.2 objectForKey:
使用objectForKey:
方法读取一个key
对应的value
时:
1
使用二分法从keys
数组中找到对应key
所在的索引;
2
从values
数组中根据索引返回对应的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
获取参数key
在keys
数组中所在地址,存储到x0
。
代码注释2
,x19
指向keys
数组首地址,这里计算出参数key
在keys
数组中的偏移量,也就是对应索引。
代码注释3
,x22
指向values
数组首地址,这里计算出value
在values
数组中的地址。
代码注释4
,从values
数组中加载出value
值,存储到x0
。
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
的内存局部如下:
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
,刚好52
个1
。
代码注释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
属性中:
这个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:]
方法。
__NSPlaceholderDictionary
的super
中,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]; }
从上面伪代码可以看到,最终会调用NADictionary
的initWithObjects:forKeys:count:
方法完成初始化。
initWithObjects:forKeys:count:
正是NSDictionary
的designated 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
在Xcode
的lldb
查看其内容为:
(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
在Xcode
的lldb
中查看其内容为:
(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
对。
其内存布局此时为:
从内存布局可以看到,key-value
对将直接存储在对象当中。
在存储key-value
之前,还有一些其他属性需要存储在__NSDictionaryI
对象中。
如上图所示:
第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
的值,不知道是不是Apple
的BUG
。
设置好这些属性,接下来就要遍历keys
和objects
数组,通过一个栈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];
如果oldKey
为nil
,说明这个位置之前没有值,那么当前的key-value
对可以安全的存储到这个位置:
需要注意的是,写入的时对 key 进行了 copy。
[key copyWithZone:nil];
因此,字典中的key
必须实现copy
协议。
如果oldKey
不为nil
,说明这个位置已经被占用了,发生了hash
冲突。
这时,需要分情形处理。
如果oldKey
与key
是同一个对象,或者他们的isEqual
方法相等:
if (oldKey == key || [oldKey isEqual:key]) { ... }
那么,当前的key-value
对不会被写入,会被丢弃,同时__NSDictionaryI._used
会减1
。
如果oldKey
与key
不是同一个对象,同时,isEqual
方法也不相等,那么就会从当前索引开始,遍历整个__NSDictionaryI._list
数组。
如果遍历的过程中,找到了空位,那么就写入key-value
对。
如果遍历的过程中,出现了上面oldKey
与key
相等的情形,那么就丢弃当前的key-value
对,同时__NSDictionaryI._used
减1
。
由于字典的size
总是大于或者等于count
,因此不会出现遍历整个__NSDictionaryI._list
数组,也找不到空位的情形。
3.4.2 内存布局
__NSDictionaryI
对象完整的内存布局如下:
3.4.3 objectForKey:
-[__NSDictionaryI objectForKey:]
方法首先调用参数key
的hash
方法:
NSUInteger hashValue = [key hash];
和初始化过程一样,获取哈希值目的是为了得到__NSDictionaryI._list
数组中的索引:
NSUInteger index = hashValue % size;
那此时size
是如何得到的呢?
上面__NSDictionaryI
对象的内存布局可以知道,size
的索引存储在第8
字节上。
获取到这个值,就可以从__NSDictionarySizes
数组中,取得size
值。
获取到index
之后,就可以从__NSDictionaryI._list
数组中的值:
ObjectType candidateKey = __NSDictionaryI._list[index];
如果candidateKey
为nil
,说明这个位置根本没有值,那么直接返回nil
。
如果candidateKey
不为nil
,那么就看candidateKey
与参数key
是否是同一个对象,或者两者的isEqual
方法相等:
if (candidateKey == key || [candidateKey isEqual:key]) { ... }
这种情况下,就是找到了目标key-value
对,直接将对应的value
值返回。
如果candidateKey
与参数key
既不是同一个对象,它们的isEqual
方法也不相等,那么就从当前的index
处开始遍历整个__NSDictionaryI._list
数组。
这个过程和初始化过程有点类似:
遍历过程中,如果有candidateKey
与参数key
是同一个对象,或者isEqual
方法相等,那么就找到了目标key-value
对,直接返回value
值。
如果遍历了整个数组,还是没有发现目标key-value
对,就返回nil
。
可以看到,如果哈希冲突比较严重,objectForKey:
并不能O(1)
时间返回目标值,可能需要O(size)
的时间。