可选接口的扩展
Swift 2.0以后,对于可选接口,我们有了另一种选择,就是使用 protocol extension
我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现。
1 | protocol OptionalProtocol { |
内存管理,weak 和 unowned
Swift 是自动管理内存的,我们不需要操心内存的申请和分配。初始化的时候 Swift 会替我们管理和分配内存,释放的原则遵循了自动计数 ARC 的规则:当一个对象没有引用的时候,内存将会被自动回收。我们只需要保证在合适的时候将引用置空,就可以确保内存使用不出现问题。
但是会有一种情况出现:循环引用:
1 | class A { |
Swift 里防止循环引用
为了防止这一点,我们习惯希望被动的一方不要去持有主动的一方。在这里 b.a 里对 A 的实例的持有是由 A 的方法设定的,之后直接使用的也是对 A 的实例的持有。因此认为 b 是被动的一方:
1 | class B { |
在 var 前面加上 weak 向编译器说明我们不希望持有 a。当 obj 指向 nil 时,整个环境中就没有对 A 这个实例的持有了。
Swift 除了 weak 以外,还有另一种就是 unowned。他们的区别是,unowned 不能修饰 Optional。也不会指向 nil。
标记为 weak 的成员释放后将会自动变成 nil。Apple 给我们的建议:如果确定在访问时不会被释放的话,尽量使用 unowned,如果存在被释放的可能,使用 weak。
使用场景:
- 设置 delegate
- 在 self 属性存储为闭包时,其中拥有对 self 的引用。
闭包中对任何其他元素的引用都会被闭包强引用,如果在闭包写了 self 这样的东西。就在闭包内持有了当前的对象。
self 引用 闭包 闭包 -> self 因此形成循环引用。
1 | lazy var printName: ()->() = { |
例子:
1 | // 标注前 |
@autoreleasepool
ARC 虽然不需要手动调用 retain,release 或者是 autorelease 这样的方法来管理引用计数,但是这些方法还是会被调用,只不过编译器在合适的地方帮我们加入了。
autorelease 会将接受该消息的对象放到一个预先建立的自动释放池,在自动释放池收到 drain 消息时将这些对象的引用计数减一,然后将他们从池子中移除。
app 中,整个主线程其实是跑在一个自动释放池里的,每个主 Runloop 结束时进行 drain 操作。
1 | int main(int argc, char * argv[]) { |
其实 @autoreleasepool 在编译时会被展开为 NSAutoreleasePool,并附带 drain 方法的调用。
1 | func loadBigData() { |
上述代码的内存使用:
在 swift 中使用 autoreleasepool 语法略有不同,变成了一个接受闭包的方法,利用尾随闭包:
1 | func loadBigData() { |
虽然每次循环都生成一个自动释放池,保证内存使用达到最小,但释放过于频繁,会带来潜在的性能忧虑。一个折中的方案是每十次循环对应一次自动释放,这样能减少带来性能的损失。
对于上面特定的例子,并不一定需要加入自动释放。Swift 中更提倡初始化方法不是上面。因为加入了可以返回 nil 的初始化方法。上面那样的工厂方法已经从 API 中删除了。因此:
1 | let data = NSData(contentsOfFile: path) |
使用初始化方法的话,就不需要面临自动释放的问题了每次超过作用域后,自动内存管理将为我们处理好内存相关的事。
值类型和引用类型
Swift 的类型分为值类型和引用类型,值类型在传递和赋值时进行赋值,引用类型只会使用对象的引用。
Swift 所有的内奸类型都是值类型,String,Array 以及 Dictionary 都是值类型。现在流行的编程语言中,数组字典这样的类型都是引用类型。
使用值类型的好处:减少了堆内存分配和回收的次数。
Swift 的值类型,特别是数组和字典这样的容器,在内存管理上经过了精心的设计。值类型在传递和赋值时进行赋值,每次赋值肯定会产生额外开销。但 Swift 的这个消耗被控制到最小范围,没有必要复制的时候,值类型的赋值都不会发生。就是说,简单的赋值,参数传递等等普通操作,虽然我们可能用不同的名字来回设置和传递值类型,但是内存上他们都是同一块内容:
1 | func test(arr: [Int]) { |
其实只在第一句 a 初始化赋值时发生了内存分配,b c 甚至传递到 test 方法内的 arr,和最开始的 a 在物理内存上都是同一个东西,a 还只在栈空间上,这个过程对于数组来说,只发生了指针移动,而完全没有堆内存的分配和释放的问题。这样的运行效率极高。
值类型被复制的时机是值类型的内容发生改变时,比如:
1 | var a = [1,2,3] |
值类型在复制时,会将存储在其中的值类型一并复制,而如果存储在其中的是引用类型,则只赋值一份引用。
:
1 | class MyObject { |
将数组和字典设计为值类型最大的考虑是为了线程安全,这样设计在存储元素或条目数量较少时,给我们带来了另一个优点,非常高效,这有效的减少了内存的分配和回收,但在少数情况下,我们可能在数组或字典中存储非常多的东西,并且还要对其中的内容进行添加或者删除。这时,swift 内建的值类型的容器类型在每次操作时都需要复制一遍,即使存储的都是引用类型,复制时还是需要存储大量的引用,这个开销就变的不容忽视了,这种情况下,我们可以使用 Cocoa 中的引用类型的容器类型,NSMutableArray 和 NSMutableDictionary
所以,在使用数组和字典时的最佳实践应该是,按照具体的数据规模和操作特点来决定到时是使用值类型的容器还是引用类型的容器:在需要处理大量数据并频繁操作(增减)其中元素时,选择 NSMutableArray 和 NSMutableDictionary 会更好,对于容器内条目小而容器本身数目多的情况, 应该使用 Swift 语言内建的 Array 和 Dictionary。
String 还是 NSString。
尽可能使用原生 String 类型
原因:
虽然 String 和 NSString 有着良好的相互转换的特性,但是 Cocoa 所有的 API 都接受和返回 String 类型。我们没有必要去把框架中返回的字符串做一遍转换,既然 Cocoa 鼓励使用 String,并且提供了足够的操作 String 的方法,直接使用。
Swift 中 String 是 stuct,相比起 NSObject 的 NSString 类来说,更切合字符串的 “不变” 这一特性。通过配合常量赋值 let,这种不变性在多线程编程就很重要,另外,不触及 NSString 特有操作和动态特性的时候,使用 String 的方法,在性能上会有所提升。
String 里的 String.CharacterView 实现了像 CollectionType 这样的接口,因此有些 Swift 的语法特性只有 String 才能使用,而 NSString 是没有的。一个典型就是 for…in 的枚举,我们可以写:
1 | let levels = "ABCDE" |
也有一些例外的情况,有一些 NSString 的方法在 String 中并没有实现,比如 containString
你可以自行用扩展的方式在自己的代码库为 String 添加这个方法,还有一些其他的像 length characterAtIndex:这样的 API 也没有。
UnsafePointer
某个 API 在 C 中是这样的话:
1 | void method(const int *num) { |
对应的 swift 方法应该是:
1 | func method(num: UnsafePointer<CInt>) { |
int bool char 对应分别是 CInt CBool 和 CChar
C API Swift API
const Type * UnsafePointer
Type * UnsafeMutablePointer
在 C 中对某个指针进行取值使用的是 * ,在 Swift 中我们可以使用 memory 属性来读取相应内存中存储的内容。通过传入指针地址进行方法调用的时候就都比较相似了,都是在前面加上 & 符号,C 的版本和 Swift 版本只在申明变量的时候有所区别:
1 | // C |
指针的内容和实际值之间进行转换。我们由于某种原因需要涉及到直接使用 CFArray 的方法来获取数组中元素:
1 | func CFArrayGetValueAtIndex(theArray: CFArray!, idx:CFIndex) -> UnsafePointer<Void> |
Swift 为我们提供了一个强制转换的方法 unsafeBitCast
1 | let arr = NSArray(object: "meow") |
unsafeBitCast 会将第一个参数的内容按照第二个参数的类型进行转换,而不会去关心实际是不是可行,所以 unsafeBitCast 是不安全的,因为我们不必存遵循类型转换的检查,有了指针层面直接操作内存的机会。
C 指针内存管理
C 指针在 Swift 中被冠名 unsafe 原因是无法对其进行自动的内存管理:
1 | class MyClass { |
造成了内存泄露
需要添加:
1 | pointer.destroy() |
这里要注意的是,如果在 dealloc 之后再去访问 pointer 或者再次调用 dealloc 的话,就会崩溃。
手动处理这类指针的内存管理时,遵循的一个基本原则就是谁创建谁释放。,一般 destroy dealloc 要与 alloc 成对出现。
否则不应该试图去管理它的内存状态
最后:如果指针的内存申请使用 malloc 或者 calloc 完成的,释放时需要使用 free 而不是 dealloc
自省
使用 .dynamicType 获取一个对象的类型。
使用 is 判断不确定的类型,is 在功能上相当于原来的 isKindOfClass
区别:不仅可以用于 class 类型,也可以对 struct 和 enum 类型进行判断
1 | class ClassA { } |
1 | let string = "String" |
KVO
1 | class MyClass: NSObject { |
局部 scope
1 | func local(closure: ()->()) { |
1 | 不过在 Swift 2.0 中,为了处理异常,Apple 加入了 do 这个关键字来作为捕获异常的作用域。这一功能恰好为我们提供了一个完美的局部作用域,现在我们可以简单地使用 do 来分隔代码了: |
1 | titleLabel = { |
类簇化(Cocoa 设计模式)
1 | class Drinking { |
通过工厂方法,调用父类的同一个方法,根据传入参数不同,得到不同的子类实例。
1 | let cokeClass = NSStringFromClass(coke.dynamicType) //Coke |
Options
对于遵循了 OptionSetType 协议的枚举,OptionSetType 实现了 SetAlgebraType,因此我们可以对两个集合进行各种集合运算,包括并集 union、交集 intersect ,对于不需要输入的情况,直接[]表示
1 | UIView.animateWithDuration(0.3, |
要实现一个 Options 的 struct 的话可以参照已有的写法建立类并实现 OptionSetType
类型编码 @encode
1 | char *typeChar1 = @encode(int32_t); |
Swift 使用了自己的 Metatype 来处理:
1 | let int: Int = 0 |
如果我们想在 NSUserDefaults 中存储一些不同类型的数字,然后读取时需要准确地还原为之前的类型的话,最容易想到的应该是使用类簇来获取这些数字转为 NSNumber 后真正的类型,然后存储,但是 NSNumber 的类簇子类都是私有的,如果要判定的话就必须使用私有 API,这是不可接受的,变通的方法就是在存储时使用 objcType 获取类型,然后将数字本身和类型的字符串一起存储。读取时通过匹配类型字符串和类型编码,确定数字本来所属的类型。从而直接得到像 Int 或者 Double 这样的类型明确的量。
C 代码调用 和 @asmname
Foundation 框架中包含了 Darwin 的导入,Swift 在导入时为我们将 Darwin 也进行了类型的自动转换对应,对于三角函数的计算输入和返回都是 Swift 的 Double 类型,而非 C 的类型:
1 | func sin(x: Double) -> Double |
而对于第三方的 C 代码,Swift 也提供了协同使用的方法。我们知道,Swift 中调用 Objective-C 代码非常简单,只需要将合适的头文件暴露在 {product-module-name}-Bridging-Header.h 文件中就行了。而如果我们想要调用非标准库的 C 代码的话,可以遵循同样的方式,将 C 代码的头文件在桥接的头文件中进行导入。
另外,我们还有一种不需要借助头文件和 Bridging-Header 来导入 C 函数的方法,使用 Swift 中的一个隐藏的符号 @asmname,@asmname 可以通过方法名字将某个 C 函数直接映射为 Swift 中的函数:
1 | //File.swift |
sizeof 与 sizeofValue
喜欢写 C 的人可能会经常和 sizeof 打交道,不论是分配内存,I/O 操作,还是计算数组打大小的时候基本都会用到。这个在 C 中定义的运算符可以作用于类型或者某个实际的变量,并返回其在内存中的尺寸。
1 | /// Does not include any dynamically-allocated or "remote" storage. |
sizeOf 用于 class Type 内存中存储这个类型需要多少个字节
1 | /// Does not include any dynamically-allocated or "remote" storage. |
sizeOfValue 用于 class Instance 存储这个实例的类型需要多少个字节。
delegate
Cocoa 开发中接口-委托(protocol-delegate)模式贯穿于整个 Cocoa 框架。作用:清理代码之间的关系,和解除耦合。
1 | protocol MyClassDelegate { |
Swift 的 protocol 是可以被除了 class 以外的其他类型遵循的,对于 struct 或者是 enum 这样的类型,本身不通过引用计数来管理内存,所以也不会用 weak 这样的 ARC 概念进行修饰,如果要在 Swift 中使用 weak delegate,需要将 protocol 限制在 class 内,一种比较好的做法:
1 | protocol MyClassDelegate: class { |
Associated Object
在使用 Category 扩展现有类的功能的时候,直接添加实例变量这种行为是不被允许的,这时候一般就使用 property 配合 Associated Object 的方式,将一个对象关联到已有的要扩展的对象上。进行关联后,在对这个目标对象访问时,从外界看来,就像是直接在通过属性访问对象的实例变量一样。
在 Swift 中这样的方法依旧有效,只是在写法上可能有些不同。两个对应的运行时 get 和 set Associated Object 的 API 是这样的:
1 | func objc_getAssociatedObject(object: AnyObject!, key: UnsafePointer<Void>) -> AnyObject! |
这两个 API 所接受的参数都 Swift 化了,在类型检查上也严格了不少。Swift 中向某个 extension 里使用 Associated Object 的方式将对象进行关联的写法:
1 | // MyClass.swift |
key 的类型在这里声明为 Void? 并且通过 & 操作符取地址,作为 UnsafePointer
Lock
Swift 中已经不存在 @synchronized, @synchronized 幕后做的事情是调用了 objc_sync 中的 objc_sync_enter 和 objc_sync_exit 方法,并且加入了一些异常判断。因此,Swift 中,如果忽略掉那些异常的话:
1 | func myMethod(anObj: AnyObject!) { |
如果我们希望以前的形式,可以写一个全局的方法,接受一个闭包将 objc_sync_enter 和 objc_sync_exit 封装起来:
1 | func synchronized(lock: AnyObject, closure: () -> () { |
结合 Swift 尾随闭包的特性:
1 | func myMethodLocked(anObj: AnyObject!) { |
Core Foundation
Cocoa 框架大部分 NS 开头的类在 CF 中都有对应的类型存在,Objective-C 中 ARC 负责只是 NSObject 的自动引用计数,如果把对象在 NS 与 CF 之间进行转换时,需要向编译器说明是否需要转移内存的管理权。
在不涉及内存管理转换的情况,直接在转换的时候加上 __bridge 来进行说明,表示内存管理权不变。例如:
1 | NSURL *fileURL = [NSURL URLWithString:@"SomeURL"]; |
在 Swift 中,这样转换可以直接省掉了,上面的代码可以写为下面:
1 | import AudioToolbox |
CFURLRef 在 Swift 中被 typealias 到 CFURL 上。各类 CF 都记性了类似处理。
Swift 中,CF 也在 ARC 的管辖范围之内了。有一点例外,对于非系统的 CF API(比如自己写的或者是第三方的),因为没有强制机制要求,他们一定遵照 Cocoa 的命名规范,所以不能贸然进行自动内存管理。:
1 | // CFGetSomething() -> Unmanaged<Something> |