本文主要讲述通过汇编分析展示Swift inout的实现原理
首先我们先了解几个汇编指令(AT&T汇编:iOS模拟器汇编,ARM汇编:iOS真机汇编)
movq %rax %rdx
:将%rax
的值赋值给%rdx
leaq -0x18(%rbp),%rax
:将%rbp-0x18
的地址值赋值给rax
callq 0x100003f60
:调用地址值为0x100003f60
的函数寄存器的具体用途
- rax、rdx常作为函数返回值使用
- rdi,rsi,rdx,rcx、r8、r9等寄存器常用于存放函数参数
- rsp、rbp用于栈操作
- rip作为指令指针
函数中的inout
首先我们看看普通的函数
1 | var number = 10 |
在函数调用那里打个断点,可以看到汇编指令是这样子的
1 | 0x100003f4e <+78>: movq -0x30(%rbp), %rdi |
分号后面是注释,用于帮助我们理解汇编指令。
这两行的含义是把寄存器%rbp-0x30
地址上的值赋值给寄存器%rdi
,将其作为参数然后调用函数地址为0x100003f60
的函数。
所以很明显,这是一个值传递
行为
然后我们再来看看使用inout的函数
1 | var number = 10 |
当然按照预期number的值会被改变成20,这里我们再次在函数调用那里打断点看看效果
1 | 0x100003f37 <+55>: leaq 0x40da(%rip), %rdi ; TestSwift.number : Swift.Int |
从这里我们可以看出,系统通过leaq
指令,将%rip+0x40da
的地址,赋值给了寄存器%rdi
,然后将其作为参数调用了地址值为0x100003f60
的函数
所以很明显,这是一个地址传递
行为
总结:在函数调用中,inout
修饰的参数是通过地址传递实现修改值的
属性使用inout
针对存储属性和计算属性进行inout
修饰传参,其实现原理会有所不同,我们看下面的一个例子
首先先写一个简单的Demo
1 | struct Shape { |
存储属性
先试试存储属性
1 | var s = Shape(width: 10, side: 4) |
结果输出
1 | width=20,side=4,girth=80 |
显然跟我们预想的一样,我们通过test
函数把s.width
改成了20,然后这时候四边形的边长就变成了20,周长变成了80
那么这次是不是通过地址传递呢?
通过断点,我们可以看到如下结果
1 | 0x10000308b <+91>: leaq 0x4fe6(%rip), %rdi ; TestSwift.s : TestSwift.Shape |
这里很明显看到是把结构体s
的地址值作为参数传进去了。之所以直接传结构体地址进去,是因为width
是一个存储属性,属性存在在结构体的内存结构中,而且又是第一个属性,所以第一个属性的地址值就是结构体的地址值。假如不是第一个属性,那么就加上偏移值,把该属性的地址传进去。
计算属性
接下来我们传一个计算属性进去试试看
1 | var s = Shape(width: 10, side: 4) |
输出结果为
1 | width=5,side=4,girth=20 |
同样也是符合预期的。然后我们分析一下汇编实现
1 | // 第1-2步 |
可以看到:
- 首先系统调用了
getter
方法,拿到计算属性girth
的值 - 然后通过
movq
指令把拿出来的值放在了地址%rbp-0x28
(一个临时变量)中 - 接下来通过传递地址值的形式,把这个临时变量的地址传了进去,把他指向的值改成了20
- 然后改完之后拿出结果值调用计算属性
girth
的setter方法(%rdi
是参数) - 最终就实现了修改
width
属性的结果
带属性观察器的存储属性
1 | var s = Shape(width: 10, side: 4) |
输出为
1 | width=10,side=20,girth=200 |
同样达到预期。然后我们分析一下汇编实现
为了方便理解,这里拆分为两个部分
1 | // 第1步 |
- 首先,取出结构体地址值+8的地址值(也就是side属性的地址值),赋值给临时变量地址
%rbp-0x28
- 取出临时变量的地址值作为函数参数,调用
test
函数,所以inout
本质依旧是地址传递
- 进入属性的
setter
方法
然后我们step into
看看setter
的主要实现
1 | /// 第1步 |
- 触发了属性观察器的
willset
方法 - 给真正的
side
的地址指向的值改为20 - 触发了属性观察器的
didset
方法
所以跟计算属性类似,也是先拿一个临时变量中转调用了test
方法,等到触发了属性观察器,在两个方法之间才真正拿到中转的临时变量再进行赋值操作
其他注意点
- 可变参数不能标记为
inout
inout
参数不能有默认值inout
参数只能传入可以被多次赋值的(var变量,可变数组的元素等)