前言
京喜APP
最早在2019年引入了Swift
,使用Swift
完成了第一個(gè)訂單模塊的開發(fā)脊串。之后一年多我們持續(xù)在團(tuán)隊(duì)/公司內(nèi)部推廣和普及Swift
辫呻,目前Swift
已經(jīng)支撐了70%+
以上的業(yè)務(wù)。通過使用Swift
提高了團(tuán)隊(duì)內(nèi)同學(xué)的開發(fā)效率琼锋,同時(shí)也帶來了質(zhì)量的提升放闺,目前來自Swift
的Crash的占比不到1%
。在這過程中不斷的學(xué)習(xí)/實(shí)踐缕坎,團(tuán)隊(duì)內(nèi)的Code Review
怖侦,也對(duì)如何使用Swift
來提高代碼質(zhì)量有更深的理解。
Swift特性
在討論如何使用Swift
提高代碼質(zhì)量之前谜叹,我們先來看看Swift
本身相比ObjC
或其他編程語言有什么優(yōu)勢匾寝。Swift
有三個(gè)重要的特性分別是富有表現(xiàn)力
/安全性
/快速
,接下來我們分別從這三個(gè)特性簡單介紹一下:
富有表現(xiàn)力
Swift
提供更多的編程范式
和特性
支持荷腊,可以編寫更少的代碼旗吁,而且易于閱讀和維護(hù)。
-
基礎(chǔ)類型
- 元組停局、Enum關(guān)聯(lián)類型
-
方法
-方法重載
-
protocol
- 不限制只支持class
、協(xié)議默認(rèn)
實(shí)現(xiàn)香府、類
專屬協(xié)議 -
泛型
-protocol
關(guān)聯(lián)類型董栽、where
實(shí)現(xiàn)類型約束、泛型擴(kuò)展 -
可選值
- 可選值申明企孩、可選鏈锭碳、隱式可選值 -
屬性
- let、lazy勿璃、計(jì)算屬性`擒抛、willset/didset、Property Wrappers -
函數(shù)式編程
- 集合filter/map/reduce
方法补疑,提供更多標(biāo)準(zhǔn)庫方法 -
并發(fā)
- async/await歧沪、actor -
標(biāo)準(zhǔn)庫框架
-Combine
響應(yīng)式框架、SwiftUI
申明式UI框架莲组、Codable
JSON模型轉(zhuǎn)換 -
Result builder
- 描述實(shí)現(xiàn)DSL
的能力 -
動(dòng)態(tài)性
- dynamicCallable诊胞、dynamicMemberLookup -
其他
- 擴(kuò)展、subscript锹杈、操作符重寫撵孤、嵌套類型、區(qū)間 -
Swift Package Manager
- 基于Swift的包管理工具竭望,可以直接用Xcode
進(jìn)行管理更方便 -
struct
- 初始化方法自動(dòng)補(bǔ)齊 -
類型推斷
- 通過編譯器強(qiáng)大的類型推斷
編寫代碼時(shí)可以減少很多類型申明
提示:類型推斷同時(shí)也會(huì)增加一定的編譯
耗時(shí)
邪码,不過Swift
團(tuán)隊(duì)也在不斷的改善編譯速度。
安全性
作為一個(gè)開發(fā)者咬清,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要闭专,這是一個(gè)我的iOS開發(fā)交流群:130 595 548奴潘,不管你是小白還是大牛都?xì)g迎入駐 ,讓我們一起進(jìn)步喻圃,共同發(fā)展S┎省(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔!)
代碼安全
-
let屬性
- 使用let
申明常量避免被修改斧拍。 -
值類型
- 值類型可以避免在方法調(diào)用等參數(shù)傳遞
過程中狀態(tài)被修改雀扶。 -
訪問控制
- 通過public
和final
限制模塊外使用class
不能被繼承
和重寫
。 -
強(qiáng)制異常處理
- 方法需要拋出異常時(shí)肆汹,需要申明為throw
方法愚墓。當(dāng)調(diào)用可能會(huì)throw
異常的方法,需要強(qiáng)制捕獲異常避免將異常暴露到上層昂勉。 -
模式匹配
- 通過模式匹配檢測switch
中未處理的case浪册。
類型安全
-
強(qiáng)制類型轉(zhuǎn)換
- 禁止隱式類型轉(zhuǎn)換
避免轉(zhuǎn)換中帶來的異常問題。同時(shí)類型轉(zhuǎn)換不會(huì)帶來額外
的運(yùn)行時(shí)消耗岗照。村象。
提示:編寫
ObjC
代碼時(shí),我們通常會(huì)在編碼時(shí)添加類型檢查避免運(yùn)行時(shí)崩潰導(dǎo)致Crash
攒至。
-
KeyPath
-KeyPath
相比使用字符串
可以提供屬性名和類型信息厚者,可以利用編譯器檢查。 -
泛型
- 提供泛型
和協(xié)議關(guān)聯(lián)類型
迫吐,可以編寫出類型安全的代碼库菲。相比Any
可以更多利用編譯時(shí)檢查發(fā)現(xiàn)類型問題。 -
Enum關(guān)聯(lián)類型
- 通過給特定枚舉指定類型避免使用Any
志膀。
內(nèi)存安全
-
空安全
- 通過標(biāo)識(shí)可選值避免空指針
帶來的異常問題 -
ARC
- 使用自動(dòng)
內(nèi)存管理避免手動(dòng)
管理內(nèi)存帶來的各種內(nèi)存問題 -
強(qiáng)制初始化
- 變量使用前必須初始化
-
內(nèi)存獨(dú)占訪問
- 通過編譯器檢查發(fā)現(xiàn)潛在的內(nèi)存沖突問題
線程安全
-
值類型
- 更多使用值類型減少在多線程中遇到的數(shù)據(jù)競爭
問題 -
async/await
- 提供async
函數(shù)使我們可以用結(jié)構(gòu)化的方式編寫并發(fā)操作熙宇。避免基于閉包
的異步方式帶來的內(nèi)存循環(huán)引用
和無法拋出異常的問題 -
Actor
- 提供Actor
模型避免多線程開發(fā)中進(jìn)行數(shù)據(jù)共享時(shí)發(fā)生的數(shù)據(jù)競爭問題,同時(shí)避免在使用鎖時(shí)帶來的死鎖等問題
快速
-
值類型
- 相比class
不需要額外的堆內(nèi)存
分配/釋放和更少的內(nèi)存消耗 -
方法靜態(tài)派發(fā)
- 方法調(diào)用支持靜態(tài)
調(diào)用相比原有ObjC消息轉(zhuǎn)發(fā)
調(diào)用性能更好 -
編譯器優(yōu)化
- Swift的靜態(tài)性
可以使編譯器做更多優(yōu)化溉浙。例如Tree Shaking
相關(guān)優(yōu)化移除未使用的類型/方法等減少二進(jìn)制文件大小烫止。使用靜態(tài)派發(fā)
/方法內(nèi)聯(lián)優(yōu)化
/泛型特化
/寫時(shí)復(fù)制
等優(yōu)化提高運(yùn)行時(shí)性能
提示:
ObjC
消息派發(fā)會(huì)導(dǎo)致編譯器無法進(jìn)行移除無用方法/類的優(yōu)化,編譯器并不知道是否可能被用到戳稽。
-
ARC優(yōu)化
- 雖然和ObjC
一樣都是使用ARC
烈拒,Swift
通過編譯器優(yōu)化,可以進(jìn)行更快的內(nèi)存回收和更少的內(nèi)存引用計(jì)數(shù)管理
提示: 相比
ObjC
广鳍,Swift內(nèi)部不需要使用autorelease
進(jìn)行管理荆几。
代碼質(zhì)量指標(biāo)
以上是一些常見的代碼質(zhì)量指標(biāo)。我們的目標(biāo)是如何更好的使用Swift
編寫出符合代碼質(zhì)量指標(biāo)要求的代碼赊时。
提示:本文不涉及設(shè)計(jì)模式/架構(gòu)吨铸,更多關(guān)注如何通過合理使用
Swift
特性做部分代碼段的重構(gòu)。
一些不錯(cuò)的實(shí)踐
利用編譯檢查
減少使用Any/AnyObject
因?yàn)?code>Any/AnyObject缺少明確的類型信息祖秒,編譯器無法進(jìn)行類型檢查诞吱,會(huì)帶來一些問題:
- 編譯器無法檢查類型是否正確保證類型安全
- 代碼中大量的
as?
轉(zhuǎn)換 - 類型的缺失導(dǎo)致編譯器無法做一些潛在的
編譯優(yōu)化
使用as?
帶來的問題
當(dāng)使用Any/AnyObject
時(shí)會(huì)頻繁使用as?
進(jìn)行類型轉(zhuǎn)換粗井。這好像沒什么問題因?yàn)槭褂?code>as?并不會(huì)導(dǎo)致程序Crash
揩悄。不過代碼錯(cuò)誤至少應(yīng)該分為兩類,一類是程序本身的錯(cuò)誤通常會(huì)引發(fā)Crash,另外一種是業(yè)務(wù)邏輯錯(cuò)誤秩霍。使用as?
只是避免了程序錯(cuò)誤Crash
先口,但是并不能防止業(yè)務(wù)邏輯錯(cuò)誤向胡。
func do(data: Any?) {
guard let string = data as? String else {
return
}
//
}
do(1)
do("")
復(fù)制代碼
以上面的例子為例窒典,我們進(jìn)行了as?
轉(zhuǎn)換,當(dāng)data
為String
時(shí)才會(huì)進(jìn)行處理阿趁。但是當(dāng)do
方法內(nèi)String
類型發(fā)生了改變函數(shù)膜蛔,使用方并不知道已變更沒有做相應(yīng)的適配,這時(shí)候就會(huì)造成業(yè)務(wù)邏輯的錯(cuò)誤脖阵。
提示:這類錯(cuò)誤通常更難發(fā)現(xiàn)皂股,這也是我們?cè)谝淮握鎸?shí)
bug
場景遇到的。
使用自定義類型
代替Dictionary
代碼中大量Dictionary
數(shù)據(jù)結(jié)構(gòu)會(huì)降低代碼可維護(hù)性命黔,同時(shí)帶來潛在的bug
:
-
key
需要字符串硬編碼呜呐,編譯時(shí)無法檢查 -
value
沒有類型限制。修改
時(shí)類型無法限制悍募,讀取時(shí)需要重復(fù)類型轉(zhuǎn)換和解包操作 - 無法利用
空安全
特性卵史,指定某個(gè)屬性必須有值
提示:
自定義類型
還有個(gè)好處,例如JSON
轉(zhuǎn)自定義類型
時(shí)會(huì)進(jìn)行類型/nil/屬性名
檢查搜立,可以避免將錯(cuò)誤數(shù)據(jù)丟到下一層。
不推薦
let dic: [String: Any]
let num = dic["value"] as? Int
dic["name"] = "name"
復(fù)制代碼
推薦
struct Data {
let num: Int
var name: String?
}
let num = data.num
data.name = "name"
復(fù)制代碼
適合使用Dictionary
的場景
-
數(shù)據(jù)不使用
- 數(shù)據(jù)并不讀取
只是用來傳遞槐秧。 -
解耦
- 1.組件間通信
解耦使用HashMap
傳遞參數(shù)進(jìn)行通信啄踊。2.跨技術(shù)棧邊界的場景,混合棧間通信/前后端通信
使用HashMap
/JSON
進(jìn)行通信刁标。
使用枚舉關(guān)聯(lián)值
代替Any
例如使用枚舉改造NSAttributedString
API颠通,原有APIvalue
為Any
類型無法限制特定的類型。
優(yōu)化前
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
復(fù)制代碼
改造后
enum NSAttributedStringKey {
case foregroundColor(UIColor)
}
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor(UIColor.red), range: range) // 不傳遞Color會(huì)報(bào)錯(cuò)
復(fù)制代碼
使用泛型
/協(xié)議關(guān)聯(lián)類型
代替Any
使用泛型
或協(xié)議關(guān)聯(lián)類型
代替Any
膀懈,通過泛型類型約束
來使編譯器進(jìn)行更多的類型檢查顿锰。
使用枚舉
/常量
代替硬編碼
代碼中存在重復(fù)的硬編碼
字符串/數(shù)字,在修改時(shí)可能會(huì)因?yàn)椴煌揭l(fā)bug
启搂。盡可能減少硬編碼
字符串/數(shù)字硼控,使用枚舉
或常量
代替。
使用KeyPath
代替字符串
硬編碼
KeyPath
包含屬性名和類型信息胳赌,可以避免硬編碼
字符串牢撼,同時(shí)當(dāng)屬性名或類型改變時(shí)編譯器會(huì)進(jìn)行檢查。
不推薦
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)
復(fù)制代碼
推薦
let object = SomeClass(someProperty: 10)
object.observe(\.someProperty) { object, change in
}
復(fù)制代碼
內(nèi)存安全
減少使用!
屬性
!
屬性會(huì)在讀取時(shí)隱式強(qiáng)解包
疑苫,當(dāng)值不存在時(shí)產(chǎn)生運(yùn)行時(shí)異常導(dǎo)致Crash熏版。
class ViewController: UIViewController {
@IBOutlet private var label: UILabel! // @IBOutlet需要使用!
}
復(fù)制代碼
減少使用!
進(jìn)行強(qiáng)解包
使用!
強(qiáng)解包會(huì)在值不存在時(shí)產(chǎn)生運(yùn)行時(shí)異常導(dǎo)致Crash纷责。
var num: Int?
let num2 = num! // 錯(cuò)誤
復(fù)制代碼
提示:建議只在小范圍的局部代碼段使用
!
強(qiáng)解包。
避免使用try!
進(jìn)行錯(cuò)誤處理
使用try!
會(huì)在方法拋出異常時(shí)產(chǎn)生運(yùn)行時(shí)異常導(dǎo)致Crash撼短。
try! method()
復(fù)制代碼
使用weak
/unowned
避免循環(huán)引用
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)
}
復(fù)制代碼
減少使用unowned
unowned
在值不存在時(shí)會(huì)產(chǎn)生運(yùn)行時(shí)異常導(dǎo)致Crash再膳,只有在確定self
一定會(huì)存在時(shí)才使用unowned
。
class Class {
@objc unowned var object: Object
@objc weak var object: Object?
}
復(fù)制代碼
unowned
/weak
區(qū)別:
-
weak
- 必須設(shè)置為可選值曲横,會(huì)進(jìn)行弱引用處理性能更差喂柒。會(huì)自動(dòng)設(shè)置為nil
-
unowned
- 可以不設(shè)置為可選值,不會(huì)進(jìn)行弱引用處理性能更好胜榔。但是不會(huì)自動(dòng)設(shè)置為nil
, 如果self
已釋放會(huì)觸發(fā)錯(cuò)誤.
錯(cuò)誤處理方式
-
可選值
- 調(diào)用方并不關(guān)注內(nèi)部可能會(huì)發(fā)生錯(cuò)誤胳喷,當(dāng)發(fā)生錯(cuò)誤時(shí)返回nil
-
try/catch
- 明確提示調(diào)用方需要處理異常,需要實(shí)現(xiàn)Error
協(xié)議定義明確的錯(cuò)誤類型 -
assert
- 斷言夭织。只能在Debug
模式下生效 -
precondition
- 和assert
類似吭露,可以再Debug
/Release
模式下生效 -
fatalError
- 產(chǎn)生運(yùn)行時(shí)崩潰會(huì)導(dǎo)致Crash,應(yīng)避免使用 -
Result
- 通常用于閉包
異步回調(diào)返回值
減少使用可選值
可選值
的價(jià)值在于通過明確標(biāo)識(shí)值可能會(huì)為nil
并且編譯器強(qiáng)制對(duì)值進(jìn)行nil
判斷尊惰。但是不應(yīng)該隨意的定義可選值讲竿,可選值不能用let
定義,并且使用時(shí)必須進(jìn)行解包
操作相對(duì)比較繁瑣弄屡。在代碼設(shè)計(jì)時(shí)應(yīng)考慮這個(gè)值是否有可能為nil
题禀,只在合適的場景使用可選值。
使用init
注入代替可選值
屬性
不推薦
class Object {
var num: Int?
}
let object = Object()
object.num = 1
復(fù)制代碼
推薦
class Object {
let num: Int
init(num: Int) {
self.num = num
}
}
let object = Object(num: 1)
復(fù)制代碼
避免隨意給予可選值默認(rèn)值
在使用可選值時(shí)膀捷,通常我們需要在可選值為nil
時(shí)進(jìn)行異常處理迈嘹。有時(shí)候我們會(huì)通過給予可選值默認(rèn)值
的方式來處理。但是這里應(yīng)考慮在什么場景下可以給予默認(rèn)值全庸。在不能給予默認(rèn)值的場景應(yīng)當(dāng)及時(shí)使用return
或拋出異常
秀仲,避免錯(cuò)誤的值被傳遞到更多的業(yè)務(wù)流程。
不推薦
func confirmOrder(id: String) {}
// 給予錯(cuò)誤的值會(huì)導(dǎo)致錯(cuò)誤的值被傳遞到更多的業(yè)務(wù)流程
confirmOrder(id: orderId ?? "")
復(fù)制代碼
推薦
func confirmOrder(id: String) {}
guard let orderId = orderId else {
// 異常處理
return
}
confirmOrder(id: orderId)
復(fù)制代碼
提示:通常強(qiáng)業(yè)務(wù)相關(guān)的值不能給予默認(rèn)值:例如
商品/訂單id
或是價(jià)格
壶笼。在可以使用兜底邏輯的場景使用默認(rèn)值神僵,例如默認(rèn)文字/文字顏色
。
使用枚舉優(yōu)化可選值
Object
結(jié)構(gòu)同時(shí)只會(huì)有一個(gè)值存在:
優(yōu)化前
class Object {
var name: Int?
var num: Int?
}
復(fù)制代碼
優(yōu)化后
-
降低內(nèi)存占用
-枚舉關(guān)聯(lián)類型
的大小取決于最大的關(guān)聯(lián)類型大小 -
邏輯更清晰
- 使用enum
相比大量使用if/else
邏輯更清晰
enum CustomType {
case name(String)
case num(Int)
}
復(fù)制代碼
減少var
屬性
使用計(jì)算屬性
使用計(jì)算屬性
可以減少多個(gè)變量同步帶來的潛在bug覆劈。
不推薦
class model {
var data: Object?
var loaded: Bool
}
model.data = Object()
loaded = false
復(fù)制代碼
推薦
class model {
var data: Object?
var loaded: Bool {
return data != nil
}
}
model.data = Object()
復(fù)制代碼
提示:計(jì)算屬性因?yàn)槊看味紩?huì)重復(fù)計(jì)算保礼,所以計(jì)算過程需要輕量避免帶來性能問題。
控制流
使用filter/reduce/map
代替for
循環(huán)
使用filter/reduce/map
可以帶來很多好處责语,包括更少的局部變量炮障,減少模板代碼,代碼更加清晰坤候,可讀性更高铝阐。
不推薦
let nums = [1, 2, 3]
var result = []
for num in nums {
if num < 3 {
result.append(String(num))
}
}
// result = ["1", "2"]
復(fù)制代碼
推薦
let nums = [1, 2, 3]
let result = nums.filter { $0 < 3 }.map { String($0) }
// result = ["1", "2"]
復(fù)制代碼
使用guard
進(jìn)行提前返回
推薦
guard !a else {
return
}
guard !b else {
return
}
// do
復(fù)制代碼
不推薦
if a {
if b {
// do
}
}
復(fù)制代碼
使用三元運(yùn)算符?:
推薦
let b = true
let a = b ? 1 : 2
let c: Int?
let b = c ?? 1
復(fù)制代碼
不推薦
var a: Int?
if b {
a = 1
} else {
a = 2
}
復(fù)制代碼
使用for where
優(yōu)化循環(huán)
for
循環(huán)添加where
語句,只有當(dāng)where
條件滿足時(shí)才會(huì)進(jìn)入循環(huán)
不推薦
for item in collection {
if item.hasProperty {
// ...
}
}
復(fù)制代碼
推薦
for item in collection where item.hasProperty {
// item.hasProperty == true铐拐,才會(huì)進(jìn)入循環(huán)
}
復(fù)制代碼
使用defer
defer
可以保證在函數(shù)退出前一定會(huì)執(zhí)行徘键×范裕可以使用defer
中實(shí)現(xiàn)退出時(shí)一定會(huì)執(zhí)行的操作例如資源釋放
等避免遺漏。
func method() {
lock.lock()
defer {
lock.unlock()
// 會(huì)在method作用域結(jié)束的時(shí)候調(diào)用
}
// do
}
復(fù)制代碼
字符串
使用"""
在定義復(fù)雜
字符串時(shí)吹害,使用多行字符串字面量
可以保持原有字符串的換行符號(hào)/引號(hào)等特殊字符螟凭,不需要使用\
進(jìn)行轉(zhuǎn)義。
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."
"""
復(fù)制代碼
提示:上面字符串中的
""
和換行可以自動(dòng)保留它呀。
使用字符串插值
使用字符串插值可以提高代碼可讀性螺男。
不推薦
let multiplier = 3
let message = String(multiplier) + "times 2.5 is" + String((Double(multiplier) * 2.5))
復(fù)制代碼
推薦
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
復(fù)制代碼
集合
使用標(biāo)準(zhǔn)庫提供的高階函數(shù)
不推薦
var nums = []
nums.count == 0
nums[0]
復(fù)制代碼
推薦
var nums = []
nums.isEmpty
nums.first
復(fù)制代碼
訪問控制
Swift
中默認(rèn)訪問控制級(jí)別為internal
。編碼中應(yīng)當(dāng)盡可能減小屬性
/方法
/類型
的訪問控制級(jí)別隱藏內(nèi)部實(shí)現(xiàn)纵穿。
提示:同時(shí)也有利于編譯器進(jìn)行優(yōu)化下隧。
使用private
/fileprivate
修飾私有屬性
和方法
private let num = 1
class MyClass {
private var num: Int
}
復(fù)制代碼
使用private(set)
修飾外部只讀/內(nèi)部可讀寫屬性
class MyClass {
private(set) var num = 1
}
let num = MyClass().num
MyClass().num = 2 // 會(huì)編譯報(bào)錯(cuò)
復(fù)制代碼
函數(shù)
使用參數(shù)默認(rèn)值
使用參數(shù)默認(rèn)值
,可以使調(diào)用方傳遞更少
的參數(shù)谓媒。
不推薦
func test(a: Int, b: String?, c: Int?) {
}
test(1, nil, nil)
復(fù)制代碼
推薦
func test(a: Int, b: String? = nil, c: Int? = nil) {
}
test(1)
復(fù)制代碼
提示:相比
ObjC
淆院,參數(shù)默認(rèn)值
也可以讓我們定義更少的方法。
限制參數(shù)數(shù)量
當(dāng)方法參數(shù)過多時(shí)考慮使用自定義類型
代替句惯。
不推薦
func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {
}
復(fù)制代碼
推薦
struct Params {
let a, b, c, d, e, f: Int
}
func f(params: Params) {
}
復(fù)制代碼
使用@discardableResult
某些方法使用方并不一定會(huì)處理返回值土辩,可以考慮添加@discardableResult
標(biāo)識(shí)提示Xcode
允許不處理返回值不進(jìn)行warning
提示。
// 上報(bào)方法使用方不關(guān)心是否成功
func report(id: String) -> Bool {}
@discardableResult func report2(id: String) -> Bool {}
report("1") // 編譯器會(huì)警告
report2("1") // 不處理返回值編譯器不會(huì)警告
復(fù)制代碼
元組
避免過長的元組
元組雖然具有類型信息抢野,但是并不包含變量名
信息拷淘,使用方并不清晰知道變量的含義。所以當(dāng)元組數(shù)量過多時(shí)考慮使用自定義類型
代替指孤。
func test() -> (Int, Int, Int) {
}
let (a, b, c) = test()
// a启涯,b,c類型一致恃轩,沒有命名信息不清楚每個(gè)變量的含義
復(fù)制代碼
系統(tǒng)庫
KVO
/Notification
使用 block
API
block
API的優(yōu)勢:
-
KVO
可以支持KeyPath
- 不需要主動(dòng)移除監(jiān)聽结洼,
observer
釋放時(shí)自動(dòng)移除監(jiān)聽
不推薦
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)
}
}
復(fù)制代碼
推薦
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
}
}
}
復(fù)制代碼
Protocol
使用protocol
代替繼承
Swift
中針對(duì)protocol
提供了很多新特性,例如默認(rèn)實(shí)現(xiàn)
详恼,關(guān)聯(lián)類型
,支持值類型引几。在代碼設(shè)計(jì)時(shí)可以優(yōu)先考慮使用protocol
來避免臃腫的父類同時(shí)更多使用值類型昧互。
提示:一些無法用
protocol
替代繼承
的場景:1.需要繼承NSObject子類。2.需要調(diào)用super
方法伟桅。3.實(shí)現(xiàn)抽象類
的能力敞掘。
Extension
使用extension
組織代碼
使用extension
將私有方法
/父類方法
/協(xié)議方法
等不同功能代碼進(jìn)行分離更加清晰/易維護(hù)。
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
}
復(fù)制代碼
代碼風(fēng)格
良好的代碼風(fēng)格可以提高代碼的可讀性
楣铁,統(tǒng)一的代碼風(fēng)格可以降低團(tuán)隊(duì)內(nèi)相互理解成本
玖雁。對(duì)于Swift
的代碼格式化
建議使用自動(dòng)格式化工具實(shí)現(xiàn),將自動(dòng)格式化添加到代碼提交流程盖腕,通過定義Lint規(guī)則
統(tǒng)一團(tuán)隊(duì)內(nèi)代碼風(fēng)格赫冬∨ň担考慮使用SwiftFormat
和SwiftLint
。
提示:
SwiftFormat
主要關(guān)注代碼樣式的格式化劲厌,SwiftLint
可以使用autocorrect
自動(dòng)修復(fù)部分不規(guī)范的代碼膛薛。
常見的自動(dòng)格式化修正
- 移除多余的
;
- 最多只保留一行換行
- 自動(dòng)對(duì)齊
空格
- 限制每行的寬度
自動(dòng)換行
性能優(yōu)化
性能優(yōu)化上主要關(guān)注提高運(yùn)行時(shí)性能
和降低二進(jìn)制體積
。需要考慮如何更好的使用Swift
特性补鼻,同時(shí)提供更多信息給編譯器
進(jìn)行優(yōu)化哄啄。
使用Whole Module Optimization
當(dāng)Xcode
開啟WMO
優(yōu)化時(shí),編譯器可以將整個(gè)程序編譯為一個(gè)文件進(jìn)行更多的優(yōu)化风范。例如通過推斷final
/函數(shù)內(nèi)聯(lián)
/泛型特化
更多使用靜態(tài)派發(fā)咨跌,并且可以移除
部分未使用的代碼。
使用源代碼
打包
當(dāng)我們使用組件化
時(shí)硼婿,為了提高編譯速度
和打包效率
锌半,通常單個(gè)組件獨(dú)立編譯生成靜態(tài)庫
,最后多個(gè)組件直接使用靜態(tài)庫
進(jìn)行打包加酵。這種場景下WMO
僅針對(duì)internal
以內(nèi)作用域生效拳喻,對(duì)于public/open
缺少外部使用信息所以無法進(jìn)行優(yōu)化。所以對(duì)于大量使用Swift
的項(xiàng)目猪腕,使用全量代碼打包
更有利于編譯器做更多優(yōu)化冗澈。
減少方法動(dòng)態(tài)
派發(fā)
-
使用final
-class
/方法
/屬性
申明為final
,編譯器可以優(yōu)化為靜態(tài)派發(fā) -
使用private
-方法
/屬性
申明為private
陋葡,編譯器可以優(yōu)化為靜態(tài)派發(fā) -
避免使用dynamic
-dynamic
會(huì)使方法通過ObjC消息轉(zhuǎn)發(fā)
的方式派發(fā) -
使用WMO
- 編譯器可以自動(dòng)分析推斷出final
優(yōu)化為靜態(tài)派發(fā)
使用Slice
共享內(nèi)存優(yōu)化性能
在使用Array
/String
時(shí)亚亲,可以使用Slice
切片獲取一部分?jǐn)?shù)據(jù)。Slice
保存對(duì)原始Array
/String
的引用共享內(nèi)存數(shù)據(jù)腐缤,不需要重新分配空間進(jìn)行存儲(chǔ)捌归。
let midpoint = absences.count / 2
let firstHalf = absences[..<midpoint]
let secondHalf = absences[midpoint...]
// firstHalf/secondHalf并不會(huì)復(fù)制和占用更多內(nèi)存
復(fù)制代碼
提示:應(yīng)
避免
一直持有Slice
,Slice
會(huì)延長原始Array
/String
的生命周期導(dǎo)致無法被釋放造成內(nèi)存泄漏
岭粤。
protocol
添加AnyObject
protocol AnyProtocol {}
protocol ObjectProtocol: AnyObject {}
復(fù)制代碼
當(dāng)protocol
僅限制為class
使用時(shí)惜索,繼承AnyObject
協(xié)議可以使編譯器不需要考慮值類型
實(shí)現(xiàn),提高運(yùn)行時(shí)性能剃浇。
使用@inlinable
進(jìn)行方法內(nèi)聯(lián)優(yōu)化
// 原始代碼
let label = UILabel().then {
$0.textAlignment = .center
$0.textColor = UIColor.black
$0.text = "Hello, World!"
}
復(fù)制代碼
以then
庫為例巾兆,他使用閉包進(jìn)行對(duì)象初始化以后的相關(guān)設(shè)置。但是 then
方法以及閉包
也會(huì)帶來額外的性能消耗虎囚。
內(nèi)聯(lián)優(yōu)化
@inlinable
public func then(_ block: (Self) throws -> Void) rethrows -> Self {
try block(self)
return self
}
復(fù)制代碼
// 編譯器內(nèi)聯(lián)優(yōu)化后
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.black
label.text = "Hello, World!"
復(fù)制代碼
屬性
使用lazy
延時(shí)初始化屬性
class View {
var lazy label: UILabel = {
let label = UILabel()
self.addSubView(label)
return label
}()
}
復(fù)制代碼
lazy
屬性初始化會(huì)延遲
到第一次使用時(shí)角塑,常見的使用場景:
- 初始化比較耗時(shí)
- 可能不會(huì)被使用到
- 初始化過程需要使用
self
提示:
lazy
屬性不能保證線程安全
避免使用private let
屬性
private let
屬性會(huì)增加每個(gè)class
對(duì)象的內(nèi)存大小。同時(shí)會(huì)增加包大小
淘讥,因?yàn)樾枰獮閷傩陨上嚓P(guān)的信息圃伶。可以考慮使用文件級(jí)private let
申明或static
常量代替。
不推薦
class Object {
private let title = "12345"
}
復(fù)制代碼
推薦
private let title = "12345"
class Object {
static let title = ""
}
復(fù)制代碼
提示:這里并不包括通過
init
初始化注入的屬性窒朋。
使用didSet
/willSet
時(shí)進(jìn)行Diff
某些場景需要使用didSet
/willSet
屬性檢查器監(jiān)控屬性變化搀罢,做一些額外的計(jì)算。但是由于didSet
/willSet
并不會(huì)檢查新/舊
值是否相同炼邀,可以考慮添加新/舊
值判斷魄揉,只有當(dāng)值真的改變時(shí)才進(jìn)行運(yùn)算提高性能。
優(yōu)化前
class Object {
var orderId: String? {
didSet {
// 拉取接口等操作
}
}
}
復(fù)制代碼
例如上面的例子拭宁,當(dāng)每一次orderId
變更時(shí)需要重新拉取當(dāng)前訂單的數(shù)據(jù)洛退,但是當(dāng)orderId值一樣時(shí),拉取訂單數(shù)據(jù)是無效執(zhí)行杰标。
優(yōu)化后
class Object {
var orderId: String? {
didSet {
// 判斷新舊值是否相等
guard oldValue != orderId else {
return
}
// 拉取接口等操作
}
}
}
復(fù)制代碼
集合
集合使用lazy
延遲序列
var nums = [1, 2, 3]
var result = nums.lazy.map { String($0) }
result[0] // 對(duì)1進(jìn)行map操作
result[1] // 對(duì)2進(jìn)行map操作
復(fù)制代碼
在集合操作時(shí)使用lazy
兵怯,可以將數(shù)組運(yùn)算操作推遲
到第一次使用時(shí),避免一次性全部計(jì)算腔剂。
提示:例如長列表媒区,我們需要?jiǎng)?chuàng)建每個(gè)
cell
對(duì)應(yīng)的視圖模型
,一次性創(chuàng)建太耗費(fèi)時(shí)間掸犬。
使用合適的集合方法優(yōu)化性能
不推薦
var items = [1, 2, 3]
items.filter({ $0 > 1 }).first // 查找出所有大于1的元素袜漩,之后找出第一個(gè)
復(fù)制代碼
推薦
var items = [1, 2, 3]
items.first(where: { $0 > 1 }) // 查找出第一個(gè)大于1的元素直接返回
復(fù)制代碼
使用值類型
Swift
中的值類型主要是結(jié)構(gòu)體
/枚舉
/元組
。
-
啟動(dòng)性能
-APP啟動(dòng)
時(shí)值類型沒有額外的消耗湾碎,class
有一定額外的消耗宙攻。 -
運(yùn)行時(shí)性能
- 值類型不需要在堆上分配空間/額外的引用計(jì)數(shù)管理。更少的內(nèi)存占用和更快的性能介褥。 -
包大小
- 相比class
座掘,值類型不需要?jiǎng)?chuàng)建ObjC
類對(duì)應(yīng)的ro_data_t
數(shù)據(jù)結(jié)構(gòu)。
提示:
class
即使沒有繼承NSObject
也會(huì)生成ro_data_t
柔滔,里面包含了ivars
屬性信息溢陪。如果屬性/方法
申明為@objc
還會(huì)生成對(duì)應(yīng)的方法列表。
提示:
struct
無法代替class
的一些場景:1.需要使用繼承
調(diào)用super
睛廊。2.需要使用引用類型形真。3.需要使用deinit
。4.需要在運(yùn)行時(shí)動(dòng)態(tài)轉(zhuǎn)換一個(gè)實(shí)例的類型超全。
> 提示:不是所有struct
都會(huì)保存在棧
上咆霜,部分?jǐn)?shù)據(jù)大的struct
也會(huì)保存在堆上。
集合元素使用值類型
集合元素使用值類型卵迂。因?yàn)?code>NSArray并不支持值類型裕便,編譯器不需要處理可能需要橋接到NSArray
的場景绒净,可以移除部分消耗见咒。
純靜態(tài)類型避免使用class
當(dāng)class
只包含靜態(tài)方法/屬性
時(shí),考慮使用enum
代替class
挂疆,因?yàn)?code>class會(huì)生成更多的二進(jìn)制代碼改览。
不推薦
class Object {
static var num: Int
static func test() {}
}
復(fù)制代碼
推薦
enum Object {
static var num: Int
static func test() {}
}
復(fù)制代碼
提示:為什么用
enum
而不是struct
下翎,因?yàn)?code>struct會(huì)額外生成init
方法。
值類型性能優(yōu)化
考慮使用引用類型
值類型為了維持值語義
宝当,會(huì)在每次賦值
/參數(shù)傳遞
/修改
時(shí)進(jìn)行復(fù)制视事。雖然編譯器本身會(huì)做一些優(yōu)化,例如寫時(shí)復(fù)制優(yōu)化
庆揩,在修改
時(shí)減少復(fù)制頻率俐东,但是這僅針對(duì)于標(biāo)準(zhǔn)庫提供的集合
和String
結(jié)構(gòu)有效,對(duì)于自定義結(jié)構(gòu)
需要自己實(shí)現(xiàn)订晌。對(duì)于參數(shù)傳遞
編譯器在一些場景會(huì)優(yōu)化為直接傳遞引用
的方式避免復(fù)制行為虏辫。
但是對(duì)于一些數(shù)據(jù)特別大的結(jié)構(gòu),同時(shí)需要頻繁變更修改時(shí)也可以考慮使用引用類型
實(shí)現(xiàn)锈拨。
使用inout
傳遞參數(shù)減少復(fù)制
雖然編譯器本身會(huì)進(jìn)行寫時(shí)復(fù)制
的優(yōu)化砌庄,但是部分場景編譯器無法處理。
不推薦
func append_one(_ a: [Int]) -> [Int] {
var a = a
a.append(1) // 無法被編譯器優(yōu)化奕枢,因?yàn)檫@時(shí)候有2個(gè)引用持有數(shù)組
return a
}
var a = [1, 2, 3]
a = append_one(a)
復(fù)制代碼
推薦
直接使用inout
傳遞參數(shù)
func append_one_in_place(a: inout [Int]) {
a.append(1)
}
var a = [1, 2, 3]
append_one_in_place(&a)
復(fù)制代碼
使用isKnownUniquelyReferenced
實(shí)現(xiàn)寫時(shí)復(fù)制
默認(rèn)情況下結(jié)構(gòu)體中包含引用類型
娄昆,在修改時(shí)只會(huì)重新拷貝引用。但是我們希望CustomData
具備值類型的特性缝彬,所以當(dāng)修改時(shí)需要重新復(fù)制NSMutableData
避免復(fù)用萌焰。但是復(fù)制
操作本身是耗時(shí)操作,我們希望可以減少一些不必要的復(fù)制跌造。
優(yōu)化前
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) // 每一次調(diào)用都會(huì)復(fù)制
}
復(fù)制代碼
優(yōu)化后
使用isKnownUniquelyReferenced
檢查如果是唯一引用
不進(jìn)行復(fù)制杆怕。
final class Box<A> {
var unbox: A
init(_ value: A) { self.unbox = value }
}
struct CustomData {
fileprivate var _data: Box<NSMutableData>
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) // 只會(huì)在第一次調(diào)用時(shí)進(jìn)行復(fù)制
}
復(fù)制代碼
提示:對(duì)于
ObjC
類型isKnownUniquelyReferenced
會(huì)直接返回false
。
減少使用Objc
特性
避免使用Objc
類型
盡可能避免在Swift
中使用NSString
/NSArray
/NSDictionary
等ObjC
基礎(chǔ)類型壳贪。以Dictionary
為例陵珍,雖然Swift Runtime
可以在NSArray
和Array
之間進(jìn)行隱式橋接需要O(1)
的時(shí)間。但是字典當(dāng)Key
和Value
既不是類也不是@objc
協(xié)議時(shí)违施,需要對(duì)每個(gè)值
進(jìn)行橋接互纯,可能會(huì)導(dǎo)致消耗O(n)
時(shí)間。
減少添加@objc
標(biāo)識(shí)
@objc
標(biāo)識(shí)雖然不會(huì)強(qiáng)制使用消息轉(zhuǎn)發(fā)
的方式來調(diào)用方法/屬性
磕蒲,但是他會(huì)默認(rèn)ObjC
是可見的會(huì)生成和ObjC
一樣的ro_data_t
結(jié)構(gòu)留潦。
避免使用@objcMembers
使用@objcMembers
修飾的類,默認(rèn)會(huì)為類
/屬性
/方法
/擴(kuò)展
都加上@objc
標(biāo)識(shí)辣往。
@objcMembers class Object: NSObject {
}
復(fù)制代碼
提示:你也可以使用
@nonobjc
取消支持ObjC
兔院。
避免繼承NSObject
你只需要在需要使用NSObject
特性時(shí)才需要繼承,例如需要實(shí)現(xiàn)UITableViewDataSource
相關(guān)協(xié)議站削。
使用let
變量/屬性
優(yōu)化集合創(chuàng)建
集合不需要修改時(shí)坊萝,使用let
修飾,編譯器會(huì)優(yōu)化創(chuàng)建集合的性能。例如針對(duì)let
集合十偶,編譯器
在創(chuàng)建時(shí)可以分配更小的內(nèi)存大小
菩鲜。
優(yōu)化逃逸閉包
在Swift
中,當(dāng)捕獲var
變量時(shí)編譯器需要生成一個(gè)在堆上的Box
保存變量用于之后對(duì)于變量的讀/寫
惦积,同時(shí)需要額外的內(nèi)存管理操作接校。如果是let
變量,編譯器可以保存值復(fù)制或引用
狮崩,避免使用Box
蛛勉。
總結(jié)
首先作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要睦柴,這是一個(gè)我的iOS開發(fā)交流群:130 595 548董习,不管你是小白還是大牛都?xì)g迎入駐 ,讓我們一起進(jìn)步爱只,共同發(fā)展C罅堋(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔!)
個(gè)人從Swift
3.0開始將Swift
作為第一語言使用恬试。編寫Swift
代碼并不只是簡單對(duì)于ObjC
代碼的翻譯/重寫窝趣,需要對(duì)于Swift
特性更多的理解才能更好的利用這些特性帶來更多的收益。同時(shí)我們需要關(guān)注每個(gè)版本Swift
的優(yōu)化/改進(jìn)和新特性训柴。在這過程中也會(huì)提高我們的編碼能力哑舒,加深對(duì)于一些通用編程概念/思想的理解,包括空安全幻馁、值類型洗鸵、協(xié)程、不共享數(shù)據(jù)的Actor并發(fā)模型仗嗦、函數(shù)式編程膘滨、面向協(xié)議編程、內(nèi)存所有權(quán)
等稀拐。對(duì)于新的現(xiàn)代編程語言例如Swift
/Dart
/TS
/Kotlin
/Rust
等火邓,很多特性/思想都是相互借鑒,當(dāng)我們理解這些概念/思想以后對(duì)于理解其他語言也會(huì)更容易德撬。