在Swift
中,如果想在运行时判断一个对象的类型是不是可选类型,则可以使用Mirror
,如下代码所示,所有的可选类型都将返回optional
。
let value: Int? = 3
Mirror(reflecting: value).displayStyle // optional
Mirror
的displayStyle
属性的类型是Mirror.DisplayStyle
,这是一个枚举类型,用于为Mirror
的主体对象提供一种建议性的解析,其定义如下代码所示:
/// A suggestion of how a `Mirror`'s `subject` is to be interpreted.
///
/// Playgrounds and the debugger will show a representation similar
/// to the one used for instances of the kind indicated by the
/// `DisplayStyle` case name when the `Mirror` is used for display.
public enum DisplayStyle {
case `struct`
case `class`
case `enum`
case tuple
case optional
case collection
case dictionary
case set
}
枚举值范围之外的类型,其Mirror
的displayStyle
属性将返回nil
,如下代码所示:
let value: Int = 20
Mirror(reflecting: value).displayStyle // nil
let str: String = "30"
Mirror(reflecting: str).displayStyle // nil
realm
开源了一套Swift
代码规范检测工具SwiftLint,它可以对代码做静态分析,以确定代码是否遵循所定制的规范(目前主要基于Github的编码规范)。
SwiftLint目前有70+条规则,同时社区还在不断贡献新的规则。支持的编辑器包括Xcode
、AppCode
、Atom
,同时还支持命令行。
其执行的基本效果如下图所示。
在Swift
中,自定义操作符就是简单的二步:首先在全局使用operator
关键字来声明操作符,同时用prefix
、infix
或postfix
来声明操作符的位置;然后在所需要的类/结构体中实现操作符。如下代码所示:
postfix operator >?
postfix operator >!
extension MIType {
public static postfix func >?(type: MIType) -> MIType {
return MIType("Optional<\(type.name)>")
}
public static postfix func >!(type: MIType) -> MIType {
return MIType("ImplicitlyUnwrappedOptional<\(type.name)>")
}
}
自定义操作符需要以两类字符开头:
ASCII
字符中的/, =, -, +, !, *, %, <, >, &, |, ^, ?, ~
Unicode
中的Mathematical Operators
,Miscellaneous Symbols
和Dingbats Unicode blocks
这些字符中的字符
然后后面允许使用组合的Unicode
字符。如下代码是以一个Miscellaneous Symbols
开头的实现向量加法的操作符。
infix operator ★+
struct Vector2D {
var x: CGFloat
var y: CGFloat
}
extension Vector2D {
static func ★+ (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
let vector1 = Vector2D(x: 10, y: 20)
let vector2 = Vector2D(x: 30, y: 10)
let vector = vector1 ★+ vector2
vector.x // 40.0
vector.y // 30.0
参考
- The Swift Programming Language (Swift 4) -- Advanced Operators
- The Swift Programming Language (Swift 4) -- Lexical Structure
在自定义操作符时,可以以dot(.)开头,这种情况下,操作符后面还可以包含其它的dot(.),如下代码所示:
infix operator .+.
struct Vector2D {
var x: CGFloat
var y: CGFloat
}
extension Vector2D {
static func .+.(left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
let vector1 = Vector2D(x: 10, y: 20)
let vector2 = Vector2D(x: 30, y: 10)
let vector = vector1 .+. vector2
vector.x
vector.y
但如果操作符不是以dot
开头,则后面不能再包含dot
,如operator +.+
这个声明会被看成是"+
"操作符后面跟了个".+
"操作符。编译器会给出如下错误提示:
infix operator +.+ // error: operator with postfix spacing cannot start a subexpression
参考
在Swift
中,我们可以使用withUnsafePointer(to:_:)
函数来获取一个变量的指针,如下代码所示:
var x = 42
var y = 3.14
var z = "foo"
var obj = NSObject()
withUnsafePointer(to: &x) {ptr in print(ptr)}
withUnsafePointer(to: &y) {ptr in print(ptr)}
withUnsafePointer(to: &z) {ptr in print(ptr)}
withUnsafePointer(to: &obj) {ptr in print(ptr)}
// 输出
// 0x000000011a145660
// 0x000000011a145668
// 0x000000011a145670
// 0x000000011a145688
withUnsafePointer(to:_:)
将第一个参数转换为指针,然后将这个指针作为参数去调用第二个参数指定的闭包。如果闭包有返回值,它将作为函数的返回值。
需要注意的是,�生成的指针的生命周期限定于闭包内部,不能将其指定给外部的变量。
第二种打印变量的指针的方式如下代码所示:
var x = 42
var y = 3.14
var z = "foo"
var obj = NSObject()
func printPointer<T>(ptr: UnsafePointer<T>) {
print(ptr)
}
printPointer(ptr: &x)
printPointer(ptr: &y)
printPointer(ptr: &z)
printPointer(ptr: &obj)
// 输出
// 0x000000011a145660
// 0x000000011a145668
// 0x000000011a145670
// 0x000000011a145688
参考
Swift
提供了ArraySlice
来执行数组的切片操作。类似于其它语言中的切片(如Python),ArraySlice
对象复用了原始数组的存储结构,而不是新开辟一块内存区域来将数组片断的元素拷贝过来。因此,它能让我们快速高效地对大数组的片段执行操作。如下代码所示:
var array: [Int] = []
for i in 0..<1000 {
array.append(i)
}
let slice = array[100..<300]
let result = slice.map {
$0 * 2
}.reduce(0) {
$0 + $1
}
print(result) // 79800
ArraySlice
与Array
有相同的接口,所以通常可以在切片数组上执行与原始数组相同的操作。
参考
使用ArraySlice
时,需要注意两点:
ArraySlice
会维持对原始数组的一个强引用,而不仅仅是它所表示的片断。这样即使原始数组对象的生命周期结束了,也可能无法释放。所以不建议长期存储ArraySlice
对象,仅用于临时操作。如下代码所示:
class MyClass {
var index: Int
init(index: Int) {
self.index = index
}
}
class ViewController: UIViewController {
var slice: ArraySlice<MyClass>? = nil
override func viewDidLoad() {
super.viewDidLoad()
var array: [MyClass] = []
for i in 0..<100 {
let clz = MyClass(index: i)
array.append(clz)
}
slice = array[10..<30]
}
}
array
在生命周期结束后不会释放,如下图所示。
- 与
Array
不同的是,ArraySlice
起始索引不一定是0,而是取决于其创建方式。一般是采用共享索引的方式,即ArraySlice
对象的起始索引就是切片的开始位置,如代码清单8-2-2所示,切片是从100开始,所以slice[100]
是OK的,而slice[99]
会报越界错误。通常建议使用startIndex
和endIndex
来取代指定的索引值,如下代码所示。
var array: [Int] = []
for i in 0..<1000 {
array.append(i)
}
let slice = array[100..<300]
slice[100]
slice[99] // fatal error: Index out of bounds
slice[slice.startIndex] // 100
slice[slice.endIndex - 1] // 299
参考
尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回。我们知道在函数调用时,会在内存中生成一个调用记录,即调用帧(call frame
),用于保存调用位置和内部变量等信息。而尾调用由于是函数的最后一步操作,所以可以不在调用栈上添加一个新的调用帧,而是直接更新外层函数的调用帧,这种处理方式即尾调用优化(Tail Call Optimization, TCO
)。
尾递归也是一种尾调用,通过TCO
,可以使原本 O(n)
的调用栈空间只需要 O(1)
。
一些语言会针对尾递归做一些优化,但Swift
不确保在所有情况下都做了优化。
参考
- Does Swift implement tail call optimization? and in mutual recursion case?
- Functional Swift: Tail Recursion Explained
- 尾调用
我们通常看到的下标操作都是一维的,不过如果有需要,我们可以定义多维的下标操作,如下代码所示:
// �定义一个三维结构体
struct Vector3D<T: Equatable> {
var x: T
var y: T
var z: T
}
// 向量数组
struct VectorArray<T: Equatable> {
var vectors: [Vector3D<T>] = []
init() {
}
mutating func append(vector: Vector3D<T>) {
vectors.append(vector)
}
// 此处是根据输入的x, y, z值来确定是否有对应的值,而不是索引
// 如果有,则返回该向量值;如果没有,返回nil
subscript(x: T, _ y: T, _ z: T) -> Vector3D<T>? {
for vector in vectors {
if vector.x == x && vector.y == y && vector.z == z {
return vector
}
}
return nil
}
}
var vectors = VectorArray<Int>()
vectors.append(vector: Vector3D(x: 0, y: 1, z: 2))
vectors.append(vector: Vector3D(x: 3, y: 4, z: 6))
vectors.append(vector: Vector3D(x: 8, y: 4, z: 8))
print(vectors[0, 0, 0]) // nil
print(vectors[3, 4, 6]) // Optional(__lldb_expr_167.Vector3D<Swift.Int>(x: 3, y: 4, z: 6))
在Swift 3
中,如果想打印线程的调用栈,可以简单的使用Thread.callStackSymbols
,如下代码所示:
for symbol in Thread.callStackSymbols {
print(symbol)
}
// 0 ??? 0x00000001198270a1 0x0 + 4722946209
// 1 MyPlayground 0x0000000106051730 main + 0
// 2 CoreFoundation 0x000000010989620c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
// 3 CoreFoundation 0x000000010987aa3b __CFRunLoopDoBlocks + 203
// 4 CoreFoundation 0x000000010987a214 __CFRunLoopRun + 1300
// 5 CoreFoundation 0x0000000109879a89 CFRunLoopRunSpecific + 409
// 6 GraphicsServices 0x000000010d5e79c6 GSEventRunModal + 62
// 7 UIKit 0x0000000106af5d30 UIApplicationMain + 159
// 8 MyPlayground 0x00000001060517f9 main + 201
// 9 libdyld.dylib 0x00000001062e5d81 start + 1
// 10 ??? 0x0000000000000001 0x0 + 1
如果想使用C函数backtrace
,需要在bridging header file
中导入execinfo.h
头文件,然后如理代码所示,记得最后需要用free
释放symbols
。
var callstack = [UnsafeMutableRawPointer?](repeating: nil, count: 128)
let frames = backtrace(&callstack, Int32(callstack.count))
if let symbols = backtrace_symbols(&callstack, frames) {
for frame in 0..<Int(frames) where symbols[frame] != nil {
let symbol = String(cString: symbols[frame]!)
print(symbol)
}
free(symbols)
}
参考
Swift
编译器自动为带有raw type
的enum
添加了RawRepresentable
协议的实现。RawRepresentable
协议的定义如代码清单11-1-1所示,它可以为某个类型定义一个关联的raw value
,并在两者之间切换。如下代码所示是一个典型的带raw value
的枚举。
public protocol RawRepresentable {
associatedtype RawValue
public init?(rawValue: Self.RawValue)
public var rawValue: Self.RawValue { get }
}
enum Counter: Int {
case one = 1, two, three, four, five
}
for i in 3...6 {
print(Counter(rawValue: i))
}
// 输出
// Optional(__lldb_expr_197.Counter.three)
// Optional(__lldb_expr_197.Counter.four)
// Optional(__lldb_expr_197.Counter.five)
// nil
参考
Swift
在类的extension
中,如果想重写父类的方法,会有以下两个限制(不局限于这两个,可能还有其它):
- 类本身必须是NSObject体系下的类,且要重写的方法的参数、返回值不能带有纯Swift的类/结构体/枚举;
- 不能使用inout参数;
当然参数和返回值的类型如果是String
、Array
、Dictionary
、Set
这些也是OK的。
Swift 3
中改进了操作符的优先级及结合性的声明方式。
在Swift 3
之前,是使用magic numbers
(魔数)的方式来声明操作符的优先级,如下代码所示:
infix operator <~ {
associativity left
precedence 125
}
magic numbers
总归是一个不好的东西,所以Swift 3
改用precedence groups
(优先级组)的方式来声明操作符的优先级,如下代码所示:
precedencegroup Equivalence {
associativity: left
higherThan: LogicalConjunctionPrecedence
lowerThan: ComparisonPrecedence
}
infix operator ~ : Equivalence
系统为我们提供了一些默认的precedence groups
,如下代码所示:
precedencegroup AssignmentPrecedence {
assignment: true
associativity: right
}
precedencegroup TernaryPrecedence {
associativity: right
higherThan: AssignmentPrecedence
}
precedencegroup DefaultPrecedence {
higherThan: TernaryPrecedence
}
precedencegroup LogicalDisjunctionPrecedence {
associativity: left
higherThan: TernaryPrecedence
}
precedencegroup LogicalConjunctionPrecedence {
associativity: left
higherThan: LogicalDisjunctionPrecedence
}
precedencegroup ComparisonPrecedence {
higherThan: LogicalConjunctionPrecedence
}
precedencegroup NilCoalescingPrecedence {
associativity: right
higherThan: ComparisonPrecedence
}
precedencegroup CastingPrecedence {
higherThan: NilCoalescingPrecedence
}
precedencegroup RangeFormationPrecedence {
higherThan: CastingPrecedence
}
precedencegroup AdditionPrecedence {
associativity: left
higherThan: RangeFormationPrecedence
}
precedencegroup MultiplicationPrecedence {
associativity: left
higherThan: AdditionPrecedence
}
precedencegroup BitwiseShiftPrecedence {
higherThan: MultiplicationPrecedence
}
参考
之前有一条知识小集讲了可以用 @noreturn
用来修饰函数,以告诉编译器函数永远不返回值给调用方。如下代码是原来的fatalError
的声明。
public func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
不过作为函数类型的一个正交属性,它与函数声明的其它一些属性一起使用时会产生一些歧义,如与@throws
和non-void
返回值。如当成 @throws
一起使用时,可能会困惑指"无法正常返回,但可以抛出异常",还是"完全无法返回"?
所以现在使用Never枚举类型来替代@noreturn
,以消除这种歧义,所以fatalError
现在声明如下代码所示。这样() throws -> Never就好理解了,即"不能正常返回,但可能抛出异常"。
/// Unconditionally prints a given message and stops execution.
///
/// - Parameters:
/// - message: The string to print. The default is an empty string.
/// - file: The file name to print with `message`. The default is the file
/// where `fatalError(_:file:line:)` is called.
/// - line: The line number to print along with `message`. The default is the
/// line number where `fatalError(_:file:line:)` is called.
public func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
需要注意的是Never
是一个空枚举,如下代码所示:
/// The return type of functions that do not return normally; a type with no
/// values.
///
/// Use `Never` as the return type when declaring a closure, function, or
/// method that unconditionally throws an error, traps, or otherwise does
/// not terminate.
///
/// func crashAndBurn() -> Never {
/// fatalError("Something very, very bad happened")
/// }
public enum Never {
}
参考
Never
是一个空枚举,即没有有效的case
。所以它是一种uninhabited type
,即明显没有值的类型。Swift
中,以下两种情况可以看成是uninhabited type
:
- 一个枚举类型,如果没有
case
,或者所有case
已知,且所有的case
都有关联值,而所有的关联值类型都为空; - 元组、结构体或者类,如果有任意的
uninhabited type
类型的存储属性;
If an expression of uninhabited type is evaluated, it is considered unreachable by control flow diagnostics.
另外,函数和元类型不能是uninhabited type
。
参考
Swift
提供了一个Repeated
结构体,用于表示一个所有元素都相同的集合。我们可以使用repeatElement(_:count:)
函数来创建Repeated
集合,如下代码所示:
let repeatedName = repeatElement("test", count: 5)
for name in repeatedName {
print(name)
}
// 输出
// test
// test
// test
// test
// test
Swift
中的@autoclosure
还是一个很有意思的属性,它用于修饰函数中的闭包类型的参数,它主要有两个作用:
- 将一个表达式自动封装成一个闭包,从而简化函数/方法的调用方式,如下代码所示;
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
// ��显示闭包参数,调用时参数以闭包的方式传入
func serve(curstomer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(curstomer: {customersInLine.remove(at: 0)})
// autoclosure闭包
func serve2(curstomer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
// 表达式`customersInLine.remove(at: 0)`被自动封装成一个闭包
serve2(curstomer: customersInLine.remove(at: 0))
- 由于表达式封装成了闭包,所以可以延迟表达式的计算,一直到闭包被调用时。关于这一点的好处,可以参考这里中对
assert
实现的介绍。
需要注意的是,封装后的闭包一般不带参数,而表达式的值就作为返回值。所以声明函数时闭包一般没有输入参数,即使有也会被忽略,实际上在写代码时,Xcode
就是将@autoclosure
参数当成一个普通的类型
参考
- [The Swift Programming Language (Swift 4) -- Closures](https://developer.apple.com/library/prerelease/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID543 https://developer.apple.com/swift/blog/?id=4)
View hierarchy
的layout
可以定义为一系列的线性方程式。每个约束都表示一个方程式,大部分约束定义的是两个视图之间的关系,当然也可以是一个视图的两个不同的属性之间的关系,如高宽比。
下图是文档中给出的一个等式样例,
下图是一个基本的解释,
参考
在Swift
中,我们通常会用FIXME/TODO/MARK
来做一些注释标注,这些标注在代码的dropdown box
中能显示出来,如下图所示:
不过,如果我们标注的是一些警告或错误信息的话,这用方式通常比较容易遗忘,不像Objective-C
中的#warning
一样,在编译时能被检测出来。
如果我们想在编译时让编译器高亮提示,即如下图所示,
则可以在Build Phases
中添加一个Run Script
,脚本代码如下图所示:
可以看到,我们还能添加自定义的注释标签,如WARNING/ERROR
。
参考
接上一条。打包的layer
和动画发送到render server
后,会被反序列化成render tree
。Render server
将对动画的每一帧执行以下两步操作:
- 计算所有
layer
属性的中间值,并设置OpenGL
的几何属性(如纹理三角形)来执行渲染操作; - 渲染可见的三角形到屏幕上
这两步操作在动画期间不断重复。第一步操作由CPU
来完成,第二步操作GPU
来完成。两步操作都是由系统控制。
在iOS中,动画的执行主要分6个阶段,其中4个阶段是在应用内部处理的:
- Layout: 这一阶段是准备好
view/layer
的层级结构并设置layer的属性; - Display: 这一阶段将绘制
layer
的内容。绘制操作会调用-drawRect:
或-drawLayer:inContext:
方法; - 准备阶段: 这一阶段是
Core Animation
准备将动画数据发送给render server
。另外,还会执行其它一些操作,如解压在动画过程中显示的图片; - 提交: 这是应用内部处理的最后一个阶段,
Core Animation
将打包layer
及动画属性并通过IPC
发送到render server
;
这几个阶段都是由CPU来处理的,而且我们只能控制前面两个阶段,后两个阶段将由Core Animation
来控制。
CALayer
的大部分属性都是通过GPU
来绘制的。以下几类操作可能导致layer
绘制的性能问题:
- 过多的几何形状:这里的瓶颈主要是在对
layer
的预处理及通过IPC
发送到render server
这个过程,即CPU
的处理,而不是GPU
的绘制; - 过多的重绘(
overdraw
):这主要是由绘制重叠的半透明层引起的; - 离屏渲染:这个应该比较熟悉,诸如
layer
的圆角绘制、mask、阴影等操作都会导致离屏渲染,主要是因为需要为离屏绘制的图片分配额外的内存并在绘制上下文之间切换; - 过大的图片:如果图片大小超过GPU所支持的最大纹理大小,则在绘制前CPU需要去做预处理。
参考
iOS Core Animation Advanced Techniques