通過彙編分下值型別的本質。
一、值型別
值型別賦值給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:10
rdx:20
0x100000ba4 <+52>: movq %rax, 0x664d(%rip)
把rax給了地址:0x100000bab + 0x664d = 0x1000071f8
0x100000bab <+59>: movq %rdx, 0x664e(%rip)
把rdx給了地址:0x100000bb2 + 0x664e = 0x100007200
0x100000bb2 <+66>: movq %rcx, %rdi
觀察發現:rdx和rax剛好相差了0x100007200 - 0x1000071f8 = 8個位元組。
--------------------------------------------------------
0x100000bce <+94>: movq 0x6623(%rip), %rax
把地址 0x100000bd5 + 0x6623 = 0x1000071f8 給了rax
0x100000bd5 <+101>: movq %rax, 0x662c(%rip)
把rax給了地址:0x100000bdc + 0x662c = 0x100007208
0x100000bdc <+108>: movq 0x661d(%rip), %rax
把地址 0x100000be3 + 0x661d = 0x100007200 給了rax
0x100000be3 <+115>: movq %rax, 0x6626(%rip)
把rax給了地址:0x100000bea + 0x6626 = 0x100007210
0x100000bea <+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 = str1
str2.append("1024星球")
print(str1)
print(str2)
/*
輸出:
idbeny
idbeny1024星球
*/
1.4.2. 示例程式碼二(陣列):
var arr1 = ["1", "2", "3"]
var arr2 = arr1
arr2.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 = dict1
dict1["name"] = "idbeny"
dict2["age"] = 30
print(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時,
結構體:
- 結構體整體不能被覆蓋;
- 結構體成員值也不能修改。
引用型別:
- 指標是不能重新指向新記憶體的。
- 指標指向的記憶體資料是可以修改的。