目錄
【傳回目錄】
Swift底層原理探索----屬性 & 方法
- 屬性
-
- 存儲屬性
- 計算屬性
- 枚舉rawValue原理
- 延遲存儲屬性(Lazy Stored Property)
-
- 延遲存儲屬性注意點
- 屬性觀察器(Property Observer)
-
- 全局變量、局部變量
- `inout`的再次研究
- `inout`的本質總結
- 類型屬性(Type Property)
- 類型屬性細節
- 單例模式
- 類型(static)存儲屬性的本質
- 方法
-
- 方法
- mutating
- @discardableResult
- 下标
-
- 下标的細節
- 結構體、類作為傳回值的對比
- 下标接受多個參數
屬性
struct Circle {
//存儲屬性
var radius: Double
//計算屬性
var diamiter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
- Swift中跟執行個體相關的屬性可以分為2大類
- 存儲屬性(
)Stored Property
- 類似于成員變量這個概念
- 存儲在執行個體的記憶體中
Swift底層原理探索----屬性 & 方法屬性方法下标 - 結構體、類可以定義存儲屬性
Swift底層原理探索----屬性 & 方法屬性方法下标 - 枚舉
定義存儲屬性不可以
我們知道枚舉的記憶體裡面可以存放的是所有的Swift底層原理探索----屬性 & 方法屬性方法下标
以及case
,并沒有所謂的成員變量概念,可是以也不存在所謂的存儲屬性關聯值
- 計算屬性(
)Computed Property
- 本質就是方法(函數)這個也可以通過彙編來證明一下
Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标 - 不占用執行個體的記憶體
Swift底層原理探索----屬性 & 方法屬性方法下标 - 枚舉、結構體、類都可以定義計算屬性
- 本質就是方法(函數)這個也可以通過彙編來證明一下
- 存儲屬性(
存儲屬性
- 關于存儲屬性,
有個明确的規定Swift
- 在建立類 或 結構體的時候,必須為所有的存儲屬性設定一個合适的初始值,也就是要求類/結構體建立執行個體後,它的全部記憶體要得到初始化,而存儲屬性正好就是放在執行個體的記憶體裡面的,是以需要将所有的存儲屬性設定初始值。
- 可以在初始化器裡為存儲屬性設定一個初始值
Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标 - 可以配置設定一個預設的屬性值作為屬性定義的一部分
Swift底層原理探索----屬性 & 方法屬性方法下标
- 可以在初始化器裡為存儲屬性設定一個初始值
- 在建立類 或 結構體的時候,必須為所有的存儲屬性設定一個合适的初始值,也就是要求類/結構體建立執行個體後,它的全部記憶體要得到初始化,而存儲屬性正好就是放在執行個體的記憶體裡面的,是以需要将所有的存儲屬性設定初始值。
計算屬性
-
傳入的新值預設叫做set
,也可以自定義newValue
- 定義計算屬性隻能用
, 不能用var
let
-
代表常量,也就是值是一成不變的let
- 計算屬性的值是可能發生變化的(即使是隻讀計算屬性)
-
- 隻讀計算屬性:隻有
, 沒有get
set
枚舉rawValue原理
- 枚舉原始值
的本質是:隻讀計算屬性,直接看彙編就可以證明rawValue
Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标 Swift底層原理探索----屬性 & 方法屬性方法下标
延遲存儲屬性(Lazy Stored Property)
看現這段代碼
class Car {
init() {
print("Car init")
}
func run() {
print("Car is running!")
}
}
class Person {
var car = Car()
init() {
print("Person init")
}
func goOut() {
car.run()
}
}
let p = Person()
print("-----------")
p.goOut()
運作結果如下
Car init
Person init
-----------
Car is running!
Program ended with exit code: 0
我們給上面代碼的car屬性增加一個關鍵字
lazy
修飾
class Car {
init() {
print("Car init")
}
func run() {
print("Car is running!")
}
}
class Person {
lazy var car = Car()
init() {
print("Person init")
}
func goOut() {
car.run()
}
}
let p = Person()
print("-----------")
p.goOut()
再看下現在的運作結果
Person init
-----------
Car init
Car is running!
Program ended with exit code: 0
可以看出,
lazy
的作用,是将屬性
var car
的初始化延遲到了它首次使用的時候進行,例子中也就是
p.goOut()
這句代碼執行的時候,才回去初始化屬性
car
通過lazy 關鍵字修飾的存儲屬性就要做
延遲存儲屬性
,這個功能的好處是顯而易見的,因為有些屬性可能需要花費很多資源進行初始化,而很可能在某些極少情況下才會被觸發使用,是以
lazy
關鍵字就可以用在這種情況下,讓核心對象的初始化變得快速而輕量。比如下面這個例子
class PhotoView {
lazy var image: Image = {
let url = "https://www.520it.com/xx.png"
let data = Data(url: url)
return Image(dada: data)
}()
}
網絡圖檔的加載往往是需要一些時間的,上面例子裡面圖檔的加載過程封裝在閉包表達式裡面,并且将其傳回值作為了
image
屬性的初始化指派,通過
lazy
,就講這個加載的過程推遲到了
image
在實際被用到的時候去執行,這樣就可以提升app順滑度,改善卡頓情況。
- 使用
可以定義一個延遲存儲屬性,在第一次用到屬性的時候才會進行初始化
lazy
屬性必須是
lazy
, 不能是
var
let
- 這個要求很容易了解,
必須在
let
的初始化方法完成之前就擁有值,而
執行個體
恰好是為了在執行個體建立并初始化之後的某個時刻對其某個屬性進行初始化指派,是以
lazy
隻能作用域
lazy
屬性
var
- 如果多線程同時第一次通路
屬性,無法保證屬性隻被初始化
lazy
次
1
延遲存儲屬性注意點
- 當結構體包含一個延遲存儲屬性時,隻有
var
才能通路延遲存儲屬性
因為延遲屬性初始化時需要改變結構體的記憶體
案例中,因為Swift底層原理探索----屬性 & 方法屬性方法下标
是常量,是以記憶體的内容初始化之後不可以變化,但是p.z會使得結構體p
的Point
屬性進行初始化,因為結構體的成員是在結構體的記憶體裡面的,是以就需要改變結構體的記憶體,是以便産生了後面的報錯。lazy var z
屬性觀察器(Property Observer)
- 可以為
的非lazy
存儲屬性設定屬性觀察器var
-
會傳遞新值,預設叫做willSet
newValue
-
會傳遞舊值,預設叫做didSet
oldValue
- 在初始化器中設定屬性值不會出發
和willSet
didSet
- 在屬性定義時設定初始值也不會出發
和willSet
didSet
struct Circle {
var radius: Double {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
init() {
self.radius = 1.0
print("Circle init!")
}
}
var circle = Circle()
circle.radius = 10.5
print(circle.radius)
運作結果
Circle init!
willSet 10.5
didSet 1.0 10.5
10.5
Program ended with exit code: 0
全局變量、局部變量
屬性觀察器、計算屬性的功能,同樣可以應用在全局變量、局部變量身上var num: Int { get { return 10 } set { print("setNum", newValue) } } num = 12 print(num) func test() { var age = 10 { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, age) } } age = 11 } test()
inout
的再次研究
inout
首先看下面的代碼
func test(_ num: inout Int) {
num = 20
}
var age = 10
test(&age) // 此處加斷點
将程式運作至斷點處,觀察彙編
SwiftTest`main:
0x1000010b0 <+0>: pushq %rbp
0x1000010b1 <+1>: movq %rsp, %rbp
0x1000010b4 <+4>: subq $0x30, %rsp
0x1000010b8 <+8>: leaq 0x6131(%rip), %rax ; SwiftTest.age : Swift.Int
0x1000010bf <+15>: xorl %ecx, %ecx
0x1000010c1 <+17>: movq $0xa, 0x6124(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x1000010cc <+28>: movl %edi, -0x1c(%rbp)
-> 0x1000010cf <+31>: movq %rax, %rdi
0x1000010d2 <+34>: leaq -0x18(%rbp), %rax
0x1000010d6 <+38>: movq %rsi, -0x28(%rbp)
0x1000010da <+42>: movq %rax, %rsi
0x1000010dd <+45>: movl $0x21, %edx
0x1000010e2 <+50>: callq 0x10000547c ; symbol stub for: swift_beginAccess
0x1000010e7 <+55>: leaq 0x6102(%rip), %rdi ; SwiftTest.age : Swift.Int
0x1000010ee <+62>: callq 0x100001110 ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
0x1000010f3 <+67>: leaq -0x18(%rbp), %rdi
0x1000010f7 <+71>: callq 0x10000549a ; symbol stub for: swift_endAccess
0x1000010fc <+76>: xorl %eax, %eax
0x1000010fe <+78>: addq $0x30, %rsp
0x100001102 <+82>: popq %rbp
0x100001103 <+83>: retq
我們可以看到函數
test
調用之前,參數的傳遞情況如下
對于上述比較簡單的情況,我們知道
inout
的本質就是進行引用傳遞,接下來,我們考慮一些更加複雜的情況
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSetSide", newValue)
}
didSet {
print("didSetSide", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setGirth", newValue)
}
get {
print("getGirth")
return width * side
}
}
func show() {
print("width= \(width), side= \(side), girth= \(girth)")
}
}
func test(_ num: inout Int) {
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.width) // 斷點1
s.show()
print("-------------")
test(&s.side) //斷點2
s.show()
print("-------------")
test(&s.girth) //斷點3
s.show()
print("-------------")
上述案例裡面,全局變量s的類型是結構體
Struct Shape
,它的記憶體放的是兩個存儲屬性
width
和
side
,其中
side
帶有屬性觀察器,另外Shape還有一個計算屬性
girth
,我們首先不加斷點運作一下程式,觀察一下運作結果
getGirth
width= 20, side= 4, girth= 80
-------------
willSetSide 20
didSetSide 4 20
getGirth
width= 20, side= 20, girth= 400
-------------
getGirth
setGirth 20
getGirth
width= 1, side= 20, girth= 20
-------------
Program ended with exit code: 0
看得出來,
inout
對于三種屬性都産生了作用,那麼它的底層到底是如何處理和實作的呢?我們還是要通過彙編來一探究竟。便于彙編分析,我們截取部分代碼進行編譯運作
首先看 普通的屬性
👇👇👇👇
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSetSide", newValue)
}
didSet {
print("didSetSide", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setGirth", newValue)
}
get {
print("getGirth")
return width * side
}
}
func show() {
print("width= \(width), side= \(side), girth= \(girth)")
}
}
func test(_ num: inout Int) {
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.width) // 斷點處,傳入普通屬性width作為test的inout參數
彙編結果如下
SwiftTest`main:
0x100001310 <+0>: pushq %rbp
0x100001311 <+1>: movq %rsp, %rbp
0x100001314 <+4>: subq $0x30, %rsp
0x100001318 <+8>: movl $0xa, %eax
0x10000131d <+13>: movl %edi, -0x1c(%rbp)
0x100001320 <+16>: movq %rax, %rdi
0x100001323 <+19>: movl $0x4, %eax
0x100001328 <+24>: movq %rsi, -0x28(%rbp)
0x10000132c <+28>: movq %rax, %rsi
0x10000132f <+31>: callq 0x100001d60 ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
0x100001334 <+36>: leaq 0x6ebd(%rip), %rcx ; SwiftTest.s : SwiftTest.Shape
0x10000133b <+43>: xorl %r8d, %r8d
0x10000133e <+46>: movl %r8d, %esi
0x100001341 <+49>: movq %rax, 0x6eb0(%rip) ; SwiftTest.s : SwiftTest.Shape
0x100001348 <+56>: movq %rdx, 0x6eb1(%rip) ; SwiftTest.s : SwiftTest.Shape + 8
-> 0x10000134f <+63>: movq %rcx, %rdi
0x100001352 <+66>: leaq -0x18(%rbp), %rax
0x100001356 <+70>: movq %rsi, -0x30(%rbp)
0x10000135a <+74>: movq %rax, %rsi
0x10000135d <+77>: movl $0x21, %edx
0x100001362 <+82>: movq -0x30(%rbp), %rcx
0x100001366 <+86>: callq 0x100006312 ; symbol stub for: swift_beginAccess
0x10000136b <+91>: leaq 0x6e86(%rip), %rdi ; SwiftTest.s : SwiftTest.Shape
0x100001372 <+98>: callq 0x100001d70 ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
0x100001377 <+103>: leaq -0x18(%rbp), %rdi
0x10000137b <+107>: callq 0x100006330 ; symbol stub for: swift_endAccess
0x100001380 <+112>: xorl %eax, %eax
0x100001382 <+114>: addq $0x30, %rsp
0x100001386 <+118>: popq %rbp
0x100001387 <+119>: retq
參數傳遞流程如下圖
是以對于
普通的存儲屬性
,
test
函數是直接将它的位址值傳入。
接下來便于直覺的對比,我們再看一下 計算屬性
的情況👇👇👇👇
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSetSide", newValue)
}
didSet {
print("didSetSide", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setGirth", newValue)
}
get {
print("getGirth")
return width * side
}
}
func show() {
print("width= \(width), side= \(side), girth= \(girth)")
}
}
func test(_ num: inout Int) {
print("開始test函數")
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.girth)
斷點處彙編如下
SwiftTest`main:
0x1000012f0 <+0>: pushq %rbp
0x1000012f1 <+1>: movq %rsp, %rbp
0x1000012f4 <+4>: pushq %r13
0x1000012f6 <+6>: subq $0x38, %rsp
0x1000012fa <+10>: movl $0xa, %eax
0x1000012ff <+15>: movl %edi, -0x2c(%rbp)
0x100001302 <+18>: movq %rax, %rdi
0x100001305 <+21>: movl $0x4, %eax
0x10000130a <+26>: movq %rsi, -0x38(%rbp)
0x10000130e <+30>: movq %rax, %rsi
0x100001311 <+33>: callq 0x100001d60 ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
0x100001316 <+38>: leaq 0x6edb(%rip), %rcx ; SwiftTest.s : SwiftTest.Shape
0x10000131d <+45>: xorl %r8d, %r8d
0x100001320 <+48>: movl %r8d, %esi
0x100001323 <+51>: movq %rax, 0x6ece(%rip) ; SwiftTest.s : SwiftTest.Shape
0x10000132a <+58>: movq %rdx, 0x6ecf(%rip) ; SwiftTest.s : SwiftTest.Shape + 8
-> 0x100001331 <+65>: movq %rcx, %rdi
0x100001334 <+68>: leaq -0x20(%rbp), %rax
0x100001338 <+72>: movq %rsi, -0x40(%rbp)
0x10000133c <+76>: movq %rax, %rsi
0x10000133f <+79>: movl $0x21, %edx
0x100001344 <+84>: movq -0x40(%rbp), %rcx
0x100001348 <+88>: callq 0x100006312 ; symbol stub for: swift_beginAccess
0x10000134d <+93>: movq 0x6ea4(%rip), %rdi ; SwiftTest.s : SwiftTest.Shape
0x100001354 <+100>: movq 0x6ea5(%rip), %rsi ; SwiftTest.s : SwiftTest.Shape + 8
0x10000135b <+107>: callq 0x1000016d0 ; SwiftTest.Shape.girth.getter : Swift.Int at main.swift:646
0x100001360 <+112>: movq %rax, -0x28(%rbp)
0x100001364 <+116>: leaq -0x28(%rbp), %rdi
0x100001368 <+120>: callq 0x100001d70 ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
0x10000136d <+125>: movq -0x28(%rbp), %rdi
0x100001371 <+129>: leaq 0x6e80(%rip), %r13 ; SwiftTest.s : SwiftTest.Shape
0x100001378 <+136>: callq 0x100001820 ; SwiftTest.Shape.girth.setter : Swift.Int at main.swift:642
0x10000137d <+141>: leaq -0x20(%rbp), %rdi
0x100001381 <+145>: callq 0x100006330 ; symbol stub for: swift_endAccess
0x100001386 <+150>: xorl %eax, %eax
0x100001388 <+152>: addq $0x38, %rsp
0x10000138c <+156>: popq %r13
0x10000138e <+158>: popq %rbp
0x10000138f <+159>: retq
這一次從彙編代碼量就可以判斷,對于計算屬性的處理肯定比存儲屬性要複雜,還是通過圖例來展示一下整個過程
可以看出,由于計算屬性在執行個體内部沒有對應的記憶體空間,編譯器通過在函數棧裡面開辟一個局部變量的方法,利用它作為計算屬性的值的臨時宿主,并且将該局部變量的位址作為
test
函數的
inout
參數傳入函數,是以本質上,仍然是
引用傳遞
。
test
函數調用前,計算屬性值給複制到局部變量上,以及
test
函數調用之後,局部變量的值傳遞給setter函數的這兩個過程,被蘋果成為 Copy In Copy Out,上面案例代碼的運作結果也驗證了這個結論
getGirth
開始test函數
setGirth 20
Program ended with exit code: 0
最後,我們來看對于 帶有屬性觀察器的存儲屬性
,處理過程會有哪些獨到之處👇👇👇👇
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSetSide", newValue)
}
didSet {
print("didSetSide", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setGirth", newValue)
}
get {
print("getGirth")
return width * side
}
}
func show() {
print("width= \(width), side= \(side), girth= \(girth)")
}
}
func test(_ num: inout Int) {
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.side) //side是帶屬性觀察期的存儲屬性, 斷點在這裡
斷點處彙編結果如下
SwiftTest`main:
0x100001230 <+0>: pushq %rbp
0x100001231 <+1>: movq %rsp, %rbp
0x100001234 <+4>: pushq %r13
0x100001236 <+6>: subq $0x38, %rsp
0x10000123a <+10>: movl $0xa, %eax
0x10000123f <+15>: movl %edi, -0x2c(%rbp)
0x100001242 <+18>: movq %rax, %rdi
0x100001245 <+21>: movl $0x4, %eax
0x10000124a <+26>: movq %rsi, -0x38(%rbp)
0x10000124e <+30>: movq %rax, %rsi
0x100001251 <+33>: callq 0x100001ca0 ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
0x100001256 <+38>: leaq 0x6f9b(%rip), %rcx ; SwiftTest.s : SwiftTest.Shape
0x10000125d <+45>: xorl %r8d, %r8d
0x100001260 <+48>: movl %r8d, %esi
0x100001263 <+51>: movq %rax, 0x6f8e(%rip) ; SwiftTest.s : SwiftTest.Shape
0x10000126a <+58>: movq %rdx, 0x6f8f(%rip) ; SwiftTest.s : SwiftTest.Shape + 8
-> 0x100001271 <+65>: movq %rcx, %rdi
0x100001274 <+68>: leaq -0x20(%rbp), %rax
0x100001278 <+72>: movq %rsi, -0x40(%rbp)
0x10000127c <+76>: movq %rax, %rsi
0x10000127f <+79>: movl $0x21, %edx
0x100001284 <+84>: movq -0x40(%rbp), %rcx
0x100001288 <+88>: callq 0x100006302 ; symbol stub for: swift_beginAccess
0x10000128d <+93>: movq 0x6f6c(%rip), %rax ; SwiftTest.s : SwiftTest.Shape + 8
0x100001294 <+100>: movq %rax, -0x28(%rbp)
0x100001298 <+104>: leaq -0x28(%rbp), %rdi
0x10000129c <+108>: callq 0x100001cb0 ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
0x1000012a1 <+113>: movq -0x28(%rbp), %rdi
0x1000012a5 <+117>: leaq 0x6f4c(%rip), %r13 ; SwiftTest.s : SwiftTest.Shape
0x1000012ac <+124>: callq 0x100001350 ; SwiftTest.Shape.side.setter : Swift.Int at main.swift:632
0x1000012b1 <+129>: leaq -0x20(%rbp), %rdi
0x1000012b5 <+133>: callq 0x100006320 ; symbol stub for: swift_endAccess
0x1000012ba <+138>: xorl %eax, %eax
0x1000012bc <+140>: addq $0x38, %rsp
0x1000012c0 <+144>: popq %r13
0x1000012c2 <+146>: popq %rbp
0x1000012c3 <+147>: retq
這次,我們發現跟計算屬性有些類似,這裡也用到了函數棧的局部變量,它的作用是用來承載計算屬性的值,然後被傳入test函數的同樣是這個局部變量的位址(引用),但是我很好奇為何要多此一舉,計算屬性因為本身沒有固定的記憶體,是以很好了解必須借助局部變臉作為臨時宿主,但是計算屬性是有固定記憶體的,可以猜的到,這麼設計的原因肯定跟屬性觀察器有關,但是目前的代碼還不足以解釋這麼設計的意圖,但是我們看到這裡最後一步,調用了side.setter函數,🤔️side是存儲屬性,怎麼會有setter函數呢?那我們就進入它内部看看喽,它的彙編如下
SwiftTest`Shape.side.setter:
-> 0x100001350 <+0>: pushq %rbp
0x100001351 <+1>: movq %rsp, %rbp
0x100001354 <+4>: pushq %r13
0x100001356 <+6>: subq $0x28, %rsp
0x10000135a <+10>: movq $0x0, -0x10(%rbp)
0x100001362 <+18>: movq $0x0, -0x18(%rbp)
0x10000136a <+26>: movq %rdi, -0x10(%rbp)
0x10000136e <+30>: movq %r13, -0x18(%rbp)
0x100001372 <+34>: movq 0x8(%r13), %rax
0x100001376 <+38>: movq %rax, %rcx
0x100001379 <+41>: movq %rdi, -0x20(%rbp)
0x10000137d <+45>: movq %r13, -0x28(%rbp)
0x100001381 <+49>: movq %rax, -0x30(%rbp)
0x100001385 <+53>: callq 0x1000013b0 ; SwiftTest.Shape.side.willset : Swift.Int at main.swift:633
0x10000138a <+58>: movq -0x28(%rbp), %rax
0x10000138e <+62>: movq -0x20(%rbp), %rcx
0x100001392 <+66>: movq %rcx, 0x8(%rax)
0x100001396 <+70>: movq -0x30(%rbp), %rdi
0x10000139a <+74>: movq %rax, %r13
0x10000139d <+77>: callq 0x1000014d0 ; SwiftTest.Shape.side.didset : Swift.Int at main.swift:636
0x1000013a2 <+82>: movq -0x30(%rbp), %rax
0x1000013a6 <+86>: addq $0x28, %rsp
0x1000013aa <+90>: popq %r13
0x1000013ac <+92>: popq %rbp
0x1000013ad <+93>: retq
原來,這個
side
的兩個屬性觀察器
willSet
和
didSet
被包裹在了這個
setter
函數裡面,而且,對于屬性
side
的指派真正發生在這個
setter
函數裡面。
是以我們看出了一個細節,屬性
side
記憶體裡的值被修改的時間點,是在
test
函數之後,也就是這個
setter
函數裡,也就是
test
函數其實并沒有修改
side
的值。
因為
test
函數的功能拿到一段記憶體,并且修改裡面的值,如果目前我們将
side
的位址送出給
test
,除了能夠修改
side
記憶體裡值以外,它是無法觸發
side
的屬性觀察器的。是以看得出局部變量以及
setter
函數出現在這裡的意義就是為了能夠去觸發屬性
side
的屬性觀察器。因為我們使用了局部變量,是以對于帶有屬性觀察器的存儲屬性,也可以說inout對其采用了
Copy In Copy Out
的做法。
通過程式運作之後的輸出結果,也可以驗證我們已上的結論
開始test函數
willSetSide 20
didSetSide 4 20
Program ended with exit code: 0
inout
的本質總結
inout
-
如果實參有實體記憶體位址,且沒有設定屬性觀察器
則直接将實參的記憶體位址傳入函數(
)實參進行引用傳遞
-
如果實參是計算屬性 或者 設定了屬性觀察器
則采取了 Copy In Copy Out的做法
- 調用該函數時,先複制實參的值,産生副本【可以了解成
操作】get
- 将副本的記憶體位址傳入函數(
),在函數内部可以修改副本的值副本進行引用傳遞
- 函數傳回後,再将副本的值覆寫實參的值【可以了解成
操作】set
- 調用該函數時,先複制實參的值,産生副本【可以了解成
總結:的本質就是
inout
(位址傳遞)
引用傳遞
類型屬性(Type Property)
- 嚴格來說,屬性可以劃分為:
- 執行個體屬性(Instance Property):隻能通過執行個體去通路
- 存儲執行個體屬性(Stored Instance Property):存儲在執行個體的記憶體中,每個執行個體都有一份
- 計算執行個體屬性(Computed Instance Property):
- 類型屬性(Type Property):隻能通過類型去通路
- 存儲類型屬性(Stored Type Property):整個程式的運作過程中,就隻有一份記憶體,它的本質就是全局變量
- 計算類型屬性(Computed Type Property)
- 執行個體屬性(Instance Property):隻能通過執行個體去通路
- 可以通過
定義類型屬性,對于類來說,還可以用關鍵字static
class
類型屬性細節
-
不同于存儲執行個體屬性,你必須給存儲類型屬性設定初始值
因為類型沒有像執行個體那樣的
初始化器來初始化存儲屬性init
Swift底層原理探索----屬性 & 方法屬性方法下标 - 存儲類型屬性預設就是
, 會在第一次使用的時候才初始化lazy
- 就算被多個線程同時通路,保證隻會初始化一次,可以保證線程安全(系統底層會有加鎖處理)
- 存儲類型屬性可以時
,因為這裡壓根不存在執行個體初始化的過程let
- 枚舉類型也可以定義類型屬性(存儲類型屬性、計算類型屬性)
單例模式
public class FileManager {
public static let shared = FileManager()
private init(){
}
}
-
:public static let shared = FileManager()
- 通過
定義了一個類型存儲屬性,static
-
確定在任何場景下,外界都能通路,public
-
保證了let
隻會被指派給FileManager()
一次,并且確定了線程安全,也就是說shared
方法隻會被調用一次,這樣就確定init()
隻會存在唯一一個執行個體,這就是Swift中的單例。FileManager
- 通過
-
:private init()
確定了外界是無法手動調用private
來建立執行個體,是以通過FileManager()
屬性得到的shared
執行個體永遠是相同的一份,這也符合了我們對與單例的要求。FileManager
類型(static)存儲屬性的本質
前面我們介紹static存儲屬性的時候,提到了它實際上是全局變量,現在來證明一下,首先我們看看普通的全局變量是怎麼樣的
var num1 = 10 // 此處加斷點
var num2 = 11
var num3 = 12
運作至斷點處,彙編如下
SwiftTest`main:
0x100001120 <+0>: pushq %rbp
0x100001121 <+1>: movq %rsp, %rbp
0x100001124 <+4>: xorl %eax, %eax
-> 0x100001126 <+6>: movq $0xa, 0x60af(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100001131 <+17>: movq $0xb, 0x60ac(%rip) ; SwiftTest.num1 : Swift.Int + 4
0x10000113c <+28>: movq $0xc, 0x60a9(%rip) ; SwiftTest.num2 : Swift.Int + 4
0x100001147 <+39>: popq %rbp
0x100001148 <+40>: retq
很明顯,下圖的這三句分别對應的就是
num1
、
num2
、
num3
我們來算一下他們的實際記憶體位址
-
&num1 = 0x60af + 0x100001131 = 0x1000071E0
-
&num2 = 0x60ac + 0x10000113c = 0x1000071E8
-
&num3 = 0x60a9 + 0x100001147 = 0x1000071F0
它們就是全局資料段上的3段連續記憶體空間。接下來我們加入static存儲屬性如下
var num1 = 10 // 斷點處
class Car {
static var num2 = 1
}
Car.num2 = 11
var num3 = 12
打開斷點處的彙編
SwiftTest`main:
0x100000d80 <+0>: pushq %rbp
0x100000d81 <+1>: movq %rsp, %rbp
0x100000d84 <+4>: subq $0x30, %rsp
-> 0x100000d88 <+8>: movq $0xa, 0x6595(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100000d93 <+19>: movl %edi, -0x1c(%rbp)
0x100000d96 <+22>: movq %rsi, -0x28(%rbp)
0x100000d9a <+26>: callq 0x100000e40 ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
0x100000d9f <+31>: xorl %ecx, %ecx
0x100000da1 <+33>: movq %rax, %rdx
0x100000da4 <+36>: movq %rdx, %rdi
0x100000da7 <+39>: leaq -0x18(%rbp), %rsi
0x100000dab <+43>: movl $0x21, %edx
0x100000db0 <+48>: movq %rax, -0x30(%rbp)
0x100000db4 <+52>: callq 0x1000053a2 ; symbol stub for: swift_beginAccess
0x100000db9 <+57>: movq -0x30(%rbp), %rax
0x100000dbd <+61>: movq $0xb, (%rax)
0x100000dc4 <+68>: leaq -0x18(%rbp), %rdi
0x100000dc8 <+72>: callq 0x1000053c6 ; symbol stub for: swift_endAccess
0x100000dcd <+77>: xorl %eax, %eax
0x100000dcf <+79>: movq $0xc, 0x655e(%rip) ; static SwiftTest.Car.num2 : Swift.Int + 4
0x100000dda <+90>: addq $0x30, %rsp
0x100000dde <+94>: popq %rbp
0x100000ddf <+95>: retq
如上圖所示,首先我們可以快速定位
num1
和
num3
,我們可以先記錄一下他們的記憶體位址
-
&num1 = 0x6595 + 0x100000d93 = 0x100007328
-
&num3 = 0x655e + 0x100000dda = 0x100007338
在
num1
和
num2
中間,我們發現了一個叫
Car.num2.unsafeMutableAddressor
的函數被調用,并且通過将它的傳回值作為位址通路了一段記憶體空間,并向其指派
11
,從
Car.num2.unsafeMutableAddressor
這個名字,我們可以看出,這個函數傳回出來的位址,就是
Car.num2
的位址,首先我們運作到
0x100000dbd <+61>: movq $0xb, (%rax)
這句彙編,記錄一下這個位址的值
(lldb) register read rax
rax = 0x0000000100007330 SwiftTest`static SwiftTest.Car.num2 : Swift.Int
👆👆👆可以看到,這個位址正好是和
num1
之間的那段空間,是以雖然
num3
作為
num2
的
Car
存儲屬性,但是從它在記憶體中的位置來看,跟普通的全局變量沒有差別,是以可以說static存儲屬性的本質就是全局變量。
static
代碼稍微調整一下
var num1 = 10
class Car {
static var num2 = 1
}
//Car.num2 = 11 //将這一句注釋掉
var num3 = 12
**********************👇對應彙編👇***********************
SwiftTest`main:
0x100000dc0 <+0>: pushq %rbp
0x100000dc1 <+1>: movq %rsp, %rbp
0x100000dc4 <+4>: xorl %eax, %eax
-> 0x100000dc6 <+6>: movq $0xa, 0x6557(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100000dd1 <+17>: movq $0xc, 0x655c(%rip) ; static SwiftTest.Car.num2 : Swift.Int + 4
0x100000ddc <+28>: popq %rbp
0x100000ddd <+29>: retq
👆👆👆可以看出,彙編裡相關的代碼就消失了,也就是說如果沒有用到
Car.num2
,那麼它是不會被初始化的,是以我們說
Car.num2
存儲屬性是預設
static
(延遲)的。
lazy
我們将代碼恢複,再次更深入的跟蹤一下彙編過程
var num1 = 10 // 斷點處
class Car {
static var num2 = 1
}
Car.num2 = 11
var num3 = 12
**********************👇對應彙編👇***********************
SwiftTest`main:
0x100000d80 <+0>: pushq %rbp
0x100000d81 <+1>: movq %rsp, %rbp
0x100000d84 <+4>: subq $0x30, %rsp
-> 0x100000d88 <+8>: movq $0xa, 0x6595(%rip) ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
0x100000d93 <+19>: movl %edi, -0x1c(%rbp)
0x100000d96 <+22>: movq %rsi, -0x28(%rbp)
0x100000d9a <+26>: callq 0x100000e40 ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
0x100000d9f <+31>: xorl %ecx, %ecx
0x100000da1 <+33>: movq %rax, %rdx
0x100000da4 <+36>: movq %rdx, %rdi
0x100000da7 <+39>: leaq -0x18(%rbp), %rsi
0x100000dab <+43>: movl $0x21, %edx
0x100000db0 <+48>: movq %rax, -0x30(%rbp)
0x100000db4 <+52>: callq 0x1000053a2 ; symbol stub for: swift_beginAccess
0x100000db9 <+57>: movq -0x30(%rbp), %rax
0x100000dbd <+61>: movq $0xb, (%rax)
0x100000dc4 <+68>: leaq -0x18(%rbp), %rdi
0x100000dc8 <+72>: callq 0x1000053c6 ; symbol stub for: swift_endAccess
0x100000dcd <+77>: xorl %eax, %eax
0x100000dcf <+79>: movq $0xc, 0x655e(%rip) ; static SwiftTest.Car.num2 : Swift.Int + 4
0x100000dda <+90>: addq $0x30, %rsp
0x100000dde <+94>: popq %rbp
0x100000ddf <+95>: retq
這一次我們從
unsafeMutableAddressor
這個函數跟進去看看
SwiftTest`Car.num2.unsafeMutableAddressor:
-> 0x100000e40 <+0>: pushq %rbp
0x100000e41 <+1>: movq %rsp, %rbp
0x100000e44 <+4>: cmpq $-0x1, 0x64f4(%rip) ; SwiftTest.num3 : Swift.Int + 7
0x100000e4c <+12>: sete %al
0x100000e4f <+15>: testb $0x1, %al
0x100000e51 <+17>: jne 0x100000e55 ; <+21> at main.swift:719:16
0x100000e53 <+19>: jmp 0x100000e5e ; <+30> at main.swift
0x100000e55 <+21>: leaq 0x64d4(%rip), %rax ; static SwiftTest.Car.num2 : Swift.Int
0x100000e5c <+28>: popq %rbp
0x100000e5d <+29>: retq
0x100000e5e <+30>: leaq -0x45(%rip), %rax ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
0x100000e65 <+37>: leaq 0x64d4(%rip), %rdi ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_token0
0x100000e6c <+44>: movq %rax, %rsi
0x100000e6f <+47>: callq 0x1000053fc ; symbol stub for: swift_once
0x100000e74 <+52>: jmp 0x100000e55 ; <+21> at main.swift:719:16
看到在最後,調用了
swift_once
函數,GCD裡面我們知道有個
dispatch_once
,是否有關聯呢,我們進入這個函數
libswiftCore.dylib`swift_once:
-> 0x7fff73447820 <+0>: pushq %rbp
0x7fff73447821 <+1>: movq %rsp, %rbp
0x7fff73447824 <+4>: cmpq $-0x1, (%rdi)
0x7fff73447828 <+8>: jne 0x7fff7344782c ; <+12>
0x7fff7344782a <+10>: popq %rbp
0x7fff7344782b <+11>: retq
0x7fff7344782c <+12>: movq %rsi, %rax
0x7fff7344782f <+15>: movq %rdx, %rsi
0x7fff73447832 <+18>: movq %rax, %rdx
0x7fff73447835 <+21>: callq 0x7fff7349c19c ; symbol stub for: dispatch_once_f
0x7fff7344783a <+26>: popq %rbp
0x7fff7344783b <+27>: retq
0x7fff7344783c <+28>: nop
0x7fff7344783d <+29>: nop
0x7fff7344783e <+30>: nop
0x7fff7344783f <+31>: nop
真相出現了,原來
swift_once
函數裡面确實是調用了GCD的
dispatch_once_f
,那麼
dispatch_once
裡面的
block
是什麼呢,直覺告訴我們應該就是
Car.num2
的初始化代碼,也就是這句代碼
static var num2 = 1
如何證明呢?我先我們将彙編運作到
callq 0x7fff7349c19c ; symbol stub for: dispatch_once_f
處,因為此時,
dispatch_once_f
函數所需的參數按照彙編的慣例,已經放到了
rsi
、
rdx
等寄存起裡面了,我們可以檢視一下此時這兩個寄存器的内容
(lldb) register read rsi
rsi = 0x00007ffeefbff598
(lldb) register read rdx
rdx = 0x0000000100000e20 SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
(lldb)
可以看到
rdx
此時存放的是一個跟globalinit(全局初始化)相關的函數
func0
,位址為
0x0000000100000e20
,該函數就是
dispatch_once_f
所接受的
block
。接下來我們回到Swift源碼,在如下處加一個斷點
那麼我們繼續運作程式,斷點會停在上面這句代碼上,如果我們猜測正确的話,那麼此時的彙編應該就在
globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0
這個函數裡面,我們運作程式後,彙編如下
SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0:
0x100000e20 <+0>: pushq %rbp
0x100000e21 <+1>: movq %rsp, %rbp
-> 0x100000e24 <+4>: movq $0x1, 0x6501(%rip) ; SwiftTest.num1 : Swift.Int + 4
0x100000e2f <+15>: popq %rbp
0x100000e30 <+16>: retq
确實是處在
globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0
函數内部,并且這裡進行初始化的記憶體位址是
0x100000e2f + 0x6501 = 0x100007330
,從初始值很明顯看出這段記憶體就是
num2
,并且跟我們在
unsafeMutableAddressor
函數傳回處記錄的傳回值相同,結果正如預期,證明完畢。
👆👆👆在Swift底層,是通過->
unsafeMutableAddressor
->
libswiftCore.dylib-swift_once
---------->
libswiftCore.dylib-dispatch_once_f:
來對
static var num2 = 1
進行初始化的,因為使用了
num2
的
GCD
,是以我們說
dispatch_once
存儲屬性是線程安全的,并且隻能被初始化一次。
static
方法
方法
class Car {
static var count = 0
init() {
Car.count += 1
}
// Type Method
static func getCount() -> Int {
//以下幾種通路count的方法是等價的
count += 1
self.count += 1
Car.self.count += 1
Car.count += 1
return count
}
}
let c0 = Car()
let c1 = Car()
let c2 = Car()
print(Car.getCount()) // 通過類名進行調用
枚舉、結構體、類都可以定義執行個體方法、類型方法
- 執行個體方法(
):通過執行個體對象進行調用Instance Method
- 類型方法(
):通過類型調用,用Type Method
或者static
關鍵字來定義class
self
- 在執行個體方法中就代表執行個體對象
- 在類型方法中就代表類型
在類型方法
static func getCount
中,以下幾種寫法等價
-
count
-
self.count
-
Car.count
-
Car.self.count
mutating
Swift文法規定,對于結構體和枚舉這兩種值類型,預設情況下,他們的屬性是不能被自身的執行個體方法所修改的(對于類沒有這個規定)
- 在
關鍵字前面加func
就可以允許這種修改行為,如下mutating
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(deltaX: Double, deltaY: Double) {
x += deltaX
y += deltaY
}
}
enum StateSwitch {
case low, middle, high
mutating func next() {
switch self {
case .low:
self = .middle
case .middle:
self = .high
case .high:
self = .low
}
}
}
@discardableResult
在func前面加上@discardableResult,可以消除:函數調用後的傳回值未被使用的警告資訊⚠️
struct Point {
var x = 0.0, y = 0.0
@discardableResult mutating
func moveX(deltaX: Double) -> Double {
x += deltaX
return x
}
}
var p = Point()
p.moveX(deltaX: 10)
下标
使用
subscript
可以給任意類型(枚舉、類、結構體)增加下表功能。
subscript
的文法類似于執行個體方法、計算屬性,它的本質就是方法(函數)
class Point {
var x = 0.0, y = 0.0
subscript(index: Int) -> Double {
set {
if index == 0 {
x = newValue
} else if index == 1 {
y = newValue
}
}
get {
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
}
var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x) // 11.1
print(p.y) // 22.2
print(p[0]) // 11.1
print(p[1]) // 22.2
從上面的案例來看,
subscript
為我們提供了通過
[i]
的方式去通路成員變量,就像數組/字典那樣去使用。下标與函數的表面差別,隻是在定義的時候,用
subscript
代替了
func funcName
,在調用的時候通過
[arg]
代替了
funcName(arg)
。而
subscript
的内部包含了
get
和
set
,很像計算屬性。
我們簡化一下代碼
class Point {
var x = 0, y = 0
subscript(index: Int) -> Int {
set {
if index == 0 {
x = newValue
} else if index == 1 {
y = newValue
}
}
get {
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
}
var p = Point()
p[0] = 10 // 0xa 在這裡放一個斷點⚠️
p[1] = 11 // 0xb
運作程式至斷點處,彙編如下
我們我們根據立即數10和11,找到綠框處代碼,紅色标記處的函數顯然不是下标的調用,我們從兩個綠框處的間接函數調用跟進去看看
0x1000016b1 <+145>: callq *0x98(%rcx) ---進入該函數-->
SwiftTest`Point.subscript.setter:
-> 0x100001c10 <+0>: pushq %rbp
0x100001c11 <+1>: movq %rsp, %rbp
0x100001c14 <+4>: pushq %r13
0x100001c16 <+6>: subq $0x48, %rsp
0x100001c1a <+10>: xorl %eax, %eax
0x100001c1c <+12>: leaq -0x10(%rbp), %rcx
0x100001c20 <+16>: movq %rdi, -0x28(%rbp)
..........
..........
..........
0x100001715 <+245>: callq *0x98(%rcx) ---進入該函數-->
SwiftTest`Point.subscript.setter:
-> 0x100001c10 <+0>: pushq %rbp
0x100001c11 <+1>: movq %rsp, %rbp
0x100001c14 <+4>: pushq %r13
0x100001c16 <+6>: subq $0x48, %rsp
0x100001c1a <+10>: xorl %eax, %eax
0x100001c1c <+12>: leaq -0x10(%rbp), %rcx
0x100001c20 <+16>: movq %rdi, -0x28(%rbp)
..........
..........
..........
上面的結果說明
callq *0x98(%rcx)
=
Point.subscript.setter
等價于
p[i] =
是以,證明了下标的本質就是函數。
🍬🍬🍬這裡為什麼是來間接調用函數呢,因為
callq *[記憶體位址]
不是一個函數名,而是一個變量,是以想要調用下标函數,是以肯定是通過
p
間接調用
的方式來操作的。
直接調用:
間接調用:
callq 函數位址
callq *記憶體位址
注意點⚠️
-
中定義的傳回值類型可以決定:subscript
-
方法的傳回值類型get
-
方法中國呢set
的類型newValue
-
-
可以接受多個參數,并且是任意類型subscript
下标的細節
subscript
可以沒有
set
方法,但是必須要有
get
方法,如果隻有
get
方法,可以了解為隻讀
class Point {
var x = 0.0, y = 0.0
subscript(index: Int) -> Double {
get {
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
}
如果隻有
get
方法,還可以省略
get
class Point {
var x = 0.0, y = 0.0
subscript(index: Int) -> Double {
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
還可以設定參數标簽
class Point {
var x = 0.0, y = 0.0
subscript(index i: Int) -> Double {
if i == 0 {
return x
} else if i == 1 {
return y
}
return 0
}
}
var p = Point()
p.y = 22.2
print(p[index: 1]) // 如果有标簽的話,在使用的時候,就一定要帶上标簽才行
上面我們看到的
subscript
都是相當于執行個體方法(預設),下标也可以是類型方法
class Sum {
static subscript(v1: Int, v2: Int) -> Int {
return v1 + v2
}
}
print(Sum[10,20])
結構體、類作為傳回值的對比
struct Point {
var x = 0
var y = 0
}
class PointManager {
var point = Point()
subscript(index: Int) -> Point {
set { point = newValue } // 如果後面有堆point進行指派,則必須要加上set方法。
get { point }
}
}
var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)
上面的案例中,
PointManager
這個類有一個下标,傳回類型是結構體
struct Point
,并且注意這個下标的特點,無論下标值傳什麼,它傳回的都是結構體變量
point
,我們需要注意的是,下标裡面的
set
的寫法應該如下
這樣你可能會好奇,
pm[0].x = 11
或者
pm[0].y = 22
時,在set方法裡面我們怎麼知道這個
newValue
的值到底是給
.x
還是給
.y
的。其實你應該注意到,這裡的newValue應該是
struct Point
類型的,如果這樣,其實設計者的思路就不難猜到
pm[0].x = 11
—>
newValue = (11, pm[0].y)
—>
set { point = newValue = (11, pm[0].y) }
pm[0].y = 22
—>
newValue = (pm[0].x, 22)
—>
set { point = newValue = (pm[0].x, 22) }
如果把
strtct Point
換成
class Point
, 這個
set
方法就可以不用寫了
class Point {
var x = 0
var y = 0
}
class PointManager {
var point = Point()
subscript(index: Int) -> Point {
get { point }
}
}
var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)
因為我們通過
pm[0]
拿到的是
point
這個對象執行個體指針,那麼
pm[0].x
等價于
point.x
,是以
point.x = 11
是符合規範的。
下标接受多個參數
class Grid {
var data = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
]
subscript( row: Int, column: Int) -> Int {
set {
guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
return
}
data[row][column] = newValue
}
get {
guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
return 0
}
return data[row][column]
}
}
}
var grid = Grid()
grid[0, 1] = 77
grid[1, 2] = 88
grid[2, 0] = 99
print(grid.data)
*********************運作結果
[[0, 77, 2], [3, 4, 88], [99, 7, 8]]
Program ended with exit code: 0
好了,屬性和方法,暫時梳理到這裡,period!