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申明常量避免被修改。值类型- 值类型可以避免在方法调用等参数传递过程中状态被修改。访问控制- 通过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编写出符合代码质量指标要求的代码。
一些不错的实践
-
利用编译检查
-
减少使用
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例如使用枚举改造
NSAttributedStringAPI,原有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已释放会触发错误.
-
-
-
错误处理方式
可选值- 调用方并不关注内部可能会发生错误,当发生错误时返回niltry/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,才会进入循环 }
-
使用
deferdefer可以保证在函数退出前一定会执行。可以使用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
-
blockAPI的优势: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[..<midpoint] let secondHalf = absences[midpoint...] // firstHalf/secondHalf并不会复制和占用更多内存提示:应
避免一直持有Slice,Slice会延长原始Array/String的生命周期导致无法被释放造成内存泄漏。 -
protocol添加AnyObjectprotocol 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常量代替。提示:这里并不包括通过init初始化注入的属性。
-
不推荐
class Object { private let title = "12345" } -
推荐
private let title = "12345" class Object { static let title = "" }
-
-
使用
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的场景,可以移除部分消耗。
-
纯静态类型避免使用
当classclass只包含静态方法/属性时,考虑使用enum代替class,因为class会生成更多的二进制代码。提示:为什么用
enum而不是struct,因为struct会额外生成init方法。-
不推荐
class Object { static var num: Int static func test() {} } -
推荐
enum Object { static var num: Int static func test() {} }
-
- 值类型性能优化
-
考虑使用引用类型
值类型为了维持
值语义,会在每次赋值/参数传递/修改时进行复制。虽然编译器本身会做一些优化,例如写时复制优化,在修改时减少复制频率,但是这仅针对于标准库提供的集合和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<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) // 只会在第一次调用时进行复制 }
提示:对于
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。
-
总结
个人从Swift3.0开始将Swift作为第一语言使用。编写Swift代码并不只是简单对于ObjC代码的翻译/重写,需要对于Swift特性更多的理解才能更好的利用这些特性带来更多的收益。同时我们需要关注每个版本Swift的优化/改进和新特性。在这过程中也会提高我们的编码能力,加深对于一些通用编程概念/思想的理解,包括空安全、值类型、协程、不共享数据的Actor并发模型、函数式编程、面向协议编程、内存所有权等。对于新的现代编程语言例如Swift/Dart/TS/Kotlin/Rust等,很多特性/思想都是相互借鉴,当我们理解这些概念/思想以后对于理解其他语言也会更容易。
这里推荐有兴趣可以关注Swift Evolution,每个特性加入都会有一个提案,里面会详细介绍动机/使用场景/实现方式/未来方向。
拓展资料
参考资料
浙公网安备 33010602011771号