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

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

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

1 类簇

非可变字典由NSDictionary表示。

可变字典由NSMutableDictionary表示。

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

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

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的值,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

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,刚好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属性中:

这个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对。

其内存布局此时为:

从内存布局可以看到,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的值,不知道是不是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对可以安全的存储到这个位置:

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

[key copyWithZone:nil];

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

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

这时,需要分情形处理。

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


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

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

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

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

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

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

3.4.2 内存布局

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

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数组。

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

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

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

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

NSDictionary 内存布局的更多相关文章

  1. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  2. C++ 系列:内存布局

    转载自http://www.cnblogs.com/skynet/archive/2011/03/07/1975479.html 为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内 ...

  3. C++类内存布局图(成员函数和成员变量分开讨论)

    一.成员函数 成员函数可以被看作是类作用域的全局函数,不在对象分配的空间里,只有虚函数才会在类对象里有一个指针,存放虚函数的地址等相关信息. 成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对 ...

  4. 根据内存布局定位的一个fastdfs坑

    在使用fastdfs时,编写数据上传代码时,遇到一个坑.最终根据指针对应的内存布局定位到一个其client API的一个坑,值得记录一下.具体是在 tracker_connect_server() 这 ...

  5. c++ 对象的内存布局

    之前介绍过了普通对象比如系统自带的int等对象的对齐方式,在学习类型转换的时候遇到了自定义类型的继承体系中的downcast与upcast. 于是顺藤摸瓜,摸到了这里.发现还是 陈皓的博客里面写的最早 ...

  6. C++使用继承时子对象的内存布局

    C++使用继承时子对象的内存布局 // */ // ]]>   C++使用继承时子对象的内存布局 Table of Contents 1 示例程序 2 对象的内存布局 1 示例程序 class ...

  7. .NET对象的内存布局

    每个虚拟机都有它自己的对象布局,本文我们将针对sscli源码和windbg调试器来查看不同类型的.net对象布局. 在.net虚拟机里,每个对象都需要保存这些信息: 对象的类型: 对象实例的成员属性( ...

  8. c/c++ 对象内存布局

    一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...

  9. C++ Data Member内存布局

    如果一个类只定义了类名,没定义任何方法和字段,如class A{};那么class A的每个实例占用1个字节的内存,编译器会会在这个其实例中安插一个char,以保证每个A实例在内存中有唯一的地址,如A ...

  10. C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Objec ...

随机推荐

  1. VMware15.5虚拟机下载及安装

    一.VMware虚拟机介绍 VMWare虚拟机软件是一个"虚拟PC"软件,它使你可以在一台机器上同时运行二个或更多Windows.DOS.LINUX系统.与"多启动&qu ...

  2. wxformbuilder 如何生成python 代码

    ?问题 正常通过F8->F6 ,我执行这两步操作后如下图,以.fbp格式显示,没生成文件 解决方案 object properties 下勾选python 效果图:

  3. mysql - 存储过程及分支语句

    存储过程是预编译好的sql语言的集合 减少编译次数,提高sql语句的重用性.但是在现阶段因为其维护困难及其他一系列的原因,有些时候并不推荐使用 创建 create procedure 存储过程的名字 ...

  4. Ubuntu Nvidia driver驱动安装(新)

    前言 英伟达更新了安装驱动的方式,更新一下文档 旧文:Ubuntu Nvidia driver驱动安装及卸载 下载官方驱动安装 1.安装驱动前一定要更新软件列表和安装必要软件.依赖(必须) sudo ...

  5. goframe API 自定义接口返回值处理

    前言 goframe 默认使用了中间键 ghttp.MiddlewareHandlerResponse, HTTP Server 的数据返回通过 ghttp.Response 对象实现,ghttp.R ...

  6. 如何在 PostgreSQL 中运行 TLS 回归测试

    概述 本文将分享一个简单的步骤,介绍如何在 PostgreSQL 中运行 SSL/TLS 回归测试. Postgres 回归测试 每当我们想要添加新功能或进行修复时,都应该运行 PostgreSQL ...

  7. 前后端不分离中使用 Ajax 请求(Django3.2)

    博客地址:https://www.cnblogs.com/zylyehuo/ view.py def get_tokens(request): token = get_token(request) r ...

  8. NodeJS运行时抛出: Error: listen EADDRINUSE :::3000

    错误详情Error: listen EADDRINUSE :::3000    at Server.setupListenHandle [as _listen2] (net.js:1360:14)   ...

  9. 如何在 Git 书写良好的 Commit Messages

    如何在 Git 书写良好的 Commit Messages Why(为什么编写) | How(如何编写) Why A diff will tell you what changed, but only ...

  10. 【DXP】如何在原理图中批量修改

    零.问题 想要修改所有电阻的封装,怎么解决? 一.解决 以修改所有电阻封装为例,可举一反三. 在电阻上右键,选择"查找相似对象". 注意在右键的时候鼠标应该是放在元器件图标上的,而 ...