关于swift:万字长文详解如何使用Swift提高代码质量-京东云技术团队

70次阅读

共计 15959 个字符,预计需要花费 40 分钟才能阅读完成。

前言

京喜 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`

正文完
 0