前言
京喜APP
最早在2019年引入了Swift
,使用Swift
完成了第一個訂單模組的開發。之後一年多我們持續在團隊/公司內部推廣和普及Swift
,目前Swift
已經支撐了70%+
以上的業務。透過使用Swift
提高了團隊內同學的開發效率,同時也帶來了質量的提升,目前來自Swift
的Crash的佔比不到1%
。在這過程中不斷的學習/實踐,團隊內的Code Review
,也對如何使用Swift
來提高程式碼質量有更深的理解。
Swift特性
在討論如何使用Swift
提高程式碼質量之前,我們先來看看Swift
本身相比ObjC
或其他程式語言有什麼優勢。Swift
有三個重要的特性分別是富有表現力
/安全性
/快速
,接下來我們分別從這三個特性簡單介紹一下:
富有表現力
Swift
提供更多的程式設計正規化
和特性
支援,可以編寫更少的程式碼,而且易於閱讀和維護。
基礎型別
- 元組、Enum關聯型別
方法
-方法過載
protocol
- 不限制只支援class
、協議預設
實現、類
專屬協議泛型
-protocol
關聯型別、where
實現型別約束、泛型擴充套件可選值
- 可選值申明、可選鏈、隱式可選值屬性
- let、lazy、計算屬性`、willset/didset、Property Wrappers函數語言程式設計
- 集合filter/map/reduce
方法,提供更多標準庫方法併發
- async/await、actor標準庫框架
-Combine
響應式框架、SwiftUI
申明式UI框架、Codable
JSON模型轉換Result builder
- 描述實現DSL
的能力動態性
- dynamicCallable、dynamicMemberLookup其他
- 擴充套件、subscript、運算子重寫、巢狀型別、區間Swift Package Manager
- 基於Swift的包管理工具,可以直接用Xcode
進行管理更方便struct
- 初始化方法自動補齊型別推斷
- 透過編譯器強大的型別推斷
編寫程式碼時可以減少很多型別申明
提示:型別推斷同時也會增加一定的編譯
耗時
,不過Swift
團隊也在不斷的改善編譯速度。
安全性
程式碼安全
let屬性
- 使用let
申明常量避免被修改。值型別
- 值型別可以避免在方法呼叫等引數傳遞
過程中狀態被修改。訪問控制
- 透過public
和final
限制模組外使用class
不能被繼承
和重寫
。強制異常處理
- 方法需要丟擲異常時,需要申明為throw
方法。當呼叫可能會throw
異常的方法,需要強制捕獲異常避免將異常暴露到上層。模式匹配
- 透過模式匹配檢測switch
中未處理的case。
型別安全
強制型別轉換
- 禁止隱式型別轉換
避免轉換中帶來的異常問題。同時型別轉換不會帶來額外
的執行時消耗。。
提示:編寫
ObjC
程式碼時,我們通常會在編碼時新增型別檢查避免執行時崩潰導致Crash
。
KeyPath
-KeyPath
相比使用字串
可以提供屬性名和型別資訊,可以利用編譯器檢查。泛型
- 提供泛型
和協議關聯型別
,可以編寫出型別安全的程式碼。相比Any
可以更多利用編譯時檢查發現型別問題。Enum關聯型別
- 透過給特定列舉指定型別避免使用Any
。
記憶體安全
空安全
- 透過標識可選值避免空指標
帶來的異常問題ARC
- 使用自動
記憶體管理避免手動
管理記憶體帶來的各種記憶體問題強制初始化
- 變數使用前必須初始化
記憶體獨佔訪問
- 透過編譯器檢查發現潛在的記憶體衝突問題
執行緒安全
值型別
- 更多使用值型別減少在多執行緒中遇到的資料競爭
問題async/await
- 提供async
函式使我們可以用結構化的方式編寫併發操作。避免基於閉包
的非同步方式帶來的記憶體迴圈引用
和無法丟擲異常的問題Actor
- 提供Actor
模型避免多執行緒開發中進行資料共享時發生的資料競爭問題,同時避免在使用鎖時帶來的死鎖等問題
快速
值型別
- 相比class
不需要額外的堆記憶體
分配/釋放和更少的記憶體消耗方法靜態派發
- 方法呼叫支援靜態
呼叫相比原有ObjC訊息轉發
呼叫效能更好編譯器最佳化
- Swift的靜態性
可以使編譯器做更多最佳化。例如Tree Shaking
相關最佳化移除未使用的型別/方法等減少二進位制檔案大小。使用靜態派發
/方法內聯最佳化
/泛型特化
/寫時複製
等最佳化提高執行時效能
提示:
ObjC
訊息派發會導致編譯器無法進行移除無用方法/類的最佳化,編譯器並不知道是否可能被用到。
ARC最佳化
- 雖然和ObjC
一樣都是使用ARC
,Swift
透過編譯器最佳化,可以進行更快的記憶體回收和更少的記憶體引用計數管理
提示: 相比
ObjC
,Swift內部不需要使用autorelease
進行管理。
程式碼質量指標
以上是一些常見的程式碼質量指標。我們的目標是如何更好的使用Swift
編寫出符合程式碼質量指標要求的程式碼。
提示:本文不涉及設計模式/架構,更多關注如何透過合理使用
Swift
特性做區域性程式碼段的重構。
一些不錯的實踐
利用編譯檢查
減少使用Any/AnyObject
因為Any/AnyObject
缺少明確的型別資訊,編譯器無法進行型別檢查,會帶來一些問題:
- 編譯器無法檢查型別是否正確保證型別安全
- 程式碼中大量的
as?
轉換 - 型別的缺失導致編譯器無法做一些潛在的
編譯最佳化
使用as?
帶來的問題
當使用Any/AnyObject
時會頻繁使用as?
進行型別轉換。這好像沒什麼問題因為使用as?
並不會導致程式Crash
。不過程式碼錯誤至少應該分為兩類,一類是程式本身的錯誤通常會引發Crash,另外一種是業務邏輯錯誤。使用as?
只是避免了程式錯誤Crash
,但是並不能防止業務邏輯錯誤。
func do(data: Any?) {
guard let string = data as? String else {
return
}
//
}
do(1)
do("")
以上面的例子為例,我們進行了as?
轉換,當data
為String
時才會進行處理。但是當do
方法內String
型別發生了改變函式,使用方並不知道已變更沒有做相應的適配,這時候就會造成業務邏輯的錯誤。
提示:這類錯誤通常更難發現,這也是我們在一次真實
bug
場景遇到的。
使用自定義型別
代替Dictionary
程式碼中大量Dictionary
資料結構會降低程式碼可維護性,同時帶來潛在的bug
:
key
需要字串硬編碼,編譯時無法檢查value
沒有型別限制。修改
時型別無法限制,讀取時需要重複型別轉換和解包操作- 無法利用
空安全
特性,指定某個屬性必須有值
提示:
自定義型別
還有個好處,例如JSON
轉自定義型別
時會進行型別/nil/屬性名
檢查,可以避免將錯誤資料丟到下一層。
不推薦
let dic: [String: Any]
let num = dic["value"] as? Int
dic["name"] = "name"
推薦
struct Data {
let num: Int
var name: String?
}
let num = data.num
data.name = "name"
適合使用Dictionary
的場景
資料不使用
- 資料並不讀取
只是用來傳遞。解耦
- 1.元件間通訊
解耦使用HashMap
傳遞引數進行通訊。2.跨技術棧邊界的場景,混合棧間通訊/前後端通訊
使用HashMap
/JSON
進行通訊。
使用列舉關聯值
代替Any
例如使用列舉改造NSAttributedString
API,原有APIvalue
為Any
型別無法限制特定的型別。
最佳化前
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
改造後
enum NSAttributedStringKey {
case foregroundColor(UIColor)
}
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor(UIColor.red), range: range) // 不傳遞Color會報錯
使用泛型
/協議關聯型別
代替Any
使用泛型
或協議關聯型別
代替Any
,透過泛型型別約束
來使編譯器進行更多的型別檢查。
使用列舉
/常量
代替硬編碼
程式碼中存在重複的硬編碼
字串/數字,在修改時可能會因為不同步引發bug
。儘可能減少硬編碼
字串/數字,使用列舉
或常量
代替。
使用KeyPath
代替字串
硬編碼
KeyPath
包含屬性名和型別資訊,可以避免硬編碼
字串,同時當屬性名或型別改變時編譯器會進行檢查。
不推薦
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
let object = SomeClass(someProperty: 10)
object.observeValue(forKeyPath: "", of: nil, change: nil, context: nil)
推薦
let object = SomeClass(someProperty: 10)
object.observe(.someProperty) { object, change in
}
記憶體安全
減少使用!
屬性
!
屬性會在讀取時隱式強解包
,當值不存在時產生執行時異常導致Crash。
class ViewController: UIViewController {
@IBOutlet private var label: UILabel! // @IBOutlet需要使用!
}
減少使用!
進行強解包
使用!
強解包會在值不存在時產生執行時異常導致Crash。
var num: Int?
let num2 = num! // 錯誤
提示:建議只在小範圍的區域性程式碼段使用
!
強解包。
避免使用try!
進行錯誤處理
使用try!
會在方法丟擲異常時產生執行時異常導致Crash。
try! method()
使用weak
/unowned
避免迴圈引用
resource.request().onComplete { [weak self] response in
guard let self = self else {
return
}
let model = self.updateModel(response)
self.updateUI(model)
}
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
減少使用unowned
unowned
在值不存在時會產生執行時異常導致Crash,只有在確定self
一定會存在時才使用unowned
。
class Class {
@objc unowned var object: Object
@objc weak var object: Object?
}
unowned
/weak
區別:
weak
- 必須設定為可選值,會進行弱引用處理效能更差。會自動設定為nil
unowned
- 可以不設定為可選值,不會進行弱引用處理效能更好。但是不會自動設定為nil
, 如果self
已釋放會觸發錯誤.
錯誤處理方式
可選值
- 呼叫方並不關注內部可能會發生錯誤,當發生錯誤時返回nil
try/catch
- 明確提示呼叫方需要處理異常,需要實現Error
協議定義明確的錯誤型別assert
- 斷言。只能在Debug
模式下生效precondition
- 和assert
類似,可以再Debug
/Release
模式下生效fatalError
- 產生執行時崩潰會導致Crash,應避免使用Result
- 通常用於閉包
非同步回撥返回值
減少使用可選值
可選值
的價值在於透過明確標識值可能會為nil
並且編譯器強制對值進行nil
判斷。但是不應該隨意的定義可選值,可選值不能用let
定義,並且使用時必須進行解包
操作相對比較繁瑣。在程式碼設計時應考慮這個值是否有可能為nil
,只在合適的場景使用可選值。
使用init
注入代替可選值
屬性
不推薦
class Object {
var num: Int?
}
let object = Object()
object.num = 1
推薦
class Object {
let num: Int
init(num: Int) {
self.num = num
}
}
let object = Object(num: 1)
避免隨意給予可選值預設值
在使用可選值時,通常我們需要在可選值為nil
時進行異常處理。有時候我們會透過給予可選值預設值
的方式來處理。但是這裡應考慮在什麼場景下可以給予預設值。在不能給予預設值的場景應當及時使用return
或丟擲異常
,避免錯誤的值被傳遞到更多的業務流程。
不推薦
func confirmOrder(id: String) {}
// 給予錯誤的值會導致錯誤的值被傳遞到更多的業務流程
confirmOrder(id: orderId ?? "")
推薦
func confirmOrder(id: String) {}
guard let orderId = orderId else {
// 異常處理
return
}
confirmOrder(id: orderId)
提示:通常強業務相關的值不能給予預設值:例如
商品/訂單id
或是價格
。在可以使用兜底邏輯的場景使用預設值,例如預設文字/文字顏色
。
使用列舉最佳化可選值
Object
結構同時只會有一個值存在:
最佳化前
class Object {
var name: Int?
var num: Int?
}
最佳化後
降低記憶體佔用
-列舉關聯型別
的大小取決於最大的關聯型別大小邏輯更清晰
- 使用enum
相比大量使用if/else
邏輯更清晰
enum CustomType {
case name(String)
case num(Int)
}
減少var
屬性
使用計算屬性
使用計算屬性
可以減少多個變數同步帶來的潛在bug。
不推薦
class model {
var data: Object?
var loaded: Bool
}
model.data = Object()
loaded = false
推薦
class model {
var data: Object?
var loaded: Bool {
return data != nil
}
}
model.data = Object()
提示:計算屬性因為每次都會重複計算,所以計算過程需要輕量避免帶來效能問題。
控制流
使用filter/reduce/map
代替for
迴圈
使用filter/reduce/map
可以帶來很多好處,包括更少的區域性變數,減少模板程式碼,程式碼更加清晰,可讀性更高。
不推薦
let nums = [1, 2, 3]
var result = []
for num in nums {
if num < 3 {
result.append(String(num))
}
}
// result = ["1", "2"]
推薦
let nums = [1, 2, 3]
let result = nums.filter { $0 < 3 }.map { String($0) }
// result = ["1", "2"]
使用guard
進行提前返回
推薦
guard !a else {
return
}
guard !b else {
return
}
// do
不推薦
if a {
if b {
// do
}
}
使用三元運算子?:
推薦
let b = true
let a = b ? 1 : 2
let c: Int?
let b = c ?? 1
不推薦
var a: Int?
if b {
a = 1
} else {
a = 2
}
使用for where
最佳化迴圈
for
迴圈新增where
語句,只有當where
條件滿足時才會進入迴圈
不推薦
for item in collection {
if item.hasProperty {
// ...
}
}
推薦
for item in collection where item.hasProperty {
// item.hasProperty == true,才會進入迴圈
}
使用defer
defer
可以保證在函式退出前一定會執行。可以使用defer
中實現退出時一定會執行的操作例如資源釋放
等避免遺漏。
func method() {
lock.lock()
defer {
lock.unlock()
// 會在method作用域結束的時候呼叫
}
// do
}
字串
使用"""
在定義複雜
字串時,使用多行字串字面量
可以保持原有字串的換行符號/引號等特殊字元,不需要使用``進行轉義。
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
提示:上面字串中的
""
和換行可以自動保留。
使用字串插值
使用字串插值可以提高程式碼可讀性。
不推薦
let multiplier = 3
let message = String(multiplier) + "times 2.5 is" + String((Double(multiplier) * 2.5))
推薦
let multiplier = 3
let message = "(multiplier) times 2.5 is (Double(multiplier) * 2.5)"
集合
使用標準庫提供的高階函式
不推薦
var nums = []
nums.count == 0
nums[0]
推薦
var nums = []
nums.isEmpty
nums.first
訪問控制
Swift
中預設訪問控制級別為internal
。編碼中應當儘可能減小屬性
/方法
/型別
的訪問控制級別隱藏內部實現。
提示:同時也有利於編譯器進行最佳化。
使用private
/fileprivate
修飾私有屬性
和方法
private let num = 1
class MyClass {
private var num: Int
}
使用private(set)
修飾外部只讀/內部可讀寫屬性
class MyClass {
private(set) var num = 1
}
let num = MyClass().num
MyClass().num = 2 // 會編譯報錯
函式
使用引數預設值
使用引數預設值
,可以使呼叫方傳遞更少
的引數。
不推薦
func test(a: Int, b: String?, c: Int?) {
}
test(1, nil, nil)
推薦
func test(a: Int, b: String? = nil, c: Int? = nil) {
}
test(1)
提示:相比
ObjC
,引數預設值
也可以讓我們定義更少的方法。
限制引數數量
當方法引數過多時考慮使用自定義型別
代替。
不推薦
func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {
}
推薦
struct Params {
let a, b, c, d, e, f: Int
}
func f(params: Params) {
}
使用@discardableResult
某些方法使用方並不一定會處理返回值,可以考慮新增@discardableResult
標識提示Xcode
允許不處理返回值不進行warning
提示。
// 上報方法使用方不關心是否成功
func report(id: String) -> Bool {}
@discardableResult func report2(id: String) -> Bool {}
report("1") // 編譯器會警告
report2("1") // 不處理返回值編譯器不會警告
元組
避免過長的元組
元組雖然具有型別資訊,但是並不包含變數名
資訊,使用方並不清晰知道變數的含義。所以當元組數量過多時考慮使用自定義型別
代替。
func test() -> (Int, Int, Int) {
}
let (a, b, c) = test()
// a,b,c型別一致,沒有命名資訊不清楚每個變數的含義
系統庫
KVO
/Notification
使用 block
API
block
API的優勢:
KVO
可以支援KeyPath
- 不需要主動移除監聽,
observer
釋放時自動移除監聽
不推薦
class Object: NSObject {
init() {
super.init()
addObserver(self, forKeyPath: "value", options: .new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(test), name: NSNotification.Name(rawValue: ""), object: nil)
}
override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
}
@objc private func test() {
}
deinit {
removeObserver(self, forKeyPath: "value")
NotificationCenter.default.removeObserver(self)
}
}
推薦
class Object: NSObject {
private var observer: AnyObserver?
private var kvoObserver: NSKeyValueObservation?
init() {
super.init()
observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: ""), object: nil, queue: nil) { (_) in
}
kvoObserver = foo.observe(.value, options: [.new]) { (foo, change) in
}
}
}
Protocol
使用protocol
代替繼承
Swift
中針對protocol
提供了很多新特性,例如預設實現
,關聯型別
,支援值型別。在程式碼設計時可以優先考慮使用protocol
來避免臃腫的父類同時更多使用值型別。
提示:一些無法用
protocol
替代繼承
的場景:1.需要繼承NSObject子類。2.需要呼叫super
方法。3.實現抽象類
的能力。
Extension
使用extension
組織程式碼
使用extension
將私有方法
/父類方法
/協議方法
等不同功能程式碼進行分離更加清晰/易維護。
class MyViewController: UIViewController {
// class stuff here
}
// MARK: - Private
extension: MyViewController {
private func method() {}
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}
程式碼風格
良好的程式碼風格可以提高程式碼的可讀性
,統一的程式碼風格可以降低團隊內相互理解成本
。對於Swift
的程式碼格式化
建議使用自動格式化工具實現,將自動格式化新增到程式碼提交流程,透過定義Lint規則
統一團隊內程式碼風格。考慮使用SwiftFormat
和SwiftLint
。
提示:
SwiftFormat
主要關注程式碼樣式的格式化,SwiftLint
可以使用autocorrect
自動修復部分不規範的程式碼。
常見的自動格式化修正
- 移除多餘的
;
- 最多隻保留一行換行
- 自動對齊
空格
- 限制每行的寬度
自動換行
效能最佳化
效能最佳化上主要關注提高執行時效能
和降低二進位制體積
。需要考慮如何更好的使用Swift
特性,同時提供更多資訊給編譯器
進行最佳化。
使用Whole Module Optimization
當Xcode
開啟WMO
最佳化時,編譯器可以將整個程式編譯為一個檔案進行更多的最佳化。例如透過推斷final
/函式內聯
/泛型特化
更多使用靜態派發,並且可以移除
部分未使用的程式碼。
使用原始碼
打包
當我們使用元件化
時,為了提高編譯速度
和打包效率
,通常單個元件獨立編譯生成靜態庫
,最後多個元件直接使用靜態庫
進行打包。這種場景下WMO
僅針對internal
以內作用域生效,對於public/open
缺少外部使用資訊所以無法進行最佳化。所以對於大量使用Swift
的專案,使用全量程式碼打包
更有利於編譯器做更多最佳化。
減少方法動態
派發
使用final
-class
/方法
/屬性
申明為final
,編譯器可以最佳化為靜態派發使用private
-方法
/屬性
申明為private
,編譯器可以最佳化為靜態派發避免使用dynamic
-dynamic
會使方法透過ObjC訊息轉發
的方式派發使用WMO
- 編譯器可以自動分析推斷出final
最佳化為靜態派發
使用Slice
共享記憶體最佳化效能
在使用Array
/String
時,可以使用Slice
切片獲取一部分資料。Slice
儲存對原始Array
/String
的引用共享記憶體資料,不需要重新分配空間進行儲存。
let midpoint = absences.count / 2
let firstHalf = absences[..
`提示:應避免一直持有Slice,Slice會延長原始Array/String的生命週期導致無法被釋放造成記憶體洩漏。
protocol新增AnyObject protocol AnyProtocol {}
protocol ObjectProtocol: AnyObject {}
當protocol僅限制為class使用時,繼承AnyObject協議可以使編譯器不需要考慮值型別實現,提高執行時效能。
使用@inlinable進行方法內聯最佳化 // 原始程式碼 let label = UILabel().then { $0.textAlignment = .center $0.textColor = UIColor.black $0.text = "Hello, World!" }
以then庫為例,他使用閉包進行物件初始化以後的相關設定。但是 then 方法以及閉包也會帶來額外的效能消耗。
內聯最佳化 @inlinable public func then(_ block: (Self) throws -> Void) rethrows -> Self { try block(self) return self }
// 編譯器內聯最佳化後 let label = UILabel() label.textAlignment = .center label.textColor = UIColor.black label.text = "Hello, World!"
屬性 使用lazy延時初始化屬性 class View { var lazy label: UILabel = { let label = UILabel() self.addSubView(label) return label }() }
lazy屬性初始化會延遲到第一次使用時,常見的使用場景:
初始化比較耗時
可能不會被使用到
初始化過程需要使用self
提示:lazy屬性不能保證執行緒安全
避免使用private let屬性
private let屬性會增加每個class物件的記憶體大小。同時會增加包大小,因為需要為屬性生成相關的資訊。可以考慮使用檔案級private let申明或static常量代替。
不推薦 class Object { private let title = "12345" }
推薦 private let title = "12345" class Object { static let title = "" }
提示:這裡並不包括透過init初始化注入的屬性。
使用didSet/willSet時進行Diff
某些場景需要使用didSet/willSet屬性檢查器監控屬性變化,做一些額外的計算。但是由於didSet/willSet並不會檢查新/舊值是否相同,可以考慮新增新/舊值判斷,只有當值真的改變時才進行運算提高效能。
最佳化前 class Object { var orderId: String? { didSet { // 拉取介面等操作 } } }
例如上面的例子,當每一次orderId變更時需要重新拉取當前訂單的資料,但是當orderId值一樣時,拉取訂單資料是無效執行。
最佳化後 class Object { var orderId: String? { didSet { // 判斷新舊值是否相等 guard oldValue != orderId else { return } // 拉取介面等操作 } } }
集合 集合使用lazy延遲序列 var nums = [1, 2, 3] var result = nums.lazy.map { String($0) } result[0] // 對1進行map操作 result[1] // 對2進行map操作
在集合操作時使用lazy,可以將陣列運算操作推遲到第一次使用時,避免一次性全部計算。
提示:例如長列表,我們需要建立每個cell對應的檢視模型,一次性建立太耗費時間。
使用合適的集合方法最佳化效能 不推薦 var items = [1, 2, 3] items.filter({ $0 > 1 }).first // 查詢出所有大於1的元素,之後找出第一個
推薦 var items = [1, 2, 3] items.first(where: { $0 > 1 }) // 查詢出第一個大於1的元素直接返回
使用值型別
Swift中的值型別主要是結構體/列舉/元組。
啟動效能 - APP啟動時值型別沒有額外的消耗,class有一定額外的消耗。
執行時效能- 值型別不需要在堆上分配空間/額外的引用計數管理。更少的記憶體佔用和更快的效能。
包大小 - 相比class,值型別不需要建立ObjC類對應的ro_data_t資料結構。
提示:class即使沒有繼承NSObject也會生成ro_data_t,裡面包含了ivars屬性資訊。如果屬性/方法申明為@objc還會生成對應的方法列表。
提示:struct無法代替class的一些場景:1.需要使用繼承呼叫super。2.需要使用引用型別。3.需要使用deinit。4.需要在執行時動態轉換一個例項的型別。
提示:不是所有struct都會儲存在棧上,部分資料大的struct也會儲存在堆上。
集合元素使用值型別
集合元素使用值型別。因為NSArray並不支援值型別,編譯器不需要處理可能需要橋接到NSArray的場景,可以移除部分消耗。
純靜態型別避免使用class
當class只包含靜態方法/屬性時,考慮使用enum代替class,因為class會生成更多的二進位制程式碼。
不推薦 class Object { static var num: Int static func test() {} }
推薦 enum Object { static var num: Int static func test() {} }
提示:為什麼用enum而不是struct,因為struct會額外生成init方法。
值型別效能最佳化 考慮使用引用型別
值型別為了維持值語義,會在每次賦值/引數傳遞/修改時進行復制。雖然編譯器本身會做一些最佳化,例如寫時複製最佳化,在修改時減少複製頻率,但是這僅針對於標準庫提供的集合和String結構有效,對於自定義結構需要自己實現。對於引數傳遞編譯器在一些場景會最佳化為直接傳遞引用的方式避免複製行為。
但是對於一些資料特別大的結構,同時需要頻繁變更修改時也可以考慮使用引用型別實現。
使用inout傳遞引數減少複製
雖然編譯器本身會進行寫時複製的最佳化,但是部分場景編譯器無法處理。
不推薦 func append_one(_ a: [Int]) -> [Int] { var a = a a.append(1) // 無法被編譯器最佳化,因為這時候有2個引用持有陣列 return a }
var a = [1, 2, 3] a = append_one(a)
推薦
直接使用inout傳遞引數
func append_one_in_place(a: inout [Int]) { a.append(1) }
var a = [1, 2, 3] append_one_in_place(&a)
使用isKnownUniquelyReferenced實現寫時複製
預設情況下結構體中包含引用型別,在修改時只會重新複製引用。但是我們希望CustomData具備值型別的特性,所以當修改時需要重新複製NSMutableData避免複用。但是複製操作本身是耗時操作,我們希望可以減少一些不必要的複製。
最佳化前 struct CustomData { fileprivate var _data: NSMutableData var _dataForWriting: NSMutableData { mutating get { _data = _data.mutableCopy() as! NSMutableData return data } } init( data: NSData) { self._data = data.mutableCopy() as! NSMutableData }
mutating func append(_ other: MyData) { _dataForWriting.append(other._data as Data)
}
}
var buffer = CustomData(NSData()) for _ in 0..<5 { buffer.append(x) // 每一次呼叫都會複製 }
最佳化後
使用isKnownUniquelyReferenced檢查如果是唯一引用不進行復制。
final class Box { var unbox: A init(_ value: A) { self.unbox = value } }
struct CustomData { fileprivate var _data: Box var _dataForWriting: NSMutableData { mutating get { // 檢查引用是否唯一 if !isKnownUniquelyReferenced(&_data) { _data = Box(_data.unbox.mutableCopy() as! NSMutableData) } return data.unbox } } init( data: NSData) { self._data = Box(data.mutableCopy() as! NSMutableData) } }
var buffer = CustomData(NSData()) for _ in 0..<5 { buffer.append(x) // 只會在第一次呼叫時進行復制 }
提示:對於ObjC型別isKnownUniquelyReferenced會直接返回false。
減少使用Objc特性 避免使用Objc型別
儘可能避免在Swift中使用NSString/NSArray/NSDictionary等ObjC基礎型別。以Dictionary為例,雖然Swift Runtime可以在NSArray和Array之間進行隱式橋接需要O(1)的時間。但是字典當Key和Value既不是類也不是@objc協議時,需要對每個值進行橋接,可能會導致消耗O(n)時間。
減少新增@objc標識
@objc標識雖然不會強制使用訊息轉發的方式來呼叫方法/屬性,但是他會預設ObjC是可見的會生成和ObjC一樣的ro_data_t結構。
避免使用@objcMembers
使用@objcMembers修飾的類,預設會為類/屬性/方法/擴充套件都加上@objc標識。
@objcMembers class Object: NSObject { }
提示:你也可以使用@nonobjc取消支援ObjC。
避免繼承NSObject
你只需要在需要使用NSObject特性時才需要繼承,例如需要實現UITableViewDataSource相關協議。
使用let變數/屬性 最佳化集合建立
集合不需要修改時,使用let修飾,編譯器會最佳化建立集合的效能。例如針對let集合,編譯器在建立時可以分配更小的記憶體大小。
最佳化逃逸閉包
在Swift中,當捕獲var變數時編譯器需要生成一個在堆上的Box儲存變數用於之後對於變數的讀/寫,同時需要額外的記憶體管理操作。如果是let變數,編譯器可以儲存值複製或引用,避免使用Box。
避免使用大型struct使用class代替
大型struct通常是指屬性特別多並且巢狀型別很多。目前swift編譯器針對struct等值型別編譯最佳化處理的並不好,會生成大量的assignWithCopy、assignWithCopy等copy相關方法,生成大量的二進位制程式碼。使用class型別可以避免生成相關的copy方法。
提示:不要小看這部分二進位制的影響,個人在日常專案中遇到過複雜的大型struct能生成幾百KB的二進位制程式碼。但是目前並沒有好的方法去發現這類struct去做最佳化,只能透過相關工具去檢視生成的二進位制詳細資訊。希望官方可以早點最佳化。
優先使用Encodable/Decodable協議代替Codable
因為實現Encodable和Decodable協議的結構,編譯器在編譯時會自動生成對應的init(from decoder: Decoder)和encode(to: Encoder)方法。Codable同時實現了Encodable和Decodable協議,但是大部分場景下我們只需要encode或decode能力,所以明確指定實現Encodable或Decodable協議可以減少生成對應的方法減少包體積。
提示:對於屬性比較多的型別結構會產生很大的二進位制程式碼,有興趣可以用相關的工具看看生成的二進位制檔案。
減少使用Equatable協議
因為實現Equatable協議的結構,編譯器在編譯時會自動生成對應的equal方法。預設實現是針對所有欄位進行比較會生成大量的程式碼。所以當我們不需要實現==比較能力時不要實現Equatable或者對於屬性特別多的型別也可以考慮重寫Equatable協議,只針對部分屬性進行比較,這樣可以生成更少的程式碼減少包體積。
提示:對於屬性特別多的型別也可以考慮重寫Equatable協議,只針對部分屬性進行比較,同時也可以提升效能。
總結
個人從Swift3.0開始將Swift作為第一語言使用。編寫Swift程式碼並不只是簡單對於ObjC程式碼的翻譯/重寫,需要對於Swift特性更多的理解才能更好的利用這些特性帶來更多的收益。同時我們需要關注每個版本Swift的最佳化/改進和新特性。在這過程中也會提高我們的編碼能力,加深對於一些通用程式設計概念/思想的理解,包括空安全、值型別、協程、不共享資料的Actor併發模型、函數語言程式設計、面向協議程式設計、記憶體所有權等。對於新的現代程式語言例如Swift/Dart/TS/Kotlin/Rust等,很多特性/思想都是相互借鑑,當我們理解這些概念/思想以後對於理解其他語言也會更容易。
這裡推薦有興趣可以關注Swift Evolution,每個特性加入都會有一個提案,裡面會詳細介紹動機/使用場景/實現方式/未來方向。
擴充套件連結
The Swift Programming Language
Swift 進階
SwiftLint Rules
OptimizationTips
深入剖析Swift效能最佳化
Google Swift Style Guide
Swift Evolution
Dictionary
Array
String
struct`