萬字長文詳解如何使用Swift提高代碼質量

来源:https://www.cnblogs.com/Jcloud/archive/2023/05/10/17387349.html
-Advertisement-
Play Games

京喜APP最早在2019年引入了Swift,使用Swift完成了第一個訂單模塊的開發。之後一年多我們持續在團隊/公司內部推廣和普及Swift,目前Swift已經支撐了70%+以上的業務。通過使用Swift提高了團隊內同學的開發效率,同時也帶來了質量的提升,目前來自Swift的Crash的占比不到1%... ...


前言

京喜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框架、CodableJSON模型轉換
  • Result builder - 描述實現DSL的能力
  • 動態性 - dynamicCallable、dynamicMemberLookup
  • 其他 - 擴展、subscript、操作符重寫、嵌套類型、區間
  • Swift Package Manager - 基於Swift的包管理工具,可以直接用Xcode進行管理更方便
  • struct - 初始化方法自動補齊
  • 類型推斷 - 通過編譯器強大的類型推斷編寫代碼時可以減少很多類型申明

提示:類型推斷同時也會增加一定的編譯耗時,不過Swift團隊也在不斷的改善編譯速度。

安全性

代碼安全

  • let屬性 - 使用let申明常量避免被修改。
  • 值類型 - 值類型可以避免在方法調用等參數傳遞過程中狀態被修改。
  • 訪問控制 - 通過publicfinal限制模塊外使用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一樣都是使用ARCSwift通過編譯器優化,可以進行更快的記憶體回收和更少的記憶體引用計數管理

提示: 相比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?轉換,當dataString時才會進行處理。但是當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

例如使用枚舉改造NSAttributedStringAPI,原有APIvalueAny類型無法限制特定的類型。

優化前

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規則統一團隊內代碼風格。考慮使用SwiftFormatSwiftLint

提示: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`


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 開發環境 以下是我的開發環境 JDK 1.8 Maven 3.6.3 IDEA 2019(2019 無所畏懼,即使現在已經 2023 年了哈哈哈) 使用 Maven 的方式創建 Spring Boot 項目 下麵的內容可能會因 IDEA 版本不同,而有些選項不同,但是大同小異。 1. 打開 IDEA ...
  • 大家是否見過這種for迴圈,在for迴圈前加了個標記的: outerLoop: for (; ; ) { for (; ; ) { break outerLoop; } } 我之前有一次在公司業務代碼中見過有這種寫法的,沒在意,今天在看JDK線程池的代碼時,又看到ThreadPoolExecutor ...
  • 大家好,3y啊。好些天沒更新了,並沒有偷懶,只不過一直在安裝環境,差點都想放棄了。 上一次比較大的更新是做了austin的預覽地址,把企業微信的應用和機器人消息各種的消息類型和功能給完善了。上一篇文章也提到了,austin常規的功能已經更新得差不多了,剩下的就是各種細節的完善。 不知道大家還記不記得 ...
  • 上一篇咱們介紹了 Hibernate 以及寫了一個 Hibernate 的工具類,快速入門體驗了一波 Hibernate 的使用,我們只需通過 Session 對象就能實現資料庫的操作了。 現在,這篇介紹使用 Hibernate 進行基本的 CRUD、懶載入以及緩存的知識。 ...
  • 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇對 VLD 2.5.1 源碼做記憶體泄漏檢測的思路進行剖析。 ...
  • 基於java的學生課程管理系統,基於java的學生選課系統,javaWeb的學生選課系統,學生成績管理系統,課表管理系統,學院管理系統,大學生選課系統設計與實現,網上選課系統,課程成績打分。 ...
  • 本文設計並實現了一種專用於路徑路由匹配的規則,以一種簡單而通用的方式描述一組路徑的特征,來簡化這種場景路由描述難度,讓小白可以快速學習並上手。 ...
  • pandas的數據檢索功能是其最基礎也是最重要的功能之一。 pandas中最常用的幾種數據過濾方式如下: 行列過濾:選取指定的行或者列 條件過濾:對列的數據設置過濾條件 函數過濾:通過函數設置更加複雜的過濾條件 本篇所有示例所使用的測試數據如下: import pandas as pd import ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...