iOS老司機萬字整理, 可能是最全的Swift Tips
我正在參加「掘金·啟航計劃」
可能是最全的Swift Tips
1. 關於Swift
1.1 Swift的優點
- Swft更加安全, 它是型別安全的語言.
- Swift容易閱讀, 語法和檔案結構簡易化.
- Swift易於維護, 檔案分離後結構更清晰.
- Swift程式碼更少, 簡潔的語法, 可以省去大量冗餘程式碼.
- Swift速度更快, 運算效能更高.
1.2 Swift和OC如何相互呼叫?
- Swift呼叫OC程式碼, 需要建立一個
Target-Bridging-Header.h
的橋接檔案, 在橋接檔案匯入需要呼叫的OC程式碼標頭檔案即可. - OC呼叫Swift程式碼, 直接匯入
專案名-Swift.h
檔案即可, Swift如果需要被OC呼叫, 需要使用@objc
對方法或者屬性進行修飾.
1.3 Swift是面向物件程式設計(Object Oriented Programing)還是函數語言程式設計(Functional programming)?
- Swift是一種混合程式語言, 它包含著兩種程式設計模式.
- 它實現了面向物件的三個基本原則: 封裝、繼承、多型.
- 函數語言程式設計語言是指: 它是一種程式設計正規化, 它將電腦運算視為函式計算, 並且避免使用程式狀態以及易變物件. 很難說Swift是一個成熟的函數語言程式設計語言, 但是它已經具備了函數語言程式設計語言的基礎.
2. 基操知識點
2.1 Swift中struct和class的區別, struct能繼承嗎(不能)
- 在Swift中, class是引用型別(指標型別), struct是值型別.
值型別
- 值型別在傳遞和賦值時將進行復制; 賦值給var、let或者給函式傳參, 是直接將所有內容拷貝一份, 類似於對檔案進行copy、paste操作, 產生了全新的檔案副本. 屬於深拷貝.
- 值型別: 比如結構體, 列舉, 是在棧空間上儲存和操作的.
引用型別
- 引用型別只會使用引用物件的一個"指向"; 賦值給var、let或者給函式傳參, 是將記憶體地址拷貝一份, 類似於製作一個檔案的替身(快捷方式、連結), 指向的是同一個檔案. 屬於淺拷貝.
- 引用型別: 比如Class, 是在堆空間上儲存和操作的.
class和struct比較, 優缺點
class有以下功能, struct是沒有的: 1. class可以繼承, 子類可以使用父類的特性和方法 2. 型別轉換可以在執行時檢查和解釋一個例項物件 3. class可以用deinit來釋放資源 4. 一個類可以被多次引用
- 類中的每一個成員變數都必須被初始化, 否則編譯器會報錯, 而結構體不寫.., 編譯器會自動幫我們生成init函式, 給一個變數賦一個預設值
struct優勢: 1. 結構較小, 適用於複製操作, 相比較一個class例項被多次引用, struct更安全 2. 無須擔心記憶體洩漏問題
2.1.1 在Swift中, 什麼時候用struct
, 什麼時候用class
?
- 函數語言程式設計傾向於struct, 面向物件程式設計更傾向於class. 在Swift中, 類和結構體有許多不同的特性如下:
- 類支援繼承, 結構體不支援.
- 類是引用型別, 結構體是值型別.
- 沒有通用的規則決定結構體和類哪一個更好用, 一般的建議是使用最小的工具來完成你的目標.
- 但是有一個好的經驗是多使用struct, 除非你用了繼承和引用語義.
- 在執行時, 結構體在效能方面更優於類, 原因是結構體的方法呼叫是靜態繫結的, 而類的方法呼叫是動態實現的. 這就是儘可能使用結構體代替類的一個重要原因之一.
2.1.2 Swift為什麼將String、Array、Dictionary設計為值型別?
- 值型別和引用型別相比, 最大的優勢是可以高效的使用記憶體.
- 值型別在棧上操作, 引用型別在堆上操作, 棧上操作僅僅是單個指標的移動.
- 堆上操作涉及到記憶體的合併、位移、重連結.
- Swfit這樣設計減少了堆上記憶體分配和回收次數, 使用寫時複製(Copy-On-Write)將值傳遞與複製開銷降到最低.
2.2 Swift中Class的內部實現和記憶體管理
2.3 檔案訪問許可權關鍵字 private public
2.3.1 訪問級別
open
- open的許可權是最大的, 可以在允許的實體模組、其它模組中訪問, 並且允許其它模組進行繼承和重寫.
-
例如: TargetA中有classA, 許可權是open, TargetB中的classB即可以繼承classA, classA的方法, 成員變數等也可以被訪問.
-
public
- public和open是差不多的, 也是允許在實體模組, 其它模組中訪問, 有一點區別是, 並不允許其它模組進行繼承和重寫.
-
例如: TargetA中有classA, 方法是testA, 許可權是public, TargetB中有classB, 那麼在classB中testB方法, 就可以初始化var a = classA(), 並且呼叫a.testA.
-
internal
-
internal只允許在定義的實體模組進行訪問, 不允許在其它模組中訪問. 這個也是很多實體預設的許可權.
-
fileprivate
- fileprivate翻譯過來就是檔案私隱, 它只允許在定義的檔案中訪問.
-
例如: 在一個Target中, 有classA和classB兩個類分別在兩個檔案, classA當前許可權是fileprivate, 那麼classB是不能訪問classA的. 如果classA和classB是在同一個檔案下, 就可以訪問.
-
private
- private只允許在當前定義實體中訪問.
- 例如: classA和classB都在同一個檔案, classA的許可權是private, 那麼classB原則上是不能訪問classA的. 要訪問的話, 需要一些情況.
2.3.2 訪問級別的使用準則
- 一個實體不可以被更低的訪問級別的實體定義.
- 變數型別的訪問級別 >= 變數的訪問級別
- 例如: 定義一個類
fileprivate class ClassA{}
, 如果定義為internal var classA: ClassA
就會報錯, 許可權ClassA的試題型別需要大於變數classA.
- 引數型別, 返回值型別 >= 函式
- 例如:
func testA(_ num: Int) -> Double{}
, 函式的訪問級別預設是internal
, 引數的num是public
, 返回值Double也是public
- 父類 >= 子類
- 相當於說我能訪問子類, 那麼父類也應該要可以訪問才對.
- 例如:
class SupClassA{}
, 子類class ClassA: SupClassA{}
, 父類的預設全顯示internal
, 那麼子類就不能為public
和open
2.3.3 成員巢狀型別
- 型別為
private, fileprivate
- 當前型別為
private, fileprivate
, 那麼成員的預設型別也是private
或fileprivate
- 例如:
fileprivate class ClassA { var a = 0, var b = 0}
, a和b預設都是fileprivate
- 型別為
internal, public
- 當型別為
internal, public
, 成員的預設型別為internalpublic class ClassA { internal var a = 0 }
2.3.4 直接在全域性作用域下定義的private
等價於fileprivate
- 可以編譯通過
- 不可以編譯通過
3.4 getter, setter許可權
- 對於讀寫方面, 很多時候我們希望別人讀我們的值, 而不允許修改我們的值, 我們可以這麼定義如下:
class ClassA { private(set) var age: Int = 0 }
2.4 Swift的module的預設訪問許可權, module內部的訪問許可權
2.5 寫時複製機制, OC中類似的機制是什麼?
值型別(比如struct), 在複製時, 複製物件與原物件實際上在記憶體中指向同一個物件, 當且僅當修改複製的物件時, 才會在記憶體中建立一個新的物件. 1. 為了提升效能, 值型別:struct, enum, Int, Double, Float, String, Array, Dictionary、Set採取了Copy On Write的技術 2. 比如僅當有"寫"操作時, 才會真正執行拷貝操作 3. 對於標準庫值型別的賦值操作, Swift能確保最佳效能, 所以沒必要為了保證效能來避免賦值 ``` let array = [1, 2, 3] var array1 = array
// 斷點1, 此時array和array2記憶體地址一致
array1 = array
// 斷點2, 此時array和array2記憶體地址不一致
```
- 寫時複製允許共享同一個記憶體地址, 直到其中之一發生改變. 這樣的設計使得值型別可以被多次複製而無需消耗多餘的記憶體, 只有在變化的時候才會增加開銷, 隱藏記憶體的使用更加高效.
- 在OC語言中, 想要獲取多個完全一致、互不干擾的物件, 可以使用mutableCopy.
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1, @2, @3, nil]; NSMutableArray *array1 = [array mutableCopy];
2.6 什麼是optional
型別, 它是用來解決什麼問題的?
optional
型別被用來表示任何型別的變數的缺少值. 在OC中, 引用型別的變數是可以缺失值, 並且使用nil
作為缺少值. 基本資料型別沒有這種功能.- Swift用
optional
擴充套件了在基本資料型別和引用型別中缺少值的概念, 一個optional
型別的變數, 在任何時候都可以儲存一個值或者為nil
.
2.7 什麼是泛型? 泛型是用來解決什麼問題的?
- 泛型是讓你能根據自定義的需求, 編寫出適用於任意型別的、靈活可複用的函式及型別. 你可以避免編寫重複的程式碼, 而是用一種清晰抽象的方式來表達程式碼的意圖.
2.7.1 Swift中泛型的高階使用
- Swift包含泛型類和泛型結構體, 泛型可以在類、結構體、列舉、全域性函式或者方法中使用.
- 泛型協議是通過
typealias
部分實現的,typealias
不是一個泛型型別, 他只是一個佔位符的名字. 它通常是作為關聯型別被引用, 只有協議被一個型別引用的時候它才被定義.
2.8 哪些情況下使用隱式解包?
- 對
optional
變數使用隱式解包最常見的原因如下: - 物件的屬性在初始化的時候不能為
nil
, 否則不能初始化成功. 典型的例子是Interface Builder outlet
型別的屬性, 它總是在它的擁有者初始化之後再初始化. 在這種特定的情況下, 假設他在Interface Builder
中被正確的配置--outlet
被使用之前, 保證它不為nil
. - 解決強引用的迴圈問題, 當兩個例項物件相互引用, 並且對引用的例項物件的值要求不能為
nil
時候. 在這種情況下, 引用的一方可以標記為unowned
, 另一方使用隱式解包. - 除非必要, 否則儘量不要對
optional
型別使用隱式解包. 使用不當會增加執行時crash的可能性.在某些情況下, crash可能是有意的行為, 但這種情況更推薦fatalError()
函式.
2.8.1 對一個optional
變數解包有哪些方法?
- 強制解包,
!
操作符, 不安全,容易引起執行時崩潰. - 隱式解包, 在變數宣告時, 大多數情況也不安全, 也有可能引起執行時崩潰.
- 可選繫結
if let
和guard let
. - 自判斷連線
optional chaining
. - 合併空值運算子
??
. guard
語句.- 可選模式
optional pattern
.
2.9 Swift中的常量定義和OC的區別
``` // 在OC中可以這樣定義常量: const int number = 0;
// 類似的Swift是這樣定義的:
let number = 0
``
-
const常量是一個在編譯時或者編譯解析時被初始化的變數.
- 通過
let建立的是一個執行時常量, 是不可變的. 它可以使用
static或者
dynamic`關鍵字類初始化. 它的值只能被分配一次.
2.10 Swift中的static
或者class
修飾符的作用
- 宣告一個靜態屬性或者函式, 我們常常使用值型別的
static
修飾符. 下面就是一個結構體的例子:struct Sun { static fun illuminate() {} }
- 對類來說, 使用
static
或者class
修飾符, 都是可以的. 他們使用後的效果是一樣的, 但是本質上是不同的. - 本質不同原因是:
static
修飾的屬性或者修飾的函式都不可以重寫. 但是使用class
修飾符, 你可以重寫屬性或者函式. - 當
static
在類中應用的時候,static
就成為class final
的一個別名. - 例如下面程式碼中, 當你嘗試重寫
illuminate()
函式的時候, 編譯器就會報錯:
``` class Star { class func spin() {} static func illuminate() {} }
class Sun : Star { override class func spin() { super.spin() } override static func illuminate() { // error: Cannot override static method super.illuminate() } } ```
- 在
sil
程式碼中可以看到class
修飾的類方法儲存在VTable
中, static
修飾的類方法是以靜態方法的形式儲存的.
2.11 Swift中能通過extension
儲存一個屬性嗎?
- 不能.
extension
可以給當前型別新增新的行為, 但不能改變本身的型別或者本身的介面. - 如果你新增一個新的可儲存的屬性, 你需要額外的記憶體來儲存新的值. 擴充套件不能實現這樣的任務.
2.12 閉包是引用型別嗎?
- 閉包是一個引用型別.
- 閉包捕獲值的本質是在堆區開闢記憶體, 然後儲存其在上下文中捕獲到的值.
- 修改值也是修改的堆空間的值.
- 閉包的底層結構是一個結構體. 首先儲存閉包的地址; 加上捕獲值的地址.
- 在捕獲的值中, 會對定義的變數和函式中的引數分開儲存.
- 儲存的時候內部會有一個
HeapObject
結構, 用於管理記憶體、引用計數 - 函式是特殊的閉包, 只不過函式不捕獲值, 所以在閉包結構體中只儲存函式地址, 不儲存指向捕獲值的指標.
2.13 如何把一個負整數轉換成一個無符號的整數?
UInt
型別是用來儲存無符號整型的. 下面的程式碼實現了一個有符號整型轉換的初始化方法:let myNegative = UInt(-1)
- 我們知道負數的內部結構是使用二進位制補碼的正數, 在保持這個負數記憶體地址不變的情況下, 如何把一個負整數轉換成一個無符號的整數?
- 原碼: 原碼就是符號位加上真值的絕對值, 即用第一個二進位制位表示符號(正數該位為0, 負數該位為1), 其餘位表示值.
- 反碼: 正數的反碼與其原碼相同; 負數的反碼是對其原碼逐位取反, 但符號位除外.
- 補碼: 正數的補碼就是其本身; 負數的補碼是在其反碼的基礎上+1
2.14 描述一種在Swift中出現迴圈引用的情況.
- 迴圈引用出現在兩個例項物件相互擁有強引用關係的時候, 這會造成記憶體洩漏, 原因是這兩個物件都不會被釋放, 只要一個物件被另一個物件強引用, 那麼該物件就不能被釋放, 由於強引用的存在, 每個物件都會保持對方存在.
- 解決辦法可以使用
weak
或者unowned
. - 轉換為值型別, 只有類會存在喧嚷引用, 如果能用把
class
換成struct
, 是可以避免迴圈引用的. delegate
使用weak
屬性.- 閉包中, 對有可能發生迴圈引用的物件, 使用
weak
或者unowned
修飾.
2.14.1 關鍵字strong、weak、unowned
的區別?
- Swfit的記憶體管理機制同OC一致, 都是ARC,
strong
和weak
同OC一樣. unowned
(無主引用), 不會產生強引用, 例項銷燬後仍然儲存著例項的記憶體地址(類似於OC中的unsafe_unretained
), 它仍然會保持對被已經釋放了的物件的一個"無效的"引用, 它不是Optional
, 也不會被指向nil
, 如果試圖在例項銷燬後訪問無主引用unowned
, 會產生執行時錯誤(懸垂指標).weak
, 當我們賦值給一個被標記為weak
的變數時, 它的引用計數不會被改變. 而且當這個弱引用變數所引用的物件被釋放時, 這個變數將被自動設為nil
. 這也是弱引用必須被宣告為Optional
的原因.- 在引用物件的生命週期內, 如果它可能為nil, 那麼就用
weak
引用. 反之, 當你知道引用物件在初始化後永遠都不會為nil, 就用unowned
. - 如果你知道你引用的物件會在正確的時機釋放掉, 且它們是相互依存的, 而你不想寫一些多餘的程式碼來情況你的引用指標, 那麼你就應該使用
unowned
引用而不是weak
引用.
``` class SwiftViewControllerA: UIViewController {
var person : Person?
override func viewDidLoad() { super.viewDidLoad()
person = Person()
person?.testClosure()
person = nil } }
// 測試unowned和weak
class SomeSigleton {
static let share = SomeSigleton()
func closure(closure: (() -> Void)?) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) { closure?() }
}
}
class Person {
let someSigleton = SomeSigleton.share let portrait = UIImage()
func testClosure() {
someSigleton.closure { [unowned self] in print(self.portrait) }
// 使用weak修飾就不會有問題!
// someSigleton.closure { [weak self] in
// print(self?.portrait) // }
}
deinit { print("Person is deinited") }
} ```
- Apple文件使用建議 ``` Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.
Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.
``
- 當我們知道兩個物件的生命週期並不相關, 那麼我們必須使用
weak. 相反, 非強引用物件擁有和強引用物件同樣或者更長的宣告週期的話, 則應該使用
unowned.
- 例如,
ViewController對它的
SubView的引用可以使用
unowned. 因為
ViewController的生命週期一定比它的
SubView長. 而在使用服務時, 則需要看情況使用
weak. 因為服務的初始化方法可能是被工廠模式或
Service Locator`所封裝. 這些服務可能在某些時候被重構為單例, 此時它們的生命週期發生了改變.
2.15 什麼關鍵字可以實現遞迴列舉?
indirect
``` enum List{ case end indirect case node(T, next: List ) }
indirect enum List
2.16 什麼是屬性觀察?
- 屬性觀察是指在當前型別內對特性屬性進行監測, 並做出相應. 屬性觀察是Swift中的特性, 具有兩種方法,
willset
和didset
var title: String { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, title) } }
willSet
會傳遞新值, 預設叫newValue
didSet
會傳遞舊值, 預設叫oldVlaue
- 在初始化器中設定屬性不會觸發willSet和didSet
2.17 比較Swift和OC中的初始化方法init
有什麼不同?
- Swift的初始化方法, 更加嚴格和準確, Swift初始化方法需要保證所有的
非Optional
的成員變數都完成初始化, 同時Swift新增了convenience
和required
兩個修飾初始化器的關鍵字. convenience
只提供了一種便捷的初始化器, 必須通過一個指定初始化器來完成初始化.required
是強制子類重寫父類中所修飾的初始化方法.
2.18比較Swift和OC中的protocol
有什麼異同?
- 相同點: 兩者都可以被用作代理.
- 不同點: Swift中的
protocol
還可以對介面進行抽象, 可以實現面向協議程式設計, 從而大大提高程式設計效率; Swift中的protocol
可以用於值型別、結構體、列舉.
2.18.1 如何將Swift中協議protocol
中的部分方法設計為可選Optional
?
- 在協議和方法前面新增
@objc
, 然後在方法前面新增optional
關鍵字, 該方式實際上是將協議轉為了OC的方式.@objc protocol someProtocol { @objc optional func testProtocol() }
- 使用擴充套件
extension
, 來規定可選方法, 在Swfit中, 協議擴充套件可以定義部分方法的預設實現 ``` protocol someProtocol { func test() }
extension someProtocol { func test() { print("test") } } ```
2.19 Swift和OC中的自省方法有什麼區別?
- OC中的自省方法就是判斷某一個物件是否屬於某一個類的操作, 有以下2種方式 ``` // 判斷obj是否是某個類 [obj isKindOfClass:[SomeClass class]];
// 判斷obj是否是某個類或者是該類的子類
[obj isMemberOfClass:[SomeClass class]];
``
- 在Swift中由於很多
class並非繼承自
NSObject, 故而Swift使用
is來判斷是否屬於某一型別,
is不僅可以作用於
class, 還能作用於
enum和
struct`.
2.20 什麼是函式過載? Swift支援函式過載嗎?
- 函式過載: 函式名相同, 函式的引數個數不同, 或者引數型別不同, 或引數標籤不同, 返回值型別與函式過載無關.
- Swift支援函式過載.
2.21 Swift中列舉的關聯值*和原始值**的區分?
- 關聯值: 有時會將列舉的成員值跟其他型別的變數關聯儲存在一起, 會非常有用.
// 關聯值 enum Date { case digit(year: Int, month: Int, day: Int) case string(String) }
- 原始值: 列舉成員可以使用相同型別的預設值預先關聯, 這個預設值叫做: 原始值.
// 原始值 enum Grade: String { case perfect = "A" case great = "B" case good = "C" case bad = "D" }
2.22 Swift中的閉包Closure相關
2.22.1 Swift中的閉包結構是什麼樣的?
{
(引數列表) -> 返回值型別 in 函式體程式碼
}
2.22.2 什麼是尾隨閉包?
- 將一個很長的閉包表示式作為函式的最後一個實參.
- 使用尾隨閉包可以增強函式的可讀性.
- 尾隨閉包是一個被書寫在函式呼叫括號外面(後面)的閉包表示式. ``` // fn就是一個尾隨閉包引數 func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) { print(fn(v1, v2)) }
// 呼叫 exec(v1: 10, v2: 20) { $0 + $1 } ```
2.22.3 什麼是逃逸閉包?
- 當閉包作為一個實際引數傳遞給一個函式或者變數的時候, 我們就說這個閉包逃逸了, 可以在形式引數前寫
@escaping
來明確閉包是允許逃逸的. - 非逃逸閉包、逃逸閉包, 一般都是當做引數傳遞給函式.
- 非逃逸閉包: 閉包呼叫發生在函式結束前, 閉包呼叫在函式作用域內.
- 逃逸閉包: 閉包有可能在函式結束後呼叫, 閉包呼叫逃逸出了函式的作用域, 需要通過
@escaping
宣告. ``` // 定義一個數組用於儲存閉包型別 var completionHandlers: [() -> Void] = []
// 在方法中將閉包當做實際引數, 儲存到外部變數中
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
``
- **上面這種情況, 如果不標記函式的形式引數為
escaping`, 就會編譯報錯.**
2.22.4 什麼是自動閉包?
- 自動閉包是一種自動建立的用來把作為實際引數傳遞給函式的表示式打包的閉包.
- 它不接受任何實際引數, 並且當它被呼叫時, 它會返回內部打包的表示式的值.
- 這個語法的好處在於通過寫普通表示式代替顯式閉包而使你省略包圍函式形式引數的括號.
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } getFirstPositive(10, 20)
- 為了避免與期望衝突, 使用了
@autoclosure
的地方最好明確註釋清楚: 這個值會被推遲執行. @autoclosure
會自動將20封裝成閉包{ 20 }
@autoclosure
只支援()->T
格式的引數@autoclosure
並非只支援最後一個引數- 有
@autoclosure
、無@autoclosure
, 構成了函式過載.
2.23 合併空值運算子 ??
a ?? b
- a 是可選項, b 可以是可選項也可以不是可選項, b跟a的儲存型別必須相同.
- 如果a不為
nil
, 就返回a. - 如果a為
nil
, 就返回b. 如果b不是可選項, 返回a時, 會對a進行自動解包操作. ``` let a: Int? = 1 let b: = 2
// 此時c為Int型, 不是可選型別. 而且值為1.
let c = a ?? b //
- 所以`??`返回的型別取決於b.
public func ??
public func ??
2.24 Swfit中, 儲存屬性和計算屬性的區別?
- Swfit中跟例項物件相關的屬性可以分為兩大類
- 儲存屬性(Stored Property)
- 類似於成員變數這個概念
- 儲存在例項物件的記憶體中
- 結構體、類可以定義儲存屬性
-
列舉不可以定義儲存屬性
-
計算屬性(Computed Property)
- 本質就是方法(函式)
- 不佔用例項物件的記憶體
-
列舉、結構體、類都可以定義計算屬性 ``` struct Circle { // 儲存屬性 var radius: Double
//計算屬性 var diameter: Double { set { radius = newValue / 2 } get { return radius * 2 } } } ```
2.24.1 什麼是延遲儲存屬性(Lazy Stored Property)?
- 使用
lazy
可以定義一個延遲儲存屬性, 在第一次用到屬性的時候才會進行初始化(類似OC中的懶載入). lazy
屬性必須是var
, 不能是let
(let
必須在例項物件的初始化方法完成之前就擁有值).- 如果多條執行緒同時第一次訪問
lazy
屬性, 無法保證屬性只被初始化一次.class PhotoView { // 延遲儲存屬性 lazy var image: Image = { let url = "http://www.baidu.com...png" let data Data(url: url) return Image(data: data) }() }
2.24.2 什麼是屬性觀察器?
-
可以為非
lazy
的var
儲存屬性設定屬性觀察器, 通過關鍵字willset
和didset
來監聽屬性變化. ``` struct Circle { var radius: Double { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, radius) } }init() { self.radius = 1.0 print("Circle init!") } } ```
2.24.3 Swift中什麼是型別屬性(Type Property)?
- 嚴格的說, 屬性可以分為
- 例項屬性(Instance Property): 只能通過例項物件去訪問.
-
- 儲存例項屬性(Stored Instance Property): 儲存在例項物件的記憶體中, 每個例項物件都有一份.
-
- 計算例項屬性(Computed Instance Property)
-
型別屬性(Type Property): 只能通過型別去訪問
-
- 儲存型別屬性(Stored Type Property): 整個程式執行過程中, 就只有一份記憶體(類似於全域性變數).
-
- 計算型別屬性(Computed Type Property)
- 可以通過
static
定義型別屬性p如果是類, 也可以用關鍵字class
.struct Car { static var count: Int = 0 init() { Car.count += 1 } }
- 不同於儲存例項屬性, 你必須給儲存型別屬性設定初始值.
-
- 因為型別沒有像例項物件那樣的
init
初始化器來初始化儲存屬性.
- 因為型別沒有像例項物件那樣的
- 儲存屬性預設就是
lazy
, 會在第一次使用的時候才初始化. -
- 就算被多個執行緒同時訪問, 保證只會初始化一次.
-
- 儲存型別屬性可以是
let
.
- 儲存型別屬性可以是
- 列舉型別也可以定義型別屬性(儲存型別屬性、計算型別屬性)
2.25 Swfit中如何使用單例模式?
-
可以通過
型別屬性 + let + private
來寫單例, 程式碼如下: ``` public class FileManager { public static let shared = { ... return FileManager() }private init() {} } ```
2.26 Swfit中的下標是什麼?
- 使用
subscript
可以給任意型別(列舉、結構體、類)增加下標功能, 有些地方也翻譯為: 下標指令碼. -
subscript
的語法類似於例項方法、計算屬性, 本質就是方法(函式). 使用如下: ``` class Point { var x = 0.0, y = 0.0subscript(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 ```
2.27 簡單說一下Swift中的初始化器
- 類、結構體、列舉都可以定義初始化器
- 類有兩種初始化器: 指定初始化器
designated initializer
、便捷初始化器convenience initializer
``` // 指定初始化器 init(parameters) { statements }
// 便捷初始化器 convenience init(parameters) { statements } ``` 規則: - 每一個類至少有一個指定初始化器, 指定初始化器是類的主要初始化器 - 預設初始化器總是類的指定初始化器 - 類偏向少量指定初始化器, 一個類通常只有一個指定初始化器
初始化器的相互呼叫規則 - 指定初始化器必須從它的直系父類呼叫指定初始化器 - 便捷初始化器必須從相同的類裡呼叫另一個初始化器 - 便捷初始化器最終必須呼叫一個指定初始化器
2.28 什麼是可選鏈?
- 可選鏈是一個呼叫和查詢可選型別、方法和下標的過程, 它可能為
nil
. - 如果可選項包含值, 那麼屬性、方法或者下標的呼叫成功;
- 如果可選項是
nil
, 屬性、方法或者下標的呼叫會返回nil
. - 多個查詢可以連結在一起, 如果鏈中任何一個節點是
nil
, 那麼整個鏈就會得體的失敗.
2.29 什麼是運算子過載(Operator Overload)?
-
類、結構體、列舉可以為現有的運算子提供自定義的實現, 這個操作叫: 運算子過載. ``` struct Point { var x: Int var y: Int
// 運算子過載 static func + (p1: Point, p2: Point) -> Point { return Point(x: p1.x + p2.x, y: p1.y + p2.y) } }
var p1 = Point(x: 10, y: 10) var p2 = Point(x: 20, y: 20) var p3 = p1 + p2 ```
3. OC和Swift執行時簡介
3.1 Objective-C執行時
- 動態型別 (dynamic typing)
- 動態繫結 (dynamic binding)
- 動態裝載 (dynamic loading)
3.1.1 OC物件呼叫方法的過程
[object methodA]
3.2 派發方式
3.2.1 直接派發 (Direct Dispatch)
- 直接派發是最快的, 不止是因為需要呼叫的指令集會更少, 並且編譯器還能夠有很大的優化空間, 例如函式內聯等, 直接派發也有人稱為靜態呼叫.
- 然而, 對於程式設計來說直接呼叫也是最大的侷限, 而且因為缺乏動態性所以沒辦法支援繼承和多型.
3.2.2 函式表派發 (Table Dispatch)
- 函式表派發是編譯型語言實現動態行為最常見的實現方式. 函式表使用了一個數組來儲存類宣告的每一個函式的指標. 大部分語言把這個稱為"Virtual table(虛擬函式表)", Swift裡稱為 "witness table". 每一個類都會維護一個函式表, 裡面記錄著類所有的函式, 如果父類函式被override的話, 表裡面只會儲存被override之後的函式. 一個子類新新增的函式, 都會被插入到這個陣列的最後. 執行時會根據這一個表去決定實際要被呼叫的函式.
- 查表是一種簡單、易實現, 而且效能可預知的方式. 然而, 這種派發方式比起直接派發還是慢一點. 從位元組碼角度來看, 多了兩次讀和一次跳轉, 由此帶來了效能的損耗. 另一個慢的原因在於編譯器可能會由於函式內執行(如果函式帶有副作用的話)的任務導致無法優化.
- 這種基於陣列的實現, 缺陷在於函式表無法拓展. 子類會在虛數函式表的最後插入新的函式, 沒有位置可以讓extension安全地插入函式.
3.2.3 訊息機制派發 (Message Dispatch)
- 訊息機制是呼叫函式最動態的方式. 也是Cocoa的基石, 這樣的機制催生了KVO、UIAppearence和CoreData等功能. 這種運作方式的關鍵在於開發者可以在執行時改變函式的行為. 不止可以通過swizzling來改變, 甚至可以用isa-swizzling修改物件的繼承關係, 可以在面向物件的基礎上實現自定義派發.
3.3 Swift執行時
- 純Swift類的函式呼叫已經不再是Objective-C的執行時發訊息, 而是型別C++的vtable, 在編譯時就確定了呼叫哪個函式, 所以沒法通過runtime獲取方法、屬性.
- 而Swift為了相容Objective-C, 凡是繼承自NSObject的類都會保留其動態性, 所以我們能通過runtime拿到他的方法. (老版本的Swift(如2.2)是編譯期隱式的自動幫你加上了
@objc
, 而Swift4.0以後編譯期去掉了隱式特性, 必須使用顯示新增.) - 不管是純Swift類還是繼承自NSObject的類, 只有在屬性和方法前添加了
@objc
關鍵字就可以使用runtime.
- 值型別總是會使用直接派發, 簡單易懂
- 協議和類的extension都會使用直接派發
- NSObject的extension會使用訊息機制進行派發
- NSObject宣告作用域裡的函式都會使用函式表進行派發
- 協議裡宣告的, 並且帶有預設實現的函式會使用函式表進行派發
3.3.1 Swift執行時-final @objc
- 可以在標記為
final
的同時, 也使用@objc
來讓函式可以使用訊息機制派發. 這麼做的結果就是, 呼叫函式的時候會使用直接派發, 但也會在Objective-C的執行時裡註冊相應的selector. 函式可以響應perform(selector:)
以及別的Objective-C特性, 但在直接呼叫時又可以有直接派發的效能.
發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~
ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~
- iOS老司機聊聊實際專案開發中的<<人月神話>>
- iOS老司機可落地在中大型iOS專案中的5大接地氣設計模式合集
- iOS老司機的跨端跨平臺Hybrid開發Tips
- iOS老司機的2022年回顧, 聊聊寒冬下的實用<<談判力>>
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips_橋接模式
- iOS老司機的多執行緒PThread學習分享
- iOS老司機整理, iOSer必會的經典演算法_2
- iOS老司機的<<藍海轉型>>讀書分享
- iOS老司機的<<程式設計師的自我修養:連結、裝載與庫>>讀書分享
- iOS老司機的接地氣演算法Tips
- iOS老司機的RunLoop原理探究及實用Tips
- iOS老司機整理, iOSer必會的經典演算法_1
- iOS老司機的App啟動優化Tips, 讓啟動速度提升10%
- iOS老司機的網路相關Tips
- 戀上資料結構與演算法
- iOS老司機帶你一起把App的崩潰率降到0.1%以下
- 探究Swift的String底層實現
- iOS老司機萬字整理, 可能是最全的Swift Tips
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips
- 聊一聊Swift中的閉包