Swift 屬性
在Swift
中屬性主要分為存儲屬性拷泽、計算屬性碘梢、延遲存儲屬性咬摇、類型屬性這四種,并且Swift
還提供了屬性觀察者煞躬,以便開發(fā)者能在屬性的改變前后進行觀察肛鹏。下面我就來一一探索逸邦。
1. 存儲屬性
存儲屬性顧名思義,就是需要存儲的一種屬性在扰,既然存儲就要占用內存空間缕减,下面我們通過一個例子來對其進行解釋。
示例代碼:
class Teacher {
let age: Int = 18
var age2: Int = 100
}
let t = Teacher()
我們通過我的上一篇文章對Swift
的類和對象的探索芒珠,我們可以知道Swift
對象的存儲前16個字節(jié)是metadata
和refCounts
1.1 通過lldb查看存儲屬性
下面我們就通過lldb
調試來查看一下示例代碼中t
這個對象的內存結構桥狡。
通過lldb
調試我們可以看到,我們的兩個age
的值都存儲在了連續(xù)的對象存儲空間中(注:這里是16進制)
1.2 通過SIL文件查看存儲屬性
我們使用如下命令將我們的swift
文件編譯成sil
文件皱卓,并打開(注:如果打開失敗就設置一下用VSCode
打開sil
類型的文件就可以了)
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
1.2.1 類
編譯后的SIL代碼:
class Teacher {
@_hasStorage @_hasInitialValue final let age: Int { get }
@_hasStorage @_hasInitialValue var age2: Int { get set }
@objc deinit
init()
}
在sil
代碼中我們可以看到屬性的前面加上了@_hasStorage
關鍵字裹芝,這是存儲屬性獨有的,對應的是計算屬性娜汁,就沒有這個關鍵字嫂易。
1.2.2 get&set
get方法及分析:
// Teacher.age.getter
sil hidden [transparent] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self" // users: %2, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
// 找出實例內部物理實例變量的地址
%2 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %3
// 開始訪問內存
%3 = begin_access [read] [dynamic] %2 : $*Int // users: %4, %5
// 內存中的值加載到%4虛擬寄存器
%4 = load %3 : $*Int // user: %6
// 結束內存訪問
end_access %3 : $*Int // id: %5
// 返回獲取到的值
return %4 : $Int // id: %6
} // end sil function 'main.Teacher.age.getter : Swift.Int'
set方法及分析:
// Teacher.age.setter
sil hidden [transparent] @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %6, %2
// %1 "self" // users: %4, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 找出實例內部物理實例變量的地址
%4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
// 開始訪問內存
%5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
// 將第一個參數(shù)%0的值存儲到%5
store %0 to %5 : $*Int // id: %6
// 停止內存訪問
end_access %5 : $*Int // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'main.Teacher.age.setter : Swift.Int'
1.3 小結
打印內存占用大小:
對象的內存結構
這個32其實就是8+8+8+8得到的掐禁,在Swift
中對象的本質是HeapObject
內部有兩個屬性metadata
占用8字節(jié)怜械,refCounts
占用8字節(jié),Swift
中Int
實際是個結構體傅事,占用8自己缕允,這里兩個Int
就是16字節(jié)。
綜上所述:
- 存儲屬性是占用對象的分配的內存空間
2. 計算屬性
計算屬性享完,指的就是需要通過計算得到一些想要的值灼芭,那么對于屬性來說計算就是get
和set
有额,下面我們就通過一個舉例來說明一下計算屬性般又。
示例代碼:
class Teacher {
var age: Int {
get {
return 18
}
set {
age = newValue
}
}
}
let t = Teacher()
t.age = 10
print("end")
對于計算屬性就是屬性自己實現(xiàn)get
和set
方法,但是示例代碼中這樣寫是不對的巍佑,首先會有警告:
??警告的意思就是會自己調用自己茴迁,如果運行這段代碼就會造成無限遞歸調用而崩潰:
2.1 計算屬性的使用
在上面的介紹中,是我們通常對屬性的get
和set
寫法萤衰,既然這樣不對堕义,那么計算屬性該怎么用呢?下面我們在來看一個正方形的示例脆栋。
實例代碼:
class Square{
var width: Double = 10.0
var area: Double{
get{
//這里的return可以省略倦卖,編譯器會自動推導
return width * width
}
set{
width = sqrt(newValue)
}
}
}
let s = Square()
s.width = 8.0
print(s.area)
s.area = 81.0
print(s.width)
print("end")
打印結果:
這里我們可以看到可以通過對計算屬性的get
和set
方法內的一些操作,實現(xiàn)對其他屬性的修改椿争。
2.3 通過lldb查看計算屬性的存儲
這里的0x4024000000000000
就是Swift
中對Double
類型的存儲怕膛,這里我們對width
的賦值為10。
2.2 通過SIL文件查看計算屬性
按照上面所說的命令秦踪,我們在編譯一個sil
文件褐捻,sil
代碼如下:
2.2.1 類
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
我們看到計算屬性area
沒有了@_hasStorage
的修飾掸茅。
2.2.2 get&set
get方法及分析:
// Square.area.getter
sil hidden @main.Square.area.getter : Swift.Double : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self" // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
debug_value %0 : $Square, let, name "self", argno 1 // id: %1
// width get 方法的函數(shù)地址
%2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
// 調用函數(shù),參數(shù)是%0-self柠逞,返回值width
%3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
// width get 方法的函數(shù)地址
%4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
// 調用函數(shù)昧狮,參數(shù)是%0-self,返回值width
%5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
// 轉成 結構體
%6 = struct_extract %3 : $Double, #Double._value // user: %8
// 轉成 結構體
%7 = struct_extract %5 : $Double, #Double._value // user: %8
// 計算板壮,并將返回值存儲到%8
%8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
// 將%8 的值包裝到%9
%9 = struct $Double (%8 : $Builtin.FPIEEE64) // user: %10
// 返回
return %9 : $Double // id: %10
} // end sil function 'main.Square.area.getter : Swift.Double'
set方法及分析:
// Square.area.setter
sil hidden @main.Square.area.setter : Swift.Double : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "newValue" // users: %5, %2
// %1 "self" // users: %7, %6, %3
bb0(%0 : $Double, %1 : $Square):
debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
debug_value %1 : $Square, let, name "self", argno 2 // id: %3
// function_ref sqrt
// 獲取sqrt函數(shù)的地址
%4 = function_ref @sqrt : $@convention(c) (Double) -> Double // user: %5
// 調用sqrt函數(shù)
%5 = apply %4(%0) : $@convention(c) (Double) -> Double // user: %7
// 獲取width set 方法的函數(shù)地址
%6 = class_method %1 : $Square, #Square.width!setter : (Square) -> (Double) -> (), $@convention(method) (Double, @guaranteed Square) -> () // user: %7
// 調用width set 方法逗鸣,傳入?yún)?shù)%5---sqrt方法的返回值,%1---self
%7 = apply %6(%5, %1) : $@convention(method) (Double, @guaranteed Square) -> ()
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'main.Square.area.setter : Swift.Double'
2.3 小結
打印內存占用大写戮:
這里的24 就是8+8+8得來的慕购,所以可以得到一個結論就是計算屬性不占用對象分配的內存空間。
綜上所述:
- 計算屬性的本質就是
get
和set
方法 - 計算屬性不占用對象的內存空間
3. 延遲存儲屬性
延遲存儲屬性就是我們常說的懶加載茬底,懶加載就是使用的時候在加載沪悲,那么我們就來看看Swift
中這個延遲是什么樣的。舉個例子:
class Teacher {
lazy var age:Int = 10
}
let t = Teacher()
print(t.age)
print("end")
3.1 通過lldb查看延遲存儲屬性在內存中的存儲
延遲存儲屬性實際上還是存儲屬性阱表,既然是存儲屬性肯定是占用對象分配的內存的殿如,下面我們通過lldb
來看看延遲存儲屬性在內存中是怎樣存儲的。
lldb調試:
在這個截圖中第一段x/8gx
打印的是第一個斷點時的數(shù)據(jù)最爬,第二個是打印的第二斷點時的數(shù)據(jù)涉馁。
- 通過以上的數(shù)據(jù)我們可以看到,延遲存儲屬性在不使用的時候是沒有存儲值的爱致,雖然在代碼的初始化中我們給它賦了個初始值
10
- 當我們使用了這個屬性后烤送,在內存中打印的時候就可以在看到這個值出現(xiàn)在了內存段中,這里的
10
是就是16進制的0xa
3.2 通過sil代碼進一步探索延遲存儲屬性
修改Swift
為如下糠悯,這里我們即使用set
也使用get
:
class Teacher {
lazy var age:Int = 10
}
let t = Teacher()
t.age = 18
print(t.age)
print("end")
使用如下命令將我們的main.swift
文件編譯成sil
文件帮坚,這里還刪除了原有的sil
文件,并通過xcrun swift-demangle
命令還原sil
代碼中的混淆互艾。
rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
3.2.1 類
class Teacher {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
我們可以看到在sil
代碼中Teacher
這個類中的延遲存儲屬性:
- 有一個和
swift
代碼中書寫差不多的屬性试和,帶有get
和set
方法 - 在存儲屬性的基礎上對
var
變量增加了final
修飾 - 生成了
$__lazy_storage_$_age
這個可選(optional
)變量
3.2.2 main 函數(shù)
針對main函數(shù)中的主要調用在截圖中加了注釋:
- 首先創(chuàng)建變量
t
的內存地址方到%3
- 獲取
t
的metadata
- 獲取
__allocating_init
的函數(shù)地址,用于對象的初始化 - 調用
__allocating_init
函數(shù)傳入metadata
初始化變量t
- 將初始化的結果存如
%3
的地址中 - 將
%3
這個內存地址中的t對象的數(shù)據(jù)加載到%8
中 - 將要賦值的18這個值放到
%9
這個虛擬寄存器中 - 通過
%9
中的值初始化一個Int
類型的的結構體放到%10
中 -
age
屬性的set
方法放到%11
中 - 調用
age
的set
方法纫普,傳入%10--18
和%8--self
- 同
%8
一致阅悍,將%3
這個內存地址中的t
對象的數(shù)據(jù)加載到%19
中 -
age
的get
方法放到%20
中 - 調用
age
的get
方法,傳入%19--self
參數(shù)昨稼,并將返回值存儲到%21
中
3.2.3 set方法
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %4, %2
// %1 "self" // users: %5, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 將傳入的value值%0存儲到%4中
%4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
// 獲取懶加載的$__lazy_storage_$_age的地址存放到%5中
%5 = ref_element_addr %1 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %6
// 開始訪問age的內存
%6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
// 將%4的值存儲到%6中
store %4 to %6 : $*Optional<Int> // id: %7
// 結束內存的訪問
end_access %6 : $*Optional<Int> // id: %8
// 返回一個空的元組
%9 = tuple () // user: %10
return %9 : $() // id: %10
} // end sil function 'main.Teacher.age.setter : Swift.Int'
由sil
中的set
方法我們可以知道节视,在延遲存儲屬性中:
- 獲取延遲屬性的內存地址
- 將傳入的值存儲到內存地址中
3.2.4 get方法
// Teacher.age.getter
sil hidden [lazy_getter] [noinline] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
// 獲取$__lazy_storage_$_age的內存地址存放到%2中
%2 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %3
// 開始訪問內存
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
// 加載內存中數(shù)據(jù)存放到%4中
%4 = load %3 : $*Optional<Int> // user: %6
// 結束內存訪問
end_access %3 : $*Optional<Int> // id: %5
// 如果%4中有值(some)則跳轉bb1,如果沒有值(none)則跳轉到bb2
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
// %7 // users: %9, %8
bb1(%7 : $Int): // Preds: bb0
debug_value %7 : $Int, let, name "tmp1" // id: %8
// 跳轉bb3
br bb3(%7 : $Int) // id: %9
bb2: // Preds: bb0
// 將給age的初始值10取出存放到%10中
%10 = integer_literal $Builtin.Int64, 10 // user: %11
// 用一個Int類型的結構體包裝這個10存放到%11
%11 = struct $Int (%10 : $Builtin.Int64) // users: %18, %13, %12
debug_value %11 : $Int, let, name "tmp2" // id: %12
// 判斷self是否為空假栓,不空則將%11的值存放到%13中寻行,否則$Int
%13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
// 獲取$__lazy_storage_$_age的內存地址存放到%14中
%14 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %15
// 開始訪問內存
%15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
// 將%13中的值存儲到%15
store %13 to %15 : $*Optional<Int> // id: %16
// 結束內存訪問
end_access %15 : $*Optional<Int> // id: %17
// 跳轉bb3
br bb3(%11 : $Int) // id: %18
// %19 // user: %20
bb3(%19 : $Int): // Preds: bb2 bb1
// 返回傳入的值%19,如果%19為空則返回$Int
return %19 : $Int // id: %20
} // end sil function 'main.Teacher.age.getter : Swift.Int'
由sil
中的get
方法我們可以知道但指,在延遲存儲屬性中:
- 獲取延遲屬性的內存地址
- 然后取出該地址中的值
- 如果獲取的值為空則將默認值取出并返回
- 如果不為空則直接返回取的值
通過該sil
代碼我們可以看出寡痰,延遲存儲屬性的get
是線程不安全的抗楔,為什么不安全呢?
- 如果線程1調用
get
拦坠,執(zhí)行完bb0
连躏,判斷age
沒有值,將會執(zhí)行bb2
- 此時正好
cpu
時間片分配給了線程2贞滨,線程2也訪問age
入热,此時age
依然沒有值,同樣會走bb2
給age
賦初始值 - 待線程2執(zhí)行完畢后線程1又獲得執(zhí)行權限晓铆,同樣開始執(zhí)行
bb2
勺良,又會給age
賦初值值 - 以上并不會影響
age
的值,如果線程2調用的是set
此時修改了age
的值骄噪,然后線程1在執(zhí)行bb2
的時候尚困,給age
賦初始值,就會因線程問題導致我們age
的值不準確
3.3 內存的占用
打印非延遲屬性和延遲屬性類的內存的占用大小
可以看到在使用lazy
修飾的延遲存儲屬性在對象的內存占用上會比不使用lazy
修飾的多链蕊,那么這是為什么呢事甜?
- 我們在上面分析
sil
代碼的時候知道lazy
修飾的屬性在底層是可選類型,在這里實際就是Optional<Int>
滔韵。 - 不使用
lazy
修飾的時候逻谦,對象的內存占用是24,是由metadat
+refCounts
+Int
得來的 - 使用
lazy
修飾的時候陪蜻,對象的內存占用是32是由metadat
+refCounts
+Optional<Int>
得來的
這里我們就來看看Optional<Int>
的內存占用:
可以看到Optional<Int>
的size是9邦马,其實這里是Int
的8,加上Optional
的1宴卖,Optional
在底層是個枚舉滋将,默認是Int8
占用1字節(jié),在后續(xù)會詳細說明嘱腥。由于需要內存對齊耕渴,都是8字節(jié)8字節(jié)的讀取,所以剩下不用的也要補齊齿兔。
3.4 lazy的其他用法
let data = 0...3
let result = data.lazy.map { (i: Int) -> Int in
print("Handling...")
return i * 2
}
print("Begin:")
for i in result {
print(i)
}
在 Swift 標準庫中,也有一些 Lazy 方法础米,就可以在不需要運行時分苇,避免消耗太多的性能。
打印結果如下:
3.5 小結
經(jīng)過上面的分析現(xiàn)總結如下:
- 延遲存儲屬性需要使用
lazy
修飾 - 延遲存儲屬性必須有一個默認的初始值
- 延遲存儲屬性在第一次訪問的時候才能被賦值
- 延遲存儲屬性并不保證線程安全
- 延遲存儲屬性對實例對象的大小有影響
4. 類型屬性
類型屬性跟類方法的命名類似屁桑,是屬于類的一個屬性医寿,不能通過實例對象去訪問,類型屬性需要使用static
進行修飾蘑斧,舉個例子:
class Teacher {
static var age:Int = 18
}
let t = Teacher()
Teacher.age = 20
print(Teacher.age)
print("end")
4.1 通過lldb對類型屬性初步探索
類型屬性也是屬性靖秩,那么類型屬性會存儲在哪里呢须眷?下面我們通過lldb
調試,分別從對象沟突、類花颗、類型屬性的內存來對類型屬性進行初步探索。
我們首先打印了對象t
惠拭,查看對象的內存扩劝,并沒有找到對象中對age
的存儲。
隨后我們查看對象的metadata
职辅,也并沒有相關屬性棒呛。
我們又打印了Teacher.age
,得到的直接是age
的值
那么類型屬性到底存儲在哪里呢域携?下面我們通過sil
代碼來看看簇秒。
4.2 通過sil進一步探索類型屬性
生成sil
代碼的方式見3.2
這里為了簡化sil
代碼,將Swift
代碼修改成如下:
class Teacher {
static var age:Int = 18
}
Teacher.age = 20
4.2.1 類
class Teacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
在類中主要的區(qū)別就是:相對于普通的存儲屬性增加了static
修飾秀鞭。
在這里我們并沒有得出什么有用的結論宰睡,下面我們繼續(xù)往下看:
此時我們看到如上圖所示的sil_global
,這里我們的Teacher.age
已經(jīng)是一個全局變量了气筋。下面我們就去main
函數(shù)中看看age
的初始化拆内。
4.2.2 main函數(shù)
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = metatype $@thick Teacher.Type
// function_ref Teacher.age.unsafeMutableAddressor
%3 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %4
// 調用%3中存儲的函數(shù),按照命名應該是獲取Teacher.age的內存地址宠默,并將返回結果存儲到%4
%4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
// 將%4的地址存放到%5
%5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
// 構建20這個值
%6 = integer_literal $Builtin.Int64, 20 // user: %7
// 構建Int結構體
%7 = struct $Int (%6 : $Builtin.Int64) // user: %9
// 開始方法內存麸恍,%5中的,也就是age的地址
%8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
// 將%7存儲到%8中搀矫,也就是將20存儲到age的地址
store %7 to %8 : $*Int // id: %9
// 結束內存訪問
end_access %8 : $*Int // id: %10
%11 = integer_literal $Builtin.Int32, 0 // user: %12
%12 = struct $Int32 (%11 : $Builtin.Int32) // user: %13
return %12 : $Int32 // id: %13
} // end sil function 'main'
經(jīng)過對main
函數(shù)的分析抹沪,我們可以知道:
- 類型屬性的內存地址是通過
xxx.unsafeMutableAddressor
函數(shù)獲取到的 - 然后將需要存儲的值存儲到這個內存中
- 這也就很好的解釋了我們在
lldb
調試的時候為什么打印出來的是存儲的值。
4.2.3 xxx.unsafeMutableAddressor
// Teacher.age.unsafeMutableAddressor
sil hidden [global_init] @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
// 全局地址
%0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
// 全局地址存放到%1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
// 函數(shù)地址存儲到%2
%2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
// once 保證全局地址只被初始化一次
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
// 初始化的全局地址存放到%4
%4 = global_addr @static main.Teacher.age : Swift.Int : $*Int // user: %5
// 地址轉為指針存放到%5
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
// 返回
return %5 : $Builtin.RawPointer // id: %6
} // end sil function 'main.Teacher.age.unsafeMutableAddressor : Swift.Int'
我們可以在這個函數(shù)中看到:
- 通過
@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
創(chuàng)建一個全局地址瓤球,這個就是類下面那段代碼 - 這里面還用到了
builtin "once"
來保證值初始化一次
4.2.4 builtin "once"
那么這個builtin "once"
是怎么保證類型屬性只初始化一次的呢融欧?
我們通過添加斷點和匯編調試來看看。
添加斷點:
開啟匯編調試:
匯編代碼:
在匯編代碼中我們看到斷點在c
處卦羡,我們按住command
點擊鼠標噪馏,跳轉到下一層中:
我們看到這里調用的是swift_once
我們在swift_once
這行添加斷點,過掉上一個斷點绿饵,command
點擊鼠標欠肾,跳轉到下一層中:
此時我們看到了dispatch_once_f
,這里基本就能證明了builtin "once"
實際上還是使用了GCD
中的dispatch_once
來保證類型屬性只被初始化一次的拟赊。
其實我們來到Swift
源碼中刺桃,搜索一下swift_once
/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#if defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
通過對源碼的探索,我們也可以清楚的看到吸祟,swift_once
在apple
平臺調用的是dispatch_once
瑟慈。
4.2.5 get&set
下面我們在來看看類型屬性的get
和set
方法
get方法及分析:
// static Teacher.age.getter
sil hidden [transparent] @static main.Teacher.age.getter : Swift.Int : $@convention(method) (@thick Teacher.Type) -> Int {
// %0 "self" // user: %1
bb0(%0 : $@thick Teacher.Type):
debug_value %0 : $@thick Teacher.Type, let, name "self", argno 1 // id: %1
// function_ref Teacher.age.unsafeMutableAddressor
// 將獲取地址的函數(shù)存儲到%2
%2 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %3
// 調用函數(shù)并將返回結果存儲到%3
%3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
// 指針轉地址存放到%4
%4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*Int // user: %5
// 開始內存訪問
%5 = begin_access [read] [dynamic] %4 : $*Int // users: %6, %7
// 加載內存地址中的數(shù)據(jù)存儲到%6
%6 = load %5 : $*Int // user: %8
// 結束內存訪問
end_access %5 : $*Int // id: %7
// 返回取到的數(shù)據(jù)
return %6 : $Int // id: %8
} // end sil function 'static main.Teacher.age.getter : Swift.Int'
set方法及分析:
// static Teacher.age.setter
sil hidden [transparent] @static main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @thick Teacher.Type) -> () {
// %0 "value" // users: %8, %2
// %1 "self" // user: %3
bb0(%0 : $Int, %1 : $@thick Teacher.Type):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $@thick Teacher.Type, let, name "self", argno 2 // id: %3
// function_ref Teacher.age.unsafeMutableAddressor
// 獲取age的地址的函數(shù)存放到%4
%4 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %5
// 調用函數(shù)并將返回值存放到%5
%5 = apply %4() : $@convention(thin) () -> Builtin.RawPointer // user: %6
// 指針轉地址存儲到%6
%6 = pointer_to_address %5 : $Builtin.RawPointer to [strict] $*Int // user: %7
// 開始方法指針
%7 = begin_access [modify] [dynamic] %6 : $*Int // users: %8, %9
// 將傳入的參數(shù)%0 存儲到%7
store %0 to %7 : $*Int // id: %8
// 結束內存訪問
end_access %7 : $*Int // id: %9
%10 = tuple () // user: %11
return %10 : $() // id: %11
} // end sil function 'static main.Teacher.age.setter : Swift.Int'
4.3 類型屬性的內存占用
通過上面的分析桃移,我們可以知道類型屬性是一個全局變量,那么類型屬性是不是不會占用類的存儲空間呢葛碧?下面我們打印來看看:
通過打印我們可以知道借杰,類型屬性并不會占用類的空間。
4.4 單例的創(chuàng)建
回想一下吹埠,在Objective-C
我們會使用如下的方式創(chuàng)建一個單例:
@implementation SITeacher
+ (instancetype)shareInstance{
static SITeacher *shareInstance = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[SITeacher alloc] init];
});
return shareInstance;
}
@end
既然類型存儲屬性是個全局變量第步,那么我們在Swift
中創(chuàng)建一個單例就簡單多了,代碼如下:
class Teacher {
// 聲明一個不可變的 let 類型 static 屬性
// 為當前類的實例對象
static let shared = Teacher()
// 將當前類的 init 方法 添加private訪問權限
private init(){}
}
其實經(jīng)常寫Swift
代碼的人都知道Swift
中單例是怎么寫的缘琅,但是其原理是什么并不是很多人都知道的粘都,其實就是類型屬性是個全局變量,而且只初始化一次刷袍,符合單例的條件翩隧。
4.5 小結
通過上面的分析,我們總結如下:
- 類型屬性通過
static
來修飾 - 類型屬性需要附一個默認的初始值
- 類型屬性只會被初始化一次
- 類型屬性是線程安全的
- 類型屬性存儲為全局變量
- 類型屬性不占用對象的存儲空間
- 類型屬性可以用作單例來使用呻纹,前提是需要
private
類的init
方法堆生。
5. 屬性觀察者
介紹了完了Swift
中的幾種屬性后,下面我們來看看在屬性中使用頻率最高的屬性觀察者雷酪。屬性觀察者在代碼中實際就是willSet
和didSet
class Teacher {
var age:Int = 18 {
//賦新值之前調用
willSet {
print("willSet newValue --- \(newValue)")
}
// 賦新值之后調用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
}
let t = Teacher()
t.age = 20
print("end")
打印結果:
通過打印結果我們可以看到淑仆,在賦新值前我們可以拿到即將要賦予的新值,在賦新值后也能夠拿到賦值前的舊值哥力。
5.2 通過sil探索屬性觀察者
sil
編譯命令詳見3.2
5.2.1 類
class Teacher {
@_hasStorage @_hasInitialValue var age: Int { get set }
@objc deinit
init()
}
在類中相比于普通的存儲屬性沒有什么區(qū)別蔗怠。
5.2.2 set 方法
其實屬性觀察主要是在設置新值的時候觸發(fā)的,這里我們直接來看set
方法吩跋。
set方法及分析:
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %13, %10, %2
// %1 "self" // users: %16, %11, %10, %4, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 找出實例內部物理實例變量的地址
%4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
// 開始訪問內存
%5 = begin_access [read] [dynamic] %4 : $*Int // users: %6, %7
// 將內存中的值加載到%6
%6 = load %5 : $*Int // users: %8, %16
// 結束內存訪問
end_access %5 : $*Int // id: %7
debug_value %6 : $Int, let, name "tmp" // id: %8
// function_ref Teacher.age.willset
// 獲取 willset 的函數(shù)地址
%9 = function_ref @main.Teacher.age.willset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %10
// 調用 willset 函數(shù)寞射,傳入%0---新值,%1---self
%10 = apply %9(%0, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
// 找出實例內部物理實例變量的地址
%11 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %12
// 開始訪問內存
%12 = begin_access [modify] [dynamic] %11 : $*Int // users: %13, %14
// 存儲新值
store %0 to %12 : $*Int // id: %13
// 結束內存訪問
end_access %12 : $*Int // id: %14
// function_ref Teacher.age.didset
// 獲取 didset 的函數(shù)地址
%15 = function_ref @main.Teacher.age.didset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %16
// 調用 didset 傳入?yún)?shù) %6---舊值锌钮,%1---self
%16 = apply %15(%6, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
%17 = tuple () // user: %18
return %17 : $() // id: %18
} // end sil function 'main.Teacher.age.setter : Swift.Int'
從set
方法中我們可以看到桥温,使用了屬性觀察者的屬性,在set
方法中:
- 會將屬性中的舊值取出梁丘,存起來
- 在設置新值前會調用
willSet
函數(shù)侵浸,并將新值作為參數(shù)傳入 - 在設置新值后會調用
didSet
函數(shù),并將舊值作為參數(shù)傳入
5.2.3 willSet&didSet
在 willSet
函數(shù)中我們可以看到兰吟,其內部聲明了newValue
變量通惫,所以我們可以直接使用
在 didSet
函數(shù)中我們同樣也看到了oldValue
變量的聲明,同樣也為我們直接使用提供了變量混蔼。
5.3 屬性觀察者的使用
5.3.1 init方法中會觸發(fā)屬性觀察者嗎
class Teacher {
var age: Int = 18 {
//賦新值之前調用
willSet {
print("willSet newValue --- \(newValue)")
}
// 賦新值之后調用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
init() {
age = 20
}
}
print("end")
運行結果:
我們可以看到在init
方法中給屬性賦值,并沒有觸發(fā)屬性觀察者珊燎,那是為什么呢惭嚣?
init
方法會初始化當前變量遵湖,對于存儲屬性,在分配內存后晚吞,會調用memset
清理內存延旧,因為可能存在臟數(shù)據(jù),清理完才會賦值槽地。
還有一種原因就是迁沫,我們如果有多個屬性,我們給A屬性賦值捌蚊,并在A屬性的觀察者中訪問了B屬性集畅,此時B屬性可能還沒有被初始化,此時我們就有可能獲取到臟數(shù)據(jù)缅糟,導致結果不準確挺智。
(PS:)仔細想想也不是很嚴謹,所以通過sil
代碼來看看窗宦,調用流程如下:
@main.Teacher.__allocating_init()
——>@main.Teacher.init()
// Teacher.init()
sil hidden @main.Teacher.init() -> main.Teacher : $@convention(method) (@owned Teacher) -> @owned Teacher {
// %0 "self" // users: %14, %10, %4, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %6
%4 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %5
%5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
store %3 to %5 : $*Int // id: %6
end_access %5 : $*Int // id: %7
%8 = integer_literal $Builtin.Int64, 20 // user: %9
%9 = struct $Int (%8 : $Builtin.Int64) // user: %12
%10 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %11
%11 = begin_access [modify] [dynamic] %10 : $*Int // users: %12, %13
store %9 to %11 : $*Int // id: %12
end_access %11 : $*Int // id: %13
return %0 : $Teacher // id: %14
} // end sil function 'main.Teacher.init() -> main.Teacher'
在init
方法中赦颇,我們可以看到,屬性的賦值都是操作的屬性的內存地址赴涵,并沒有調用willSet
和didSet
相關方法媒怯。但是賦值也是在屬性初始化之后,所以對于memset
應該是站不住腳的髓窜,對于屬性觀察者中調用其他存儲屬性按道理也是站不住腳的扇苞。
下面我們換一下測試代碼:
class Teacher {
var age2: Int {
get{
return age
}
set{
self.age = newValue
}
}
var age: Int = 18 {
//賦新值之前調用
willSet {
print("willSet newValue --- \(newValue)")
}
// 賦新值之后調用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
init() {
age2 = 20
}
}
打印結果:
此時我們發(fā)現(xiàn)在計算屬性中給存儲屬性賦值就會調用屬性觀察者,下面我們通過sil
代碼看看調用纱烘。
init 函數(shù):
我們可以看到在init
函數(shù)中對于計算屬性是直接調用了其setter
方法杨拐。并且是在存儲屬性age
初始化完畢后才調用的計算屬性的setter
。
我們可以看到在計算屬性的的setter
方法中給存儲屬性賦值是調用的存儲屬性的setter
方法擂啥,在上面的分析中我們已經(jīng)知道了哄陶,在存儲屬性的setter
方法中會調用willSet
和didSet
。
綜上所述哺壶,在init
方法中給存儲屬性賦值是否觸發(fā)屬性觀察者應該是這樣的:
- 賦值前后自己是能知道的屋吨,沒必要在調用屬性觀察者
- 在計算屬性中給存儲屬性賦值,是因為計算屬性的代碼一般是固定的山宾,并且這個值有可能計算后的至扰,所以需要觸發(fā)屬性觀察者,使其做后續(xù)處理
5.3.2 在那些地方可以添加屬性觀察者
- 屬性觀察者應用在存儲屬性上
- 計算屬性自己實現(xiàn)了
get
和set
资锰,此時要想做些什么事情敢课,可以隨便去做,不需要屬性觀察者 - 但是繼承的計算屬性也是可以添加屬性觀察者的
應用在存儲屬性上在上面一直在使用,這里就不驗證了
對于計算屬性直秆,驗證結果如下:
我們可以看到willSet
和didSet
不能和getter
一起提供濒募,那么我們注釋掉getter
呢?
此時我們看到有setter
的變量也必須有getter
所以屬性觀察者不能添加到計算屬性中
下面我們來驗證在繼承的計算屬性中的屬性觀察者:
我們看到對于繼承計算屬性的同樣可以添加屬性觀察者圾结,并在賦值的時候調用瑰剃。
5.3.3 屬性觀察者的調用順序
如果是繼承存儲屬性,并且父類和子類都添加了屬性觀察者筝野,那么這個調用順序是什么呢晌姚?,驗證如下:
我們看到調用順序是:
- 先調用子類的
willSet
歇竟,再調用父類的willSet
- 先調用父類的
didSet
挥唠,在調用子類的didSet
其實也很好理解,首先通知子類要改變途蒋,畢竟是給子類賦值猛遍,然后在通知父類改變,父類改變完了先在父類做些處理号坡,然后在讓子類改變懊烤,子類在做最后的處理。
5.3.4 在子類的init方法中給屬性賦值
如果在子類的init方法中給屬性賦值會怎樣調用屬性觀察者呢宽堆?測試結果如下:
我們可以看到在子類的init
方法中給屬性賦值與直接給子類對象賦值的調用屬性是一模模一樣樣的腌紧。我的理解是這樣的:
- 在子類的
init
方法中賦值,父類是監(jiān)聽不到畜隶,所以需要觸發(fā)父類的屬性觀察者壁肋,使其作出相應處理 - 當父類做完處理子類也是監(jiān)聽不到的,所以同意需要觸發(fā)子類屬性觀察者通知子類作出相應處理
6. Then
在Swift
中對于屬性也會經(jīng)常用到一個叫Then
的框架:
其用法如下:
6.1 Then
import Then
let label = UILabel().then {
$0.textAlignment = .center
$0.textColor = .black
$0.text = "Hello, World!"
}
以上等效于
let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.text = "Hello, World!"
return label
}()
6.2 with
let newFrame = oldFrame.with {
$0.size.width = 200
$0.size.height = 100
}
newFrame.width // 200
newFrame.height // 100
6.3 do
UserDefaults.standard.do {
$0.set("devxoul", forKey: "username")
$0.set("devxoul@gmail.com", forKey: "email")
$0.synchronize()
}
其原理其實也蠻簡單的籽慢,感興趣的可以去看看浸遗。
7. 總結
至此我們對Swift
中的屬性基本就分析完畢了先在總結如下:
存儲屬性:
- 存儲屬性是占用實例存儲空間的
- 在
sil
代碼中使用``@_hasStorage關鍵字
修飾 - 再其
get
和set
方法中通過訪問內存獲取和修改屬性值
計算屬性:
- 計算屬性的本質就是
get
和set
方法 - 計算屬性不占用實例對象的內存空間
延遲存儲屬性:
- 延遲存儲屬性也是存儲屬性
- 延遲存儲屬性需要使用
lazy
修飾 - 延遲存儲屬性必須有一個默認的初始值
- 延遲存儲屬性在第一次訪問的時候才能被賦值
- 延遲存儲屬性并不保證線程安全
- 延遲存儲屬性對實例對象的大小有影響,一般會增加內存占用
類型屬性:
- 類型屬性通過
static
來修飾 - 類型屬性需要賦一個默認的初始值
- 類型屬性只會被初始化一次
- 類型屬性是線程安全的
- 類型屬性存儲為全局變量
- 類型屬性不占用對象的存儲空間
- 類型屬性可以用作單例來使用箱亿,前提是需要
private
類的init
方法跛锌。
屬性觀察者:
- 屬性觀察者是觀察屬性改變前后的,本質就是
willSet
和didSet
- 屬性觀察者可以添加在
- 存儲屬性
- 繼承的存儲屬性
- 繼承的計算屬性
- 如果并沒有在屬性中添加屬性觀察者届惋,則不會調用屬性觀察者
- 在底層代碼中會根據(jù)開發(fā)者對屬性觀察者的依賴調用屬性觀察者