一、前言
京喜APP最早在2019年引入了Swift,使用Swift完成了第一個訂單模塊的開發(fā)。之后一年多我們持續(xù)在團隊/公司內(nèi)部推廣和普及Swift,目前Swift已經(jīng)支撐了70%+以上的業(yè)務(wù)。通過使用Swift提高了團隊內(nèi)同學的開發(fā)效率,同時也帶來了質(zhì)量的提升,目前來自Swift的Crash的占比不到1%。在這過程中不斷的學習/實踐,團隊內(nèi)的Code Review,也對如何使用Swift來提高代碼質(zhì)量有更深的理解。 二、Swift特性
在討論如何使用Swift提高代碼質(zhì)量之前,我們先來看看Swift本身相比ObjC或其他編程語言有什么優(yōu)勢。Swift有三個重要的特性分別是富有表現(xiàn)力/安全性/快速,接下來我們分別從這三個特性簡單介紹一下:
富有表現(xiàn)力
Swift提供更多的編程范式和特性支持,可以編寫更少的代碼,而且易于閱讀和維護。
基礎(chǔ)類型- 元組、Enum關(guān)聯(lián)類型
方法-方法重載
protocol- 不限制只支持class、協(xié)議默認實現(xiàn)、類專屬協(xié)議
泛型-protocol關(guān)聯(lián)類型、where實現(xiàn)類型約束、泛型擴展
可選值- 可選值申明、可選鏈、隱式可選值
屬性- let、lazy、計算屬性`、willset/didset、Property Wrappers
函數(shù)式編程- 集合filter/map/reduce方法,提供更多標準庫方法
標準庫框架-Combine響應(yīng)式框架、SwiftUI申明式UI框架、CodableJSON模型轉(zhuǎn)換
Result builder- 描述實現(xiàn)DSL的能力
動態(tài)性- dynamicCallable、dynamicMemberLookup
其他- 擴展、subscript、操作符重寫、嵌套類型、區(qū)間
Swift Package Manager- 基于Swift的包管理工具,可以直接用Xcode進行管理更方便
struct- 初始化方法自動補齊
類型推斷- 通過編譯器強大的類型推斷編寫代碼時可以減少很多類型申明
提示:類型推斷同時也會增加一定的編譯耗時,不過Swift團隊也在不斷的改善編譯速度。
安全性
代碼安全
let屬性- 使用let申明常量避免被修改。
值類型- 值類型可以避免在方法調(diào)用等參數(shù)傳遞過程中狀態(tài)被修改。
訪問控制- 通過public和final限制模塊外使用class不能被繼承和重寫。
強制異常處理- 方法需要拋出異常時,需要申明為throw方法。當調(diào)用可能會throw異常的方法,需要強制捕獲異常避免將異常暴露到上層。
模式匹配- 通過模式匹配檢測switch中未處理的case。
類型安全
強制類型轉(zhuǎn)換- 禁止隱式類型轉(zhuǎn)換避免轉(zhuǎn)換中帶來的異常問題。同時類型轉(zhuǎn)換不會帶來額外的運行時消耗。
提示:編寫ObjC代碼時,我們通常會在編碼時添加類型檢查避免運行時崩潰導致Crash。
KeyPath - KeyPath相比使用字符串可以提供屬性名和類型信息,可以利用編譯器檢查。
泛型- 提供泛型和協(xié)議關(guān)聯(lián)類型,可以編寫出類型安全的代碼。相比Any可以更多利用編譯時檢查發(fā)現(xiàn)類型問題。
Enum關(guān)聯(lián)類型- 通過給特定枚舉指定類型避免使用Any。
內(nèi)存安全
空安全- 通過標識可選值避免空指針帶來的異常問題
ARC- 使用自動內(nèi)存管理避免手動管理內(nèi)存帶來的各種內(nèi)存問題
強制初始化- 變量使用前必須初始化
內(nèi)存獨占訪問- 通過編譯器檢查發(fā)現(xiàn)潛在的內(nèi)存沖突問題
線程安全
值類型- 更多使用值類型減少在多線程中遇到的數(shù)據(jù)競爭問題
async/await - 提供async函數(shù)使我們可以用結(jié)構(gòu)化的方式編寫并發(fā)操作。避免基于閉包的異步方式帶來的內(nèi)存循環(huán)引用和無法拋出異常的問題
Actor- 提供Actor模型避免多線程開發(fā)中進行數(shù)據(jù)共享時發(fā)生的數(shù)據(jù)競爭問題,同時避免在使用鎖時帶來的死鎖等問題
快速
值類型- 相比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)化移除未使用的類型/方法等減少二進制文件大小。使用靜態(tài)派發(fā)/方法內(nèi)聯(lián)優(yōu)化/泛型特化/寫時復制等優(yōu)化提高運行時性能
提示:ObjC消息派發(fā)會導致編譯器無法進行移除無用方法/類的優(yōu)化,編譯器并不知道是否可能被用到。
ARC優(yōu)化- 雖然和ObjC一樣都是使用ARC,Swift通過編譯器優(yōu)化,可以進行更快的內(nèi)存回收和更少的內(nèi)存引用計數(shù)管理
提示:相比ObjC,Swift內(nèi)部不需要使用autorelease進行管理。
三、代碼質(zhì)量指標
以上是一些常見的代碼質(zhì)量指標。我們的目標是如何更好的使用Swift編寫出符合代碼質(zhì)量指標要求的代碼。
提示:本文不涉及設(shè)計模式/架構(gòu),更多關(guān)注如何通過合理使用Swift特性做局部代碼段的重構(gòu)。
一些不錯的實踐
利用編譯檢查
減少使用Any/AnyObject
因為Any/AnyObject缺少明確的類型信息,編譯器無法進行類型檢查,會帶來一些問題:
編譯器無法檢查類型是否正確保證類型安全
代碼中大量的as?轉(zhuǎn)換
類型的缺失導致編譯器無法做一些潛在的編譯優(yōu)化
使用as?帶來的問題
當使用Any/AnyObject時會頻繁使用as?進行類型轉(zhuǎn)換。這好像沒什么問題因為使用as?并不會導致程序Crash。不過代碼錯誤至少應(yīng)該分為兩類,一類是程序本身的錯誤通常會引發(fā)Crash,另外一種是業(yè)務(wù)邏輯錯誤。使用as?只是避免了程序錯誤Crash,但是并不能防止業(yè)務(wù)邏輯錯誤。
func do(data: Any?) { guard let string = data as? String else { return } // } do(1) do("")
以上面的例子為例,我們進行了as?轉(zhuǎn)換,當data為String時才會進行處理。但是當do方法內(nèi)String類型發(fā)生了改變函數(shù),使用方并不知道已變更沒有做相應(yīng)的適配,這時候就會造成業(yè)務(wù)邏輯的錯誤。
提示:這類錯誤通常更難發(fā)現(xiàn),這也是我們在一次真實bug場景遇到的。
使用自定義類型代替Dictionary
代碼中大量Dictionary數(shù)據(jù)結(jié)構(gòu)會降低代碼可維護性,同時帶來潛在的bug:
key需要字符串硬編碼,編譯時無法檢查
value沒有類型限制。修改時類型無法限制,讀取時需要重復類型轉(zhuǎn)換和解包操作
無法利用空安全特性,指定某個屬性必須有值
提示:自定義類型還有個好處,例如JSON轉(zhuǎn)自定義類型時會進行類型/nil/屬性名檢查,可以避免將錯誤數(shù)據(jù)丟到下一層。
不推薦
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的場景
數(shù)據(jù)不使用- 數(shù)據(jù)并不讀取只是用來傳遞。
解耦- 1.組件間通信解耦使用HashMap傳遞參數(shù)進行通信。2.跨技術(shù)棧邊界的場景,混合棧間通信/前后端通信使用HashMap/JSON進行通信。
使用枚舉關(guān)聯(lián)值代替Any
例如使用枚舉改造NSAttributedStringAPI,原有APIvalue為Any類型無法限制特定的類型。
優(yōu)化前
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會報錯
使用泛型/協(xié)議關(guān)聯(lián)類型代替Any
使用泛型或協(xié)議關(guān)聯(lián)類型代替Any,通過泛型類型約束來使編譯器進行更多的類型檢查。
使用枚舉/常量代替硬編碼
代碼中存在重復的硬編碼字符串/數(shù)字,在修改時可能會因為不同步引發(fā)bug。盡可能減少硬編碼字符串/數(shù)字,使用枚舉或常量代替。
使用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 }
內(nèi)存安全
減少使用!屬性
!屬性會在讀取時隱式強解包,當值不存在時產(chǎn)生運行時異常導致Crash。
class ViewController: UIViewController { @IBOutlet private var label: UILabel! // @IBOutlet需要使用! }
減少使用!進行強解包
使用!強解包會在值不存在時產(chǎn)生運行時異常導致Crash。
var num: Int? let num2 = num! // 錯誤
提示:建議只在小范圍的局部代碼段使用!強解包。
避免使用try!進行錯誤處理
使用try!會在方法拋出異常時產(chǎn)生運行時異常導致Crash。
try! method()
使用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) }
減少使用unowned
unowned在值不存在時會產(chǎn)生運行時異常導致Crash,只有在確定self一定會存在時才使用unowned。
class Class { @objc unowned var object: Object @objc weak var object: Object? }
unowned/weak區(qū)別:
weak - 必須設(shè)置為可選值,會進行弱引用處理性能更差。會自動設(shè)置為nil
unowned - 可以不設(shè)置為可選值,不會進行弱引用處理性能更好。但是不會自動設(shè)置為nil, 如果self已釋放會觸發(fā)錯誤.
錯誤處理方式
可選值- 調(diào)用方并不關(guān)注內(nèi)部可能會發(fā)生錯誤,當發(fā)生錯誤時返回nil
try/catch- 明確提示調(diào)用方需要處理異常,需要實現(xiàn)Error協(xié)議定義明確的錯誤類型
assert - 斷言。只能在Debug模式下生效
precondition- 和assert類似,可以再Debug/Release模式下生效
fatalError- 產(chǎn)生運行時崩潰會導致Crash,應(yīng)避免使用
Result- 通常用于閉包異步回調(diào)返回值
減少使用可選值
可選值的價值在于通過明確標識值可能會為nil并且編譯器強制對值進行nil判斷。但是不應(yīng)該隨意的定義可選值,可選值不能用let定義,并且使用時必須進行解包操作相對比較繁瑣。在代碼設(shè)計時應(yīng)考慮這個值是否有可能為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時進行異常處理。有時候我們會通過給予可選值默認值的方式來處理。但是這里應(yīng)考慮在什么場景下可以給予默認值。在不能給予默認值的場景應(yīng)當及時使用return或拋出異常,避免錯誤的值被傳遞到更多的業(yè)務(wù)流程。
不推薦
func confirmOrder(id: String) {} // 給予錯誤的值會導致錯誤的值被傳遞到更多的業(yè)務(wù)流程 confirmOrder(id: orderId ?? "")
推薦
func confirmOrder(id: String) {} guard let orderId = orderId else { // 異常處理 return } confirmOrder(id: orderId)
提示:通常強業(yè)務(wù)相關(guān)的值不能給予默認值:例如商品/訂單id或是價格。在可以使用兜底邏輯的場景使用默認值,例如默認文字/文字顏色。
使用枚舉優(yōu)化可選值
Object結(jié)構(gòu)同時只會有一個值存在:
優(yōu)化前
class Object { var name: Int? var num: Int? }
優(yōu)化后
降低內(nèi)存占用-枚舉關(guān)聯(lián)類型的大小取決于最大的關(guān)聯(lián)類型大小
邏輯更清晰- 使用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循環(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"]
推薦
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優(yōu)化循環(huán)
for循環(huán)添加where語句,只有當where條件滿足時才會進入循環(huán)
不推薦
for item in collection { if item.hasProperty { // ... } }
推薦
for item in collection where item.hasProperty { // item.hasProperty == true,才會進入循環(huán) }
使用defer
defer可以保證在函數(shù)退出前一定會執(zhí)行??梢允褂胐efer中實現(xiàn)退出時一定會執(zhí)行的操作例如資源釋放等避免遺漏。
func method() { lock.lock() defer { lock.unlock() // 會在method作用域結(jié)束的時候調(diào)用 } // do }
字符串
使用"""
在定義復雜字符串時,使用多行字符串字面量可以保持原有字符串的換行符號/引號等特殊字符,不需要使用進行轉(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." """
提示:上面字符串中的""和換行可以自動保留。
使用字符串插值
使用字符串插值可以提高代碼可讀性。
不推薦
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)"
集合
使用標準庫提供的高階函數(shù)
不推薦
var nums = [] nums.count == 0 nums[0]
推薦
var nums = [] nums.isEmpty nums.first
訪問控制
Swift中默認訪問控制級別為internal。編碼中應(yīng)當盡可能減小屬性/方法/類型的訪問控制級別隱藏內(nèi)部實現(xiàn)。
提示:同時也有利于編譯器進行優(yōu)化。
使用private/fileprivate修飾私有屬性和方法
private let num = 1 class MyClass { private var num: Int }
使用private(set)修飾外部只讀/內(nèi)部可讀寫屬性
class MyClass { private(set) var num = 1 } let num = MyClass().num MyClass().num = 2 // 會編譯報錯
函數(shù)
使用參數(shù)默認值
使用參數(shù)默認值,可以使調(diào)用方傳遞更少的參數(shù)。
不推薦
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,參數(shù)默認值也可以讓我們定義更少的方法。
限制參數(shù)數(shù)量
當方法參數(shù)過多時考慮使用自定義類型代替。
不推薦
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提示。
// 上報方法使用方不關(guān)心是否成功 func report(id: String) -> Bool {} @discardableResult func report2(id: String) -> Bool {} report("1") // 編譯器會警告 report2("1") // 不處理返回值編譯器不會警告
元組
避免過長的元組
元組雖然具有類型信息,但是并不包含變量名信息,使用方并不清晰知道變量的含義。所以當元組數(shù)量過多時考慮使用自定義類型代替。
func test() -> (Int, Int, Int) { } let (a, b, c) = test() // a,b,c類型一致,沒有命名信息不清楚每個變量的含義
系統(tǒng)庫
KVO/Notification使用blockAPI
block API的優(yōu)勢:
KVO可以支持KeyPath
不需要主動移除監(jiān)聽,observer釋放時自動移除監(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) } }
推薦
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提供了很多新特性,例如默認實現(xiàn),關(guān)聯(lián)類型,支持值類型。在代碼設(shè)計時可以優(yōu)先考慮使用protocol來避免臃腫的父類同時更多使用值類型。
提示:一些無法用protocol替代繼承的場景:1.需要繼承NSObject子類。2.需要調(diào)用super方法。3.實現(xiàn)抽象類的能力。
Extension
使用extension組織代碼
使用extension將私有方法/父類方法/協(xié)議方法等不同功能代碼進行分離更加清晰/易維護。
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 }
代碼風格
良好的代碼風格可以提高代碼的可讀性,統(tǒng)一的代碼風格可以降低團隊內(nèi)相互理解成本。對于Swift的代碼格式化建議使用自動格式化工具實現(xiàn),將自動格式化添加到代碼提交流程,通過定義Lint規(guī)則統(tǒng)一團隊內(nèi)代碼風格??紤]使用SwiftFormat和SwiftLint。
提示:SwiftFormat主要關(guān)注代碼樣式的格式化,SwiftLint可以使用autocorrect自動修復部分不規(guī)范的代碼。
常見的自動格式化修正
移除多余的;
最多只保留一行換行
自動對齊空格
限制每行的寬度自動換行
性能優(yōu)化
性能優(yōu)化上主要關(guān)注提高運行時性能和降低二進制體積。需要考慮如何更好的使用Swift特性,同時提供更多信息給編譯器進行優(yōu)化。
使用Whole Module Optimization
當Xcode開啟WMO優(yōu)化時,編譯器可以將整個程序編譯為一個文件進行更多的優(yōu)化。例如通過推斷final/函數(shù)內(nèi)聯(lián)/泛型特化更多使用靜態(tài)派發(fā),并且可以移除部分未使用的代碼。
使用源代碼打包
當我們使用組件化時,為了提高編譯速度和打包效率,通常單個組件獨立編譯生成靜態(tài)庫,最后多個組件直接使用靜態(tài)庫進行打包。這種場景下WMO僅針對internal以內(nèi)作用域生效,對于public/open缺少外部使用信息所以無法進行優(yōu)化。所以對于大量使用Swift的項目,使用全量代碼打包更有利于編譯器做更多優(yōu)化。
減少方法動態(tài)派發(fā)
使用final-class/方法/屬性申明為final,編譯器可以優(yōu)化為靜態(tài)派發(fā)
使用private-方法/屬性申明為private,編譯器可以優(yōu)化為靜態(tài)派發(fā)
避免使用dynamic-dynamic會使方法通過ObjC消息轉(zhuǎn)發(fā)的方式派發(fā)
使用WMO- 編譯器可以自動分析推斷出final優(yōu)化為靜態(tài)派發(fā)
使用Slice共享內(nèi)存優(yōu)化性能
在使用Array/String時,可以使用Slice切片獲取一部分數(shù)據(jù)。Slice保存對原始Array/String的引用共享內(nèi)存數(shù)據(jù),不需要重新分配空間進行存儲。
let midpoint = absences.count / 2 let firstHalf = absences[..
提示:應(yīng)避免一直持有Slice,Slice會延長原始Array/String的生命周期導致無法被釋放造成內(nèi)存泄漏。
protocol添加AnyObject
protocol AnyProtocol {} protocol ObjectProtocol: AnyObject {}
當protocol僅限制為class使用時,繼承AnyObject協(xié)議可以使編譯器不需要考慮值類型實現(xiàn),提高運行時性能。
使用@inlinable進行方法內(nèi)聯(lián)優(yōu)化
// 原始代碼 let label = UILabel().then { $0.textAlignment = .center $0.textColor = UIColor.black $0.text = "Hello, World!" }
以then庫為例,他使用閉包進行對象初始化以后的相關(guān)設(shè)置。但是 then 方法以及閉包也會帶來額外的性能消耗。
內(nèi)聯(lián)優(yōu)化
@inlinable public func then(_ block: (Self) throws -> Void) rethrows -> Self { try block(self) return self } // 編譯器內(nèi)聯(lián)優(yōu)化后 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對象的內(nèi)存大小。同時會增加包大小,因為需要為屬性生成相關(guān)的信息??梢钥紤]使用文件級private let申明或static常量代替。
不推薦
class Object { private let title = "12345" }
推薦
private let title = "12345" class Object { static let title = "" }
提示:這里并不包括通過init初始化注入的屬性。
使用didSet/willSet時進行Diff
某些場景需要使用didSet/willSet屬性檢查器監(jiān)控屬性變化,做一些額外的計算。但是由于didSet/willSet并不會檢查新/舊值是否相同,可以考慮添加新/舊值判斷,只有當值真的改變時才進行運算提高性能。
優(yōu)化前
class Object { var orderId: String? { didSet { // 拉取接口等操作 } } }
例如上面的例子,當每一次orderId變更時需要重新拉取當前訂單的數(shù)據(jù),但是當orderId值一樣時,拉取訂單數(shù)據(jù)是無效執(zhí)行。
優(yōu)化后
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,可以將數(shù)組運算操作推遲到第一次使用時,避免一次性全部計算。
提示:例如長列表,我們需要創(chuàng)建每個cell對應(yīng)的視圖模型,一次性創(chuàng)建太耗費時間。
?
使用合適的集合方法優(yōu)化性能
不推薦
var items = [1, 2, 3] items.filter({ $0 > 1 }).first // 查找出所有大于1的元素,之后找出第一個
推薦
var items = [1, 2, 3] items.first(where: { $0 > 1 }) // 查找出第一個大于1的元素直接返回
使用值類型
Swift中的值類型主要是結(jié)構(gòu)體/枚舉/元組。
啟動性能- APP啟動時值類型沒有額外的消耗,class有一定額外的消耗。
運行時性能- 值類型不需要在堆上分配空間/額外的引用計數(shù)管理。更少的內(nèi)存占用和更快的性能。
包大小- 相比class,值類型不需要創(chuàng)建ObjC類對應(yīng)的ro_data_t數(shù)據(jù)結(jié)構(gòu)。
提示:class即使沒有繼承NSObject也會生成ro_data_t,里面包含了ivars屬性信息。如果屬性/方法申明為@objc還會生成對應(yīng)的方法列表。
提示:struct無法代替class的一些場景:1.需要使用繼承調(diào)用super。2.需要使用引用類型。3.需要使用deinit。4.需要在運行時動態(tài)轉(zhuǎn)換一個實例的類型。
提示:不是所有struct都會保存在棧上,部分數(shù)據(jù)大的struct也會保存在堆上。
集合元素使用值類型
集合元素使用值類型。因為NSArray并不支持值類型,編譯器不需要處理可能需要橋接到NSArray的場景,可以移除部分消耗。
純靜態(tài)類型避免使用class
當class只包含靜態(tài)方法/屬性時,考慮使用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方法。
值類型性能優(yōu)化
考慮使用引用類型
值類型為了維持值語義,會在每次賦值/參數(shù)傳遞/修改時進行復制。雖然編譯器本身會做一些優(yōu)化,例如寫時復制優(yōu)化,在修改時減少復制頻率,但是這僅針對于標準庫提供的集合和String結(jié)構(gòu)有效,對于自定義結(jié)構(gòu)需要自己實現(xiàn)。對于參數(shù)傳遞編譯器在一些場景會優(yōu)化為直接傳遞引用的方式避免復制行為。
但是對于一些數(shù)據(jù)特別大的結(jié)構(gòu),同時需要頻繁變更修改時也可以考慮使用引用類型實現(xiàn)。
使用inout傳遞參數(shù)減少復制
雖然編譯器本身會進行寫時復制的優(yōu)化,但是部分場景編譯器無法處理。
不推薦
func append_one(_ a: [Int]) -> [Int] { var a = a a.append(1) // 無法被編譯器優(yōu)化,因為這時候有2個引用持有數(shù)組 return a } var a = [1, 2, 3] a = append_one(a)
推薦
直接使用inout傳遞參數(shù)
func append_one_in_place(a: inout [Int]) { a.append(1) } var a = [1, 2, 3] append_one_in_place(&a)
使用isKnownUniquelyReferenced實現(xiàn)寫時復制
默認情況下結(jié)構(gòu)體中包含引用類型,在修改時只會重新拷貝引用。但是我們希望CustomData具備值類型的特性,所以當修改時需要重新復制NSMutableData避免復用。但是復制操作本身是耗時操作,我們希望可以減少一些不必要的復制。
優(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)用都會復制 }
優(yōu)化后
使用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) // 只會在第一次調(diào)用時進行復制 }
提示:對于ObjC類型isKnownUniquelyReferenced會直接返回false。
減少使用Objc特性
避免使用Objc類型
盡可能避免在Swift中使用NSString/NSArray/NSDictionary等ObjC基礎(chǔ)類型。以Dictionary為例,雖然Swift Runtime可以在NSArray和Array之間進行隱式橋接需要O(1)的時間。但是字典當Key和Value既不是類也不是@objc協(xié)議時,需要對每個值進行橋接,可能會導致消耗O(n)時間。
減少添加@objc標識
@objc標識雖然不會強制使用消息轉(zhuǎn)發(fā)的方式來調(diào)用方法/屬性,但是他會默認ObjC是可見的會生成和ObjC一樣的ro_data_t結(jié)構(gòu)。
避免使用@objcMembers
使用@objcMembers修飾的類,默認會為類/屬性/方法/擴展都加上@objc標識。
@objcMembers class Object: NSObject { }
提示:你也可以使用@nonobjc取消支持ObjC。
避免繼承NSObject
你只需要在需要使用NSObject特性時才需要繼承,例如需要實現(xiàn)UITableViewDataSource相關(guān)協(xié)議。
使用let變量/屬性
優(yōu)化集合創(chuàng)建
集合不需要修改時,使用let修飾,編譯器會優(yōu)化創(chuàng)建集合的性能。例如針對let集合,編譯器在創(chuàng)建時可以分配更小的內(nèi)存大小。
優(yōu)化逃逸閉包
在Swift中,當捕獲var變量時編譯器需要生成一個在堆上的Box保存變量用于之后對于變量的讀/寫,同時需要額外的內(nèi)存管理操作。如果是let變量,編譯器可以保存值復制或引用,避免使用Box。
避免使用大型struct使用class代替
大型struct通常是指屬性特別多并且嵌套類型很多。目前swift編譯器針對struct等值類型編譯優(yōu)化處理的并不好,會生成大量的assignWithCopy、assignWithCopy等copy相關(guān)方法,生成大量的二進制代碼。使用class類型可以避免生成相關(guān)的copy方法。
提示:不要小看這部分二進制的影響,個人在日常項目中遇到過復雜的大型struct能生成幾百KB的二進制代碼。但是目前并沒有好的方法去發(fā)現(xiàn)這類struct去做優(yōu)化,只能通過相關(guān)工具去查看生成的二進制詳細信息。希望官方可以早點優(yōu)化。
優(yōu)先使用Encodable/Decodable協(xié)議代替Codable
因為實現(xiàn)Encodable和Decodable協(xié)議的結(jié)構(gòu),編譯器在編譯時會自動生成對應(yīng)的init(from decoder: Decoder)和encode(to: Encoder)方法。Codable同時實現(xiàn)了Encodable和Decodable協(xié)議,但是大部分場景下我們只需要encode或decode能力,所以明確指定實現(xiàn)Encodable或Decodable協(xié)議可以減少生成對應(yīng)的方法減少包體積。
提示:對于屬性比較多的類型結(jié)構(gòu)會產(chǎn)生很大的二進制代碼,有興趣可以用相關(guān)的工具看看生成的二進制文件。
減少使用Equatable協(xié)議
因為實現(xiàn)Equatable協(xié)議的結(jié)構(gòu),編譯器在編譯時會自動生成對應(yīng)的equal方法。默認實現(xiàn)是針對所有字段進行比較會生成大量的代碼。所以當我們不需要實現(xiàn)==比較能力時不要實現(xiàn)Equatable或者對于屬性特別多的類型也可以考慮重寫Equatable協(xié)議,只針對部分屬性進行比較,這樣可以生成更少的代碼減少包體積。
提示:對于屬性特別多的類型也可以考慮重寫Equatable協(xié)議,只針對部分屬性進行比較,同時也可以提升性能。
四、總結(jié)
個人從Swift3.0開始將Swift作為第一語言使用。編寫Swift代碼并不只是簡單對于ObjC代碼的翻譯/重寫,需要對于Swift特性更多的理解才能更好的利用這些特性帶來更多的收益。同時我們需要關(guān)注每個版本Swift的優(yōu)化/改進和新特性。在這過程中也會提高我們的編碼能力,加深對于一些通用編程概念/思想的理解,包括空安全、值類型、協(xié)程、不共享數(shù)據(jù)的Actor并發(fā)模型、函數(shù)式編程、面向協(xié)議編程、內(nèi)存所有權(quán)等。對于新的現(xiàn)代編程語言例如Swift/Dart/TS/Kotlin/Rust等,很多特性/思想都是相互借鑒,當我們理解這些概念/思想以后對于理解其他語言也會更容易。
審核編輯:劉清
-
DSL
+關(guān)注
關(guān)注
2文章
58瀏覽量
38293 -
SWIFT
+關(guān)注
關(guān)注
0文章
116瀏覽量
23798 -
ARC
+關(guān)注
關(guān)注
0文章
42瀏覽量
16468 -
編譯器
+關(guān)注
關(guān)注
1文章
1623瀏覽量
49108
原文標題:萬字長文詳解如何使用 Swift 提高代碼質(zhì)量
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論