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

前言

京喜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申明常量避免被修改。
  • 值类型 - 值类型可以避免在方法调用等参数传递过程中状态被修改。
  • 访问控制 - 通过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编写出符合代码质量指标要求的代码。

提示:本文不涉及设计模式/架构,更多关注如何通过合理使用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已释放会触发错误.

错误处理方式

  • 可选值 - 调用方并不关注内部可能会发生错误,当发生错误时返回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规则统一团队内代码风格。考虑使用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[..


`提示:应避免一直持有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`

相关推荐

业务复杂度治理方法论--十年系统设计经验总结

软件设计的核心在于降低复杂性。--《软件设计的哲学》业界对于复杂度并没有统一的定义,斯坦福教授John Ousterhout从认知负担和工作量方面给出了一个复杂度量公式子模块的复杂度cp乘以该模块对应的开发时间权重值tp,累加后得到系统的整体复杂度C这里的子模块复杂度cp是一个经验值需要注意:如果一个子系统特别复杂,但是很少使用及修改,也不会对整体复杂度造成太大影响。例:spring框架内部代码较为复杂,但由于几乎不需要我们去变动,所以对系统的整体复杂度影响并不大。

JDDTechTalk的博客 2444

【转载】golang内存分配

在 1.10 以前 go 的堆地址空间是线性连续扩展的, 比如在 1.10(linux amd64)中, 最大可扩展到 512GB. 因为 go 在 gc 的时候会根据拿到的指针地址来判断是否位于 go 的 heap 的, 以及找到其对应的 span, 其判断机制需要 gc heap 是连续的. 但是连续扩展有个问题, cgo 中的代码(尤其是 32 位系统上)可能会占用未来会用于 go heap 的内存. 这样在扩展 go heap 时, mmap 出现不连续的地址, 导致运行时 throw.

JDDTechTalk的博客 1485

自增主键去哪了?---一次开发过程中的思考

MySQL是作为大家都经常接触的DB,相信大家都会有一定的认知,自增主键不连续大家肯定也遇到过,这次在联调过程中遇到这个情况,在跟别的小伙伴分享的时候,突然就想写一篇文章,文章里面如果有不正确或者不准确的地方也欢迎大家斧正~说实话,作为一名交易端的研发人员,业务开发任务压力真的蛮大的,我也迷茫过,如何在这个过程中成长,珍惜我们遇到的问题,将遇到的问题记录在册,深追问题,你会发现有很多问题其实真的是因为我们某些知识的薄弱点造成的。

JDDTechTalk的博客 1679

万字长文浅谈三高系统建设方法论和实践

整个软件的发展历程是一部软件复杂性对抗史,软件的复杂性分为技术复杂性和业务复杂性,业务复杂性主要是建模和抽象设计,技术复杂性主要是三高(高性能,高并发,高可用)的应对,C端的业务一般以技术复杂性为主,业务复杂性为辅,而B端或者M端的业务通常以业务复杂性为主,技术复杂性为辅。本篇文章主要是从后端研发的视角结合自己多年的B C端系统建设实践谈下三高系统的建设方法论和实践,希望和大家相互交流,共同进步,同时这是我参与创作者计划的第1篇文章。

JDDTechTalk的博客 2527

Vision Pro开发实践(结合24黑马idea)

利用ModelEntity加载模型,添加到RealityView的content中展示,这样可以对模型做各种操作,比如添加子模型,修改光照,材质等要素。举个例子来讲,我需要在运动鞋模型“sneaker”上面添加一个可以与用户交互的白色圆点模型,用于在用户点击的时候添加/展示对应部分的评价。这次结合的idea是我参加黑马比赛时想到的,主要内容是:在商品评价时增加商品的3D模型,用户可以在虚拟现实环境中查看商品的3D模型,并对模型的特定位置进行评论。:通过3D模型动画,用户可以直观地了解产品的特性和使用方法。

JDDTechTalk的博客 2174

买药秒送 JADE动态线程池实践及原理浅析

买药秒送是健康即时零售业务新的核心流量场域,面对京东首页高流量曝光,我们对频道页整个技术架构方案进行升级,保障接口高性能、系统高可用。动态线程池是买药频道应用的技术之一,我们通过3轮高保真压测最终初步确定了线程池的核心参数。但我们仍面临一些保障系统稳定性问题:如何监控线程池运行状态?以及因流量飙升出现任务堆积和拒绝时能否实时报警,线程池核心参数能否做到不重启应用,动态调整即时生效?经调研,业界成熟的动态线程池开源项目有dynamic-tp和hippo4j。

JDDTechTalk的博客 2051

从原理聊JVM(一):染色标记和垃圾回收算法

本篇介绍了JVM中垃圾回收器相关的基础知识,后续会深入介绍CMS、G1、ZGC等不同垃圾收集器的运作流程和原理,欢迎关注。。

JDDTechTalk的博客 1913

BFF层聚合查询服务异步改造及治理实践

本篇主要是思考如何依赖现有框架、环境的能力,从代码层面系统化的实现相关治理规范。最后仍引用王晓老师文章结尾来结束。

JDDTechTalk的博客 1922

QPS提升10倍的sql优化

总结本次优化经历•慢sql 往往是影响数据库性能的大瓶颈,sql写好了不但可以优化性能,还能节约机器成本,降本增效。•最好能看到sql语句执行的第一现场,本次主要是由于查看代码时没有及时注意到索引字段的传参类型不对这一细节,造成花了很多时间分析问题•虽然整个问题分析过程比较曲折,但问题分析的方向应该还是对的,过程中学到不少知识。•表结构的设计也有一些历史遗留原因,site_id 字段在表中定义为整型可能比较符合业务含义。表字段定义和业务含义一致,写sql也不容易犯错。

JDDTechTalk的博客 1793

一站式统一返回值封装、异常处理、异常错误码解决方案—最强的Sping Boot接口优雅响应处理器

Graceful Response内置了两种风格的响应格式,可以在文件中通过进行配置。•gr.responseStyle=0,或者不配置(默认情况)"msg": "有内鬼,终止交易"},"data": {•自定义响应格式如果以上两种格式均不能满足业务需要,可以通过自定义去满足,Response@Override@Override@Override@Overridereturn msg;注意,不需要返回的属性可以返回null或者加上注解。

JDDTechTalk的博客 2005

【原创】【深入浅出系列】之代码可读性

原创声明:该文章是个人在项目中亲历后的经验总结和分享,如有搬运需求请注明出处。这是“深入浅出系列”文章的第一篇,主要记录和分享程序设计的一些思想和方法论,如果读者觉得所有受用,还请“一键三连”,这是对我最大的鼓励。一句话:见名知其义。有人说好的代码必然有清晰完整的注释,我不否认;也有人说代码即注释,是代码简洁之道的最高境界,我也不否认。但我都不完全接受,如果照搬前者,有人会在每个方法、每个循环、每个判断都添加大量注释,对于一个表达不严谨的coder来说,代码与汉字可能词不达意;

JDDTechTalk的博客 1747

动态化-鸿蒙跨端方案介绍

动态化目前已经具备一份代码在iOS、Android、H5三端运行的能力,该方案就是将动态化一码多端能力对接到鸿蒙系统,降低首次适配研发投入;一次开发,四端共用,长期降低业务迭代成本;并且使业务具备随时发布能力。。

JDDTechTalk的博客 2626

【ChatGPT应用篇-1】助力Beauty代码的初体验

以下是一个与ChatGPT 结对编程的一个Excel数据解析案例,大家可以根据我的思考过程进行体验,以下放了一些截图,ChatGPT的反应速度惊人,代码版本一步步进行迭代优化,感触较深的是。对于ChatGPT输出的代码没有做仔细的Review及调试,但是看代码风格基本上是改改就能用于生产的,对于代码规范、命名还是很认可的!10.提升程序效率,把解析性能在提升10倍 (架构师的成本意识,这个问题的回答有点意思)6.替换掉 poi 采用EasyExcel (替换原始的默认技术选型,替换三方包)

JDDTechTalk的博客 1524

一文了解电商大促系统的高可用保障思路-献给技术伙伴们

电商大促是电商平台组织的一种大型销售推广活动,目的是通过提供各种优惠、折扣等方法,提高商品销售额和网站流量,增加消费者的购物欲望,以实现销售目标。电商大促活动通常会在一些特定的节点或者节日举行,比如“双11”、“618”、“黑色星期五”等,这些时期的电商大促极具吸引力,既有大量的商品打折优惠,又有丰富多样的活动供消费者参与,是电商平台提升销售业绩的重要手段。

JDDTechTalk的博客 2122

一起单测引起的项目加载失败惨案

是相对于静态编程而言的,平时我们讨论比较多的就是静态编程语言,例如Java,与动态编程语言,例如JavaScript。最近在开发一个功能模块时,在功能自测阶段,通过使用单测测试功能的完整性,在测试单测联通性使用到静态方法测试时,发现单测报错,通过查阅解决方案发现需要对。建议大家在构建Maven项目时,仔细检查POM文件中的依赖,确保没有漏掉任何必要的库,以免因为遗漏而引起不必要的问题。但是,在我们的POM文件中,虽然我们添加了对Spring框架的依赖,但是并没有添加。: 返回修改后的字节码。

JDDTechTalk的博客 1909

京东小程序折叠屏适配探索

应用在折叠屏运行时,可以从一个屏幕切换到另一个屏幕。应用应该做好配置变更的适配和界面状态的保存,以保证应用当前任务能无缝迁移到转换后的屏幕,从而为用户提供出色的连续性体验。折叠屏作为未来Android屏幕发展的新趋势,具有很大的发展前景,做好折叠屏相关适配支持也势在必行。小程序相关适配已经跟随京东主站、小家App、小家三星预装版等发布上线,本文是作者进行相关适配的一些心得体会,如有不足敬请见谅,欢迎交流探讨。

JDDTechTalk的博客 1万+

KubeCon China 2024全球大会在香港举行,京东云受邀参加探讨云原生、开源及 AI

和数字化大潮一样,在AI化的革命下,云端也在全面拥抱AI,并在方方面面变得更安全、更高效,让全球各行各业受益。大会首日吸引了来自全球 30 多个国家和地区的 1000 多名参会者,共同探讨云原生、开源及 AI 领域的前沿进展、核心技术和最佳实践。在京东云中,联邦Serverless服务基于联邦管理模型和Serverless应用模型,为联邦应用容器部署、弹性扩展和故障迁移提供JDOS应用容器控制服务。弹性扩缩容技术方案有很广大的应用前景,京东云内部也在进行大量的生产实践和验证。1. 多集群管理和分发实践。

JDDTechTalk的博客 1167

京东搜索重排:基于互信息的用户偏好导向模型

。

JDDTechTalk的博客 1939

探索AC自动机:多关键词搜索的原理与应用案例

目前,大多数自由文本搜索技术采用类似于Lucene的策略,通过解析搜索文本为各个组成部分来定位关键词。这种方法在处理少量关键词时表现良好。但当搜索的关键词数量达到10万个或更多时,这种方法的效率会显著下降,尤其是在需要与词典进行详尽对比的场景中。本文将介绍的Aho-Corasick(AC)自动机作为多模式匹配中的经典算法,不仅能够处理大规模文本数据,还能确保搜索过程的实时性和准确性。AC自动机可以被形象地比喻为一个超级找词机器。

JDDTechTalk的博客 1491

“前端”工匠系列(一):合格的工匠,究竟该搞什么

有效的交流是需要以有效的信息为载体的。在用户端日志查询、特殊边界场景复现、日志排查定位故障等场景,“实时”就不是必要的,这种场景下一般采用T+1查询,但是又引入了大量级日志的存储周期的话题,一般企业应用级的用户日志保存14天就完全够用了,因为对于C端日志来说,更多的是对现场故障的复现、处理及跟进,如果算法策略对用户日志有需要,只需要在一定时间内采用处理任务对用户日志进行一次处理,把输出的标签、行为特征等关键数据存储就可以,基础的用户日志还是应该被存档或清除释放资源供给更有价值的最新日志来使用。

JDDTechTalk的博客 1043

mysql磁盘碎片整理

不管是使用云还是商城数据库,只要使用mysql,必然会遇到Mysql碎片问题痛点,数据量大的业务表应该设计成分区表方便磁盘碎片整理,降低维护成本和业务影响。碎片清理前后,IO性能会上升,SQL执行效率更快。所以,在日常运维工作中,应对碎片进行定期清理,保证数据库有稳定的性能和充足的空间。扩展提到提高IO性能,在紧急情况下还可以考虑开启刷盘(设置),但开启刷盘会有数据丢失风险(集团数据库模板配置参数默认。

JDDTechTalk的博客 1105

【我在京东做产研】校招 2 年,个人角度(成长)回顾 - 行且知

回顾毕业后的工作历程(文章会以现实时间为顺序来进行回顾),有很多大佬的帮助和指点。因此,想将自己的思考也分享给后来的同学们文章从个人角度(成长)出发,回顾工作点滴。后续会从团队角度(互惠)出发,写成长历程二,敬请期待1.JD的技术体系,各种中间件的了解和使用▪各种中间件的使用文档,还是非常有价值的,各种各样的名词,使用细节。如果你都不知道,怎么细节性学习?▪切勿眼高手低:▪程序员都知道文档易读,但是不如手敲一遍。敲完运行运行,可能你就有新的疑问。(例如:JSF 配置后,如何生成代理对象的呀)

JDDTechTalk的博客 927

京东云《黑神话 · 悟空》现已支持快速部署!

黑神话:悟空》是一款以中国神话为背景的动作角色扮演游戏。本服务内置《黑神话:悟空》游戏、《Steam》客户端和远程桌面《GameViewer》软件,配备顶级游戏显卡,通过计算工厂快速部署后按下方文档操作即可开启流畅游戏体验。

JDDTechTalk的博客 1429

京东平台内容合规的技术与挑战

合规域在电商平台中扮演着重要的角色,它相当于电商体系中的“免疫系统”,通过识别恶意的商品、商家以及UGC内容,确保电商生态的健康、有序和可持续发展。因此我们部门致力于打造业内领先的内容理解与审核平台,在具体工作中,通过识别并治理商家在产品、价格、服务维度的违规行为以及短视频、直播、评价、问答等内容的违规信息,降低审核成本、提升运营效率,并最终保障用户体验。。

JDDTechTalk的博客 1102

视频审核架构实践

针对不同的编码器、封装协议、传输协议,提供统一的音视频处理接口。跨平台,兼容200多种编码、180多种封装格式、20多种传输协议。世界上90%以上的音视频开发基于FFmpeg。。

JDDTechTalk的博客 1605

QUIC在京东直播的应用与实践

国内的互联网直播技术从2005年前后兴起,彼时最具代表性的直播产品是由PPLive创始人姚欣在华中科技大学就读期间发起的校园直播项目PPLive。当时的直播技术用的还是基于windows系统自带的mediaplayer内置的COM组件开发的播放器,采用的是RTSP协议。受当时的互联网传输带宽及成本限制,PPLive并没有采用现在比较流行的单播技术,而是采用P2P技术分发直播流。国内的直播技术也进入了一段以P2P技术为主的时期,其中比较有代表性的就是PPLive和PPStream。

JDDTechTalk的博客 1409

突破传统监测模式:业务状态监控HM的新思路

以上是hm的全部技术细节。hm灵魂是数据计算、治理、数据统计。hm根基是集成百家之长。是我们平台研发部自研的一款面向业务的,异常监控处理的解决方案。汇集了团队每位伙伴的智慧。

JDDTechTalk的博客 1247

如何优雅的处理异常

Java 语言按照错误严重性,从 throwale 根类衍生出 Error 和 Exception 两大派系。Error(错误):程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm 虚拟机自身的非正常运行,calss 文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error 是系统内部的错误,由 jvm 抛出,交给系统来处理。程序正常运行中,可以预料的意外情况。比如数据库连接中断,空指针,数组下标越界。

JDDTechTalk的博客 1192

鸿蒙跨端实践-ArkTS和CAPI的混合开发实现

针对以上问题,通过和华为沟通,鸿蒙系统提供了C语言的命令式接口。C组件接口是介于UI组件的Native实现和ArkTS对接层之间的一层C接口封装,它绕过了状态管理对组件变化、刷新的自动化管理,同时避免了JS引擎和C++之间类型转换和跨语言调用的开销,因此具有较好的性能。通过C接口的对接,UI层级能直接和另外两端基本一致,通讯过程直接从JS到C++,C++可以直接调用C接口,流程大大缩短,数据类型转换变少了,列表渲染过程也由接入方自主控制,并且可以做预渲染等优化方案,同时避免了系统的二次布局。。

JDDTechTalk的博客 1756

Proxyless的多活流量和微服务治理

Joylive Agent 是一个基于字节码增强的框架,专注于多活和单元化场景下的流量治理。它提供了以下功能:多活流量调度、全链路灰度发布、QPS和并发限制、标签路由、负载均衡,熔断降级,鉴权等流量治理策略。其特性包括微内核架构、强类隔离、业务零侵入等,使其在保持高性能的同时对业务代码影响最小,是面向Java领域的新一代Proxyless Service Mesh探索实现。重要的事情说三遍:求Star,求Star,求Star。请Star完毕后继续阅读后文。

JDDTechTalk的博客 1823
上一篇: 文盘Rust —— rust连接oss | 京东云技术团队
下一篇: Webpack5构建性能优化:构建耗时从150s到60s再到10s | 京东云技术团队
京东云技术团队
京东云技术团队 企业官方账号 企业官方账号
博客等级 码龄6年 3625粉丝 686原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值