inout 的哲学
我 时 常 想 着 改 变 自 己
在清晨上班的路上,在傍晚下班的公交
就像这个 change 函数
func change(num: Int) {
num = 20
}
var age = 18
print(change(num: age)) // 运行错误
永 远 18
直到有一天,
func change(num: inout Int) {
num = 20
}
var age = 18
print(change(num: &age))
// 20
我突然长大了
路人:
我懂了,我懂了,作者你是想告诉我们,想改变就要付出,没有in就没有out
口好渴
这鸡汤我先干为敬
...
fun pee
我的核心思想是
学 的 越 多,老 的 越 快
不 想 认 输,只 好 变 秃
& 地址传递
接下来,看看 inout 到底干了什么
change(num: &age)
&符号,这里表示取址符,取 全局变量age 的 内存地址
不难猜测出是将 age 的内存地址 传到函数内,修改 age 内存地址指向的值
怎么证明这一点呢?
好的,断点落在 change(num: &age)
1\. 0x100000ed1 <+17>: movq $0x12, 0x1144(%rip) ; _dyld_private + 4
2\. 0x100000edc <+28>: movl %edi, -0x1c(%rbp)
-> 3\. 0x100000edf <+31>: movq %rax, %rdi
4\. 0x100000ee2 <+34>: leaq -0x18(%rbp), %rax
5\. 0x100000ee6 <+38>: movq %rsi, -0x28(%rbp)
6\. 0x100000eea <+42>: movq %rax, %rsi
7\. 0x100000eed <+45>: movl $0x21, %edx
8\. 0x100000ef2 <+50>: callq 0x100000f78 ; symbol stub for: swift_beginAccess
9\. 0x100000ef7 <+55>: leaq 0x1122(%rip), %rdi ; inout.age : Swift.Int
10\. 0x100000efe <+62>: callq 0x100000f20 ; inout.change(num: inout Swift.Int) -> Swift.Int at main.swift:11
第1行:
movq $0x12, 0x1144(%rip)
将 8个字节 的Int 型 18 ,放入 0x1144(%rip) 这块内存地址中,0x1144(%rip)
之前文章说过,这个形式(0xXXXX(rip%))代表全局变量的地址值, 这里应该是 变量age 的地址值
第2行:
rip% : 指向下一条指令的地址
将第二行 的 0x100000edc(rip的地址) + 0x1144 = 0x100002020
0x100002020 就是 存储 18 的内存地址
第9行:
leaq 0x1122(%rip), %rdi
将 0x1122(%rip) 地址值 传给rdi, rdi 表参数,也就是将 地址 0x1122(%rip) 当做参数 ,传递给 第十行,
这个 0x1122(%rip) ,通过 (下一条指令地址值 + 0x1122)可以算出 值 就是 0x100002020
就是 18 的地址值
将18 的地址值,当做参数 传给了change
第10行:
既然将 地址值传入 了函数 change,那就继续深入change 内部
inout`change(num:):
-> 1\. 0x100000f60 <+0>: pushq %rbp
2\. 0x100000f61 <+1>: movq %rsp, %rbp
3\. 0x100000f64 <+4>: movq $0x0, -0x8(%rbp)
4\. 0x100000f6c <+12>: movq %rdi, -0x8(%rbp)
5\. 0x100000f70 <+16>: movq $0x14, (%rdi)
6\. 0x100000f77 <+23>: popq %rbp
7\. 0x100000f78 <+24>: retq
第4行:
movq %rdi, -0x8(%rbp)
既然rdi% 是 age 18 的内存地址,这句话就是说把 18 放入了 -0x8(%rbp)
-0x8(%rbp) 是函数change 的 栈空间,后续释放
第5行:
movq $0x14, (%rdi)
因为此时 rdi 指向的还是 age 的内存地址,未曾发生改变 ,第5行将立即数 20 存入 rdi
作为返回值 出栈赋值 给 age
so
age 变成了 20
小结:
从上面简单的例子,应该可以暂时总结
inout 的本质 确实是 引用传递,也就是 引用地址传递
Class的 存储属性 传递
定义一个class,以及 存储属性 age,看一下 存储属性是在inout 中是如何 传递的?
func change(num: inout Int) {
num = 20
}
class Person {
var age: Int
}
var p = Person()
-> change(num: &p.age)
p 的字节占用是 8个字节,指的是 栈空间的 8个字节作为地址,指向堆空间的 内存分布
分析关键点的汇编代码
初始化
1 0x100001a04 <+36>: callq 0x100001d50 ; inout.Person.__allocating_init() -> inout.Person at main.swift:16
2 0x100001a09 <+41>: leaq 0x1798(%rip), %rcx ; inout.p : inout.Person
3 0x100001a10 <+48>: xorl %r8d, %r8d
4 0x100001a13 <+51>: movl %r8d, %edx
->5 0x100001a16 <+54>: movq %rax, 0x178b(%rip) ; inout.p : inout.Person
第1行:
__allocating_init
我们都知道 类class 的内存是存放于堆空间的,__allocating_init 就是向堆空间 申请内存
这里我们了解一下class 的内存分布
第5行:内存申请完毕,作为存放返回值得 rax%,返回的就是Person申请的 在 堆空间的内存地址
通过断点 第5行, register read rax 得到 一个地址值
rax = 0x00000001006318c0
打开Debug -> DebugWorkflow -> ViewMemory ,输入此地址
如下图
得出 -> 第 16个字节确实存放的是 0x12,也就是p.age 的值 18
传参
....
->1\. 0x100001a77 <+151>: movq %rdx, %rdi
2\. 0x100001a7a <+154>: movq %rax, -0x80(%rbp)
3\. 0x100001a7e <+158>: callq 0x100001af0 ; inout.change(num: inout Swift.Int) -> () at main.swift:12
由案例1 分析可得,rdi% 作为参数,这里打印出的地址值 是0x1006318D0
发现了吗?
0x1006318D0 比 0x00000001006318c0 多 16个字节
意味着什么?
函数入参的地址是 Person 地址 偏移 16个字节,就是 age 的内存地址
小结
类对象 Class 的存储属性,inout 函数也是通过 改变 age 的内存地址里的值,来改变 age
也同样是 引用传递
具体流程如下
Class的 计算属性 传递
添加一个计算属性 count
func change(num: inout Int) {
num = 20
}
class Person {
var age = 18
var count: Int {
set {
age = newValue * 2
}
get {
return age / 2
}
}
}
var p = Person()
change(num: &p.count)
print(p.count)
首先我们试着打印 p 的内存占用大小
MemoryLayout.size(ofValue: p)
得出的结果依旧是8个字节,这意味着
计算属性是不占用 类的内存大小的,它相当于一个方法的调用,存放于当前函数 的栈空间
试着猜想一下?
如果计算属性不占用p 的内存空间,它就意味着无法从 p 得到 count 的内存地址
调用 inout 函数 必然是 无法改变 count 属性的,因为没有 地址的输入
这才符合 上述的验证
那么结果是
print(p.count)
// 20
count 被改变了
看汇编
1\. 0x1000015d4 <+36>: callq 0x100001bb0 ; inout.Person.__allocating_init() -> inout.Person at main.swift:16
...
..
-> 2\. 0x100001648 <+152>: callq *%rdx
3\. 0x10000164a <+154>: movq %rdx, %rdi
4\. 0x10000164d <+157>: movq %rax, -0x80(%rbp)
5\. 0x100001651 <+161>: callq 0x1000016c0 ; inout.change(num: inout Swift.Int) -> () at main.swift:12
6\. 0x100001658 <+168>: movq -0x78(%rbp), %rdi
7\. 0x100001660 <+176>: callq *%rax
同样的在初始化 Person 之后,我们看到了 第3行 rdi% 的值 是 从rdx% 得来的
第2行
callq *%rdx
这是一个间接调用指令,rdx% 存放的是一个用于跳转的间接地址
这为什么是 间接地址呢?
因为 类的继承关系,属性很有可能被重写,系统不确定 此 计算属性的 的 setter getter 是否被重写
只能在运行时 去查找对应的方法地址
所以 这里是 间接寻址
好,继续敲入 si,进入内部
inout`Person.count.modify:
2.1 0x100001b06 <+22>: movq %rax, -0x10(%rbp)
2.2 0x100001b0a <+26>: callq 0x100001a10 ; inout.Person.count.getter : Swift.Int at main.swift:23
2.3 0x100001b0f <+31>: movq -0x8(%rbp), %rcx
2.4 0x100001b13 <+35>: movq %rax, 0x8(%rcx)
2.5 0x100001b17 <+39>: leaq 0x12(%rip), %rax ; inout.Person.count.modify : Swift.Int at <compiler-generated>
2.6 0x100001b1e <+46>: movq -0x10(%rbp), %rdx
2.7 0x100001b22 <+50>: addq $0x10, %rsp
2.8 0x100001b26 <+54>: popq %rbp
2.9 0x100001b27 <+55>: retq
第 2-> 1行:
movq %rax, -0x10(%rbp)
将 寄存器rax% 存放的地址 指向 -0x10(%rbp) 栈空间
第 2-> 2行:
映入眼帘的就是 count 的getter 方法,也就是说在 change 函数 之前,会先拿到 count 的值 ,age = 18,那么count 就是9
(lldb) register read rax
rax = 0x0000000000000009
第 2-> 6行:
movq -0x10(%rbp), %rdx
此时的 -0x10(%rbp) 指向的 是rax% 的地址值,赋值给 rdx%
rdx% 存放的就是 9的地址 ,结束调用
以上
callq *rdx 结束
第5行:
change 函数调用,同之前分析
此时 rdi% 通过change 返回 的rax% 已经修改为 20,作后续 的参数使用
第7行:
callq *%rax,传入 rdi%
敲下 si 进入 callq *%rax,可以看到一个熟悉的面孔
inout`Person.count.modify:
->
0x100001b3e <+14>: callq 0x100001980 ; inout.Person.count.setter : Swift.Int at main.swift:20
count 的 setter 函数,到此我想你已经明白了。
小结
Class 的 计算属性 不同于 存储属性,并非直接将 地址传入
通过 计算属性的 getter 取值,然后将 值 存放于一个 地址中
将地址 传入inout ,修改 地址存放的值
结果传入计算属性的 setter
Class 的 带有属性观察器的属性也类似计算属性
如下图:
Copy in Copy out
inout 的本质 就是引用地址的 传递
函数具有单一职责的特性
inout 函数就像 是一个黑盒,我们要做的仅仅是传入需要修改的变量的地址
Copy in Copy out 仅仅是这种行为方式
参数传入,拷贝一份 临时变量的地址
函数内修改 临时变量 的值
函数返回, 临时变量 被赋予给 原始参数
总结
本文只针对了 Class 的计算 和 存储 属性做了 简单的验证, 对于 Struct 也大同小异
不同的地方可能仅仅是 Class 与 Struct 的内存分布不同
读者可以自行分析
谢谢你的阅读
让我们在强者的道路上越走越秃吧!!
领取专属 10元无门槛券
私享最新 技术干货