前言
京喜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框架、Codable
JSON模型转换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]