通过汇编分下值类型的本质。
一、值类型
值类型赋值给var
,let
或者给参数传参,是直接将所有内容拷贝一份。类似于对文件进行复制粘贴操作,产生了全新的文件副本,属于深拷贝(deep copy)。
示例:
func testStruct() { struct Point { var x: Int var y: Int } var p1 = Point(x: 10, y: 20) print("before:p1.x:\(p1.x),p1.y:\(p1.y)") var p2 = p1 print("before:p2.x:\(p2.x),p2.y:\(p2.y)") p2.x = 30 p2.y = 40 print("after:p1.x:\(p1.x),p1.y:\(p1.y)") print("after:p2.x:\(p2.x),p2.y:\(p2.y)")}/* 输出: before:p1.x:10,p1.y:20 before:p2.x:10,p2.y:20 after:p1.x:10,p1.y:20 after:p2.x:30,p2.y:40 */
通过上面的示例可以看出,给p2
重新赋值确实没有影响到p1
的值。
1.1. 内存分析
我们也可以通过内存看下上面示例中变量地址是否发生改变,如果生成了新的地址值,则说明是深拷贝。
func testStruct() { struct Point { var x: Int var y: Int } var p1 = Point(x: 10, y: 20) var p2 = p1 print(Mems.ptr(ofVal: &p1)) print(Mems.ptr(ofVal: &p2))}/* 输出: 0x00007ffeefbff4c0 0x00007ffeefbff490 */
打印结果显示:p2
和p1
的内存地址是不同的,所以修改p2
不会影响p1
。
1.2. 汇编分析(局部变量)
第一步:示例代码:
第二步:进入汇编代码后先查找立即数:
第三步:进入p1的初始化方法中:
第四步:继第三步finish
后,继续回到之前的汇编:
movq %rax, -0x10(%rbp)movq %rdx, -0x8(%rbp)movq %rax, -0x20(%rbp)movq %rdx, -0x18(%rbp)movq $0x1e, -0x20(%rbp)movq $0x28, -0x18(%rbp)
通过上面分析得出:
p1的变量x内存地址:
rbp-0x10
;p1的变量y内存地址:
rbp-0x8
;且p1的两个变量相差
rbp-0x8-(rbp-0x10) = 8
个字节;p1的内存地址是
rbp-0x10
。0x1e
赋值给rbp-0x20
的地址,和上面的rax
赋值给rbp-0x20
是同一个地址,并且仅仅修改了一次。
所以,通过汇编也可以有力的证明值类型传递是深拷贝。
扩展:
%edi
和%esi
是局部变量,将来传给形参后会变成%rdi
和%rsi
。
1.3. 汇编分析(全局变量)
第一步:示例代码:
第二步:查看汇编:
进入init
方法发现和上面的1.2分析基本一致,rdi
给了rax
,rsi
给了rdx
:
第三步:继续往后面看call
之后的代码:
rip就是下一条指令的地址。rax:10rdx:200x100000ba4 <+52>: movq %rax, 0x664d(%rip)把rax给了地址:0x100000bab + 0x664d = 0x1000071f80x100000bab <+59>: movq %rdx, 0x664e(%rip) 把rdx给了地址:0x100000bb2 + 0x664e = 0x1000072000x100000bb2 <+66>: movq %rcx, %rdi观察发现:rdx和rax刚好相差了0x100007200 - 0x1000071f8 = 8个字节。--------------------------------------------------------0x100000bce <+94>: movq 0x6623(%rip), %rax把地址 0x100000bd5 + 0x6623 = 0x1000071f8 给了rax0x100000bd5 <+101>: movq %rax, 0x662c(%rip)把rax给了地址:0x100000bdc + 0x662c = 0x1000072080x100000bdc <+108>: movq 0x661d(%rip), %rax 把地址 0x100000be3 + 0x661d = 0x100007200 给了rax0x100000be3 <+115>: movq %rax, 0x6626(%rip)把rax给了地址:0x100000bea + 0x6626 = 0x1000072100x100000bea <+122>: leaq -0x18(%rbp), %rdi--------------------------------------------------------观察发现:0x1000071f8就是上面的10,0x100007200就是上面的20就是说,把0x1000071f8里面的值(10)取出来赋值给了另外一块内存地址0x100007208;把0x100007200里面的值(20)取出来赋值给了另外一块内存地址0x100007210并且,0x100007210和0x100007208相差8个字节。
通过上面的分析可以得出,p1的内存地址就是0x1000071f8,p2的内存地址是0x100007208。也可以证明值类型是深拷贝。
经验:
- 内存地址格式为:
0x486f(%rip)
,一般是全局变量,全局区(数据段); - 内存地址格式为:
-0x8(%rbp)
,一般是局部变量,栈空间。 - 内存地址格式为:
0x10(%rax)
,一般是堆空间。
规律:
- 全局变量意味着内存地址是固定的;
- 局部变量的地址依赖
rbp
,而rbp右依赖于rsp
,rsp
是外部传进来的(即函数调用)。
1.4. 赋值操作
在Swift标准库中,为了提升性能,String
、Array
、Dictionary
、Set
采取了Copy On Write的技术。
Copy On Write: 当需要进行内存操作(写)时,才会进行深度拷贝。
对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。
建议:不需要修改的,尽量定义为
let
。
1.4.1. 示例代码一(字符串):
var str1 = "idbeny"var str2 = str1str2.append("1024星球")print(str1)print(str2)/* 输出: idbeny idbeny1024星球 */
1.4.2. 示例代码二(数组):
var arr1 = ["1", "2", "3"]var arr2 = arr1arr2.append("4")arr1[0] = "one"print(arr1)print(arr2)/* 输出: ["one", "2", "3"] ["1", "2", "3", "4"] */
1.4.3. 示例代码三(字典):
var dict1 = ["name": "大奔", "age": 20] as [String : Any]var dict2 = dict1dict1["name"] = "idbeny"dict2["age"] = 30print(dict1)print(dict2)/* 输出: ["name": "idbeny", "age": 20] ["name": "大奔", "age": 30] */
二、引用类型
引用赋值给var
、let
或者给函数传参,是将内存地址拷贝一份。
类似于制作一个文件的替身(快捷方式),指向的是同一个文件。属于浅拷贝(shallow copy)。
2.1. 内存分析
示例代码:
class Size { var width: Int var height: Int init(width: Int, height: Int) { self.width = width self.height = height }}func test() { var s1 = Size(width: 10, height: 20) var s2 = s1 print("s1指针的内存地址:",Mems.ptr(ofVal: &s1)) print("s1指针指向的内存地址:",Mems.ptr(ofRef: s1)) print("s2指针的内存地址:",Mems.ptr(ofVal: &s2)) print("s2指针指向的内存地址:",Mems.ptr(ofRef: s2))}test()/* 输出: s1指针的内存地址: 0x00007ffeefbff478 s1指针指向的内存地址: 0x000000010061fe80 s2指针的内存地址: 0x00007ffeefbff470 s2指针指向的内存地址: 0x000000010061fe80 */
示例代码在内存中的表现:
思考:
s2.width = 11; s2.height = 22
,代码执行后,s1.width
和s1.height
分别是多少?
s2.width == 11, s2.height == 22
,因为修改的是指针指向的内存地址保存的数据,而s1
和s2
指向的是同一块内存。
2.2. 汇编分析
第一步:示例代码:
第二步:查看初始化方法函数的返回值:
通过lldb
指令得到rax
的地址:
(lldb) register read rax输出:rax = 0x0000000100599840
再通过View Memory查看rax保存的数据有哪些:
第三步:找到p1
和p2
:
函数地址rax
给了局部变量-0x10(%rbp)
,所以-0x10(%rbp)
就是p1,同理-0x28(%rbp)
是p2。
第四步:查看s2
的width
和height
是如何被修改的:
- 前面通过
movq %rax, -0x28(%rbp)
把函数返回值rax
给了-0x28(%rbp)
; - 之后又通过
movq -0x28(%rbp), %rdx
把函数返回值给了rdx
; - 经过
(%rdx), %rsi
和0x68(%rsi), %rsi
中转后,把rdx
给了rsi
; $0xb, %edi
其实是把值11给了edi
(即rdx
)。
所以,width和height其实修改的是同一块内存地址。
2.3. 赋值操作
示例代码:
class Size { var width: Int var height: Int init(width: Int, height: Int) { self.width = width self.height = height }}var s1 = Size(width: 10, height: 20)s1 = Size(width: 11, height: 22)
在内存中的表现:
s1刚开始指向堆空间02,后又指向堆空间01。当堆空间02没有强指针指向时就会被销毁。
三、值类型、引用类型的let
使用let时,
结构体:
- 结构体整体不能被覆盖;
- 结构体成员值也不能修改。
引用类型:
- 指针是不能重新指向新内存的。
- 指针指向的内存数据是可以修改的。
原文转载:http://www.shaoqun.com/a/760848.html
壹米滴答:https://www.ikjzd.com/w/2314
心怡:https://www.ikjzd.com/w/1327
通过汇编分下值类型的本质。一、值类型值类型赋值给var,let或者给参数传参,是直接将所有内容拷贝一份。类似于对文件进行复制粘贴操作,产生了全新的文件副本,属于深拷贝(deepcopy)。示例:functestStruct(){structPoint{varx:Intvary:Int}varp1=Point(x:10,y:20)print("before:p1.x:\(p1.x),p1.
启明星:https://www.ikjzd.com/w/1436
stylenanda:https://www.ikjzd.com/w/1675.html
拍怕:https://www.ikjzd.com/w/2205
好消息!Wish 商户回款时间将加快了:https://www.ikjzd.com/articles/114570
Wish专区| 上线300多天后,A+ 物流计划为超过百万件商品提供物流托管服务:https://www.ikjzd.com/articles/124303
中东电商:一文看懂2018年出海中东的机遇和挑战!:https://www.ikjzd.com/articles/13930
No comments:
Post a Comment