[toc]
前言
今天的文章结构会比上篇清晰很多,请放心食用。
cow
cow
是 copy on write 的简写,是一种通过延迟复制时机节省时间和空间的编程技术
本文会通过对 NSMutableDictionary
的底层分析,来分享苹果官方对 cow
的设计思路
以 NSMutableDictionary
为例,cow
机制对时间复杂度和空间复杂度有着非常明显的优化:
不采用 cow 机制 | 采用 cow 机制 | |
---|---|---|
复制字典的空间复杂度 | 为字典的所有键值对申请内存 | copy 或者 mutalbeCopy 均不需要,第一次修改才会触发申请 |
复制字典的时间复杂度 | 需要对所有键值对进行 retain 操作 | copy 或者 mutalbeCopy 均不需要,第一次修改才会触发 retain |
销毁复制字典的成本 | 需要对所有键值对进行 release 操作 | 没有修改过的可变字典不需要对键值对 release 操作 |
可变字典结构
首先,我们先看看可变字典的内存结构:
注意:此时的 cow 是
NULL
对应到 Obj-C
代码后如下:
|
|
可变字典的内存分 4 个部分:
Class isa
:代表类实例的类型id *buffer
:指向可变字典保存的键值对struct state
:代表可变字典的各种状态used
代表已经使用的空间大小,与开发者常用的count
属性对应mutbits
代表对字典变更的次数。初始化时是 1,增删会加 1szidx
通过搭配 常量数组__NSDictionarySizes
,获取字典的容量copyeKeys
: 代表需要复制 key
struct __cow_state_t *cow
:会指向一个结构体__cow_state_t
cow_lock
:多线程对单个可变字典进行复制时,可能存在崩溃或者内存泄露,所以需要锁进行多线程保护copyCount:16
:代表对可变字典copy
的次数mutableCopyCount:16
:代表对可变字典进行mutableCopy
的次数
NSMutableDictionary 的 cow 机制浅析
NSMutableDictionary
通过引入一个指向结构体:__cow_state_t
的指针实现了 cow
机制
当复制发生时,__cow_state_t
的引用计数会加 1,并且多个字典会共享一份 __cow_state_t
和 buffer
。从而实现常量时间的复制成本
以下面的代码为例:
|
|
等效的代码非常简单:
|
|
通过 dic0->cow->mutableCopyCount += 1;
,系统可以避免对申请新的空间持有键值对,并且避免对字典持有的每个键值对进行 retain
操作
[__NSDictionaryM mutableCopy]
[mutableDic mutableCopy]
代码执行时,会被转发到 -[__NSDictionaryM mutableCopy]
处执行可变字典复制逻辑。
-[__NSDictionaryM mutableCopy]
会依次完成以下任务:
- 序章
- 旧字典懒加载创建
cow
- 创建新的字典
- 复制 cow
序章
注意:部分指令只有在开启 线程消毒剂 等场景才会执行
|
|
iOS 模拟器开启线程消毒剂
iOS 模拟器开启线程消毒剂后的堆栈如下:
|
|
对应汇编指令:
|
|
1、旧字典通过懒加载方式创建 cow
第一次复制时,会通过调用函数 _cow_create
进行懒加载 cow
|
|
屏障 (Barriers)
为了保证多线程安全,上一段汇编指令使用了 屏障指令
普通的内存屏障是双向的,可以控制内存屏障之前的某些存储操作在内存屏障之前完成,并且内存屏障之后的某些存储操作要在内存屏障之后才能开始。
但是 ARMv8 架构新增的 单向屏障 (One-way barriers) Load-Acquire(LDAR 指令)和 Store-Release(STLR 指令)却只限定了单个方向的
Load-Acquire:指令之后的所有加载和存储操作一定不会被重排序到这条指令之前,但是没有要求这条指令之前的所有加载和存储操作一定不能被重排序到这条指令之后。所以,这条指令是单向屏障,只挡住了后面出现的所有内存操作指令,但是没有挡住这条指令之前的所有内存操作指令。
Store-Release:指令之前的所有加载和存储才做一定不会被重排序到这条指令之后,但是没有要求这条指令之后的所有加载和存储操作一定不能被重排序到这条指令之前。所以,这条指令也是单向屏障,只挡住了前面出现的所有内存操作指令,但是没有挡住这条指令之后的所有内存操作指令。
旧字典创建 cow (_cow_create)
注意:创建失败会通过函数
_cow_allocation_failure
抛出异常
|
|
_cow_allocation_failure
更新栈后,转到 函数 _cow_failure
进一步处理
|
|
_cow_failure
|
|
2、创建新的字典
|
|
注意:新的字典各项属性都是空值
3、旧字典的 cow 进行复制到新字典
通过函数 _cow_copy
和 函数 cow_copy_instance
搭配将旧字典的 cow 进行复制,并填充给新字典
|
|
_cow_copy
_cow_copy
函数通过 cow_copy_instance
复制 cow,并将 cow 的计数+1
参数列表:
- 旧字典
- 是否属于谢谢
- 指向旧字典的 cow
- 函数
cow_copy_instance
指针 - 新字典
- 1 代表是 mutableCopy 复制;0 代表 copy 复制
序章
|
|
加锁
存在多线程复制的可能性,需要加锁
|
|
加锁相关指令(不同的 cpu 架构,加锁指令会存在一些差异):
|
|
|
|
mutableCopy 逻辑
如果是是通过 [dic mutableCopy]
调用该函数,需要将 mutableCopyCount
加一,然后通过 函数cow_copy_instance
进行复制
|
|
copy 逻辑
如果是是通过 [dic copy]
调用该函数,需要将 copyCount
加一,然后通过 函数cow_copy_instance
进行复制
|
|
cow_copy_instance
函数 cow_copy_instance
接收的参数列表如下:
-
旧字典
-
旧字典是否属于 __NSDictionaryM
-
新字典
-
新字典是否属于 __NSDictionaryM
|
|
复制完成后的内存情况:
恢复栈&unlock
|
|
|
|
引用计数超限处理
根据设计,可变字典内部只有两个 16 位分别保存不可变字典和可变字典的引用计数
|
|
所以,需要在复制次数超过限制时,进行一次特殊处理
|
|
-[__NSDictionaryM setObject:forKey:]
当我们对一个可变字典的可变副本进行赋值时,就会触发 cow 机制
|
|
本段开始,只会对关键位置的汇编指令添加注释
_cow_mutate_slow
_cow_mutate_slow
会通过 cow_copy_storage
进行复制操作
参数列表:
- 字典
- cow
- 保存 4 个特殊函数指针的结构体
__NSDictionary_cowCallbacks
|
|
_cow_mutate_slow
:
|
|
cow_copy_storage
cow_set_cow
|
|
总结
本文通过对可变字典的复制和修改方法的实现进行了逐步的分析,分享了可变字典的 cow
机制