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. Visio绘制时间轴安排图的方法

      本文介绍基于Visio软件绘制时间轴.日程安排图.时间进度图等的方法.   在很多学习.工作场合中,我们往往需要绘制如下所示的一些带有具体时间进度的日程安排.工作流程.项目进展等可视化图表.    ...

  2. goland无法识别包

    新建 Go 项目时,一定要通过 "File -> New -> Project..." 方式建立,千万不要通过 "File -> Open", ...

  3. DeepSeek 加持!IvorySQL 文档智能助手正式上线!

    DeepSeek 加持!IvorySQL 文档智能助手正式上线! "那个配置参数到底在第几章?"--正在部署 IvorySQL 的运维工程师小 "I",第 5 ...

  4. mysql-installer-community-8.0.19.0.msi 的自定义安装与卸载

    一.双击运行安装包执行安装 1.选择Custom,该种方式可以设置安装位置,仅安装所需的组件,点击Next 2.选择需要的组件,点击Advanced Options 3.设置安装位置,点击OK 4.点 ...

  5. 什么是swagger,一篇带你入门

    一.前言 在前后端分离开发的过程中,前端和后端需要进行api对接进行交互,就需要一个api规范文档,方便前后端的交互,但api文档不能根据代码的变化发生实时动态的改变,这样后端修改了接口,前端不能及时 ...

  6. 实现领域驱动设计 - 使用ABP框架 - 应用程序服务

    应用程序服务 应用程序服务是一种无状态的服务,它实现应用程序的用例.应用程序服务通常获取和返回dto.它由表示层使用.它使用并协调领域对象(实体.存储库等)来实现用例 应用程序服务的常见原则如下: 实 ...

  7. 小白的第一篇blog

    Markdown学习 1.标题 要写标题可用#加空格,再下字,然后再用回车键. 2.字体 1.粗体打法:在文字两侧加两个* 如:hello world! 2.斜体打法:在文字两侧加一个* 如: hel ...

  8. 最爱lx-music的音源哪里去了?

    最爱lx-music,让你满心喜欢,可是音源没有了,因为被投诉给全部关了. 公心作者增加了自定义源. 六音提供了音源,做了一件大善事.注意的是音源会一直初始化.那就下载适合的版本: 欣赏阿鲁阿卓如痴如 ...

  9. DevOps的工作岗位的要求

    ## 为什么需要DevOps 不是每个人都能理解可靠的版本管理和牢固的构建系统的重要性. 也不是任何人能使得软件的发布达到可靠性,可重复性和可审计的高标准.Devops的职责就是将软件的构建和发布的流 ...

  10. emmy断点调试

    package.cpath = package.cpath .. ';C:/Users/Administrator/AppData/Roaming/JetBrains/IntelliJIdea2021 ...