Swift 轻松掌握喵神高阶心法 X
Kla0239
8年前
<h3>Tip48 @autoreleasepool</h3> <p>Swift在内存管理上使用的是自动引用计数(ARC)的一套方法, 在ARC中虽然不需要手动地调用像是retain, release或者是autorelease这样的方法来管理引用计数, 但是这些方法还是都会被调用的 --只不过是编译器在编译时在合适的地方帮我们加入了而已. 其中retain和release都很直接, 就是将对象的引用计数加一或者减一. 但是autorelease就比较特殊一些, 它会将接受该消息的对象放到一个预先建立的自动释放池(auto release pool)中, 并在自动释放池收到drain消息时将这些对象的引用计数减一, 然后将它们从池子中移除(这一过程形象地称为"抽干池子").</p> <p>在app中, 整个主线程其实是跑在一个自动释放池里的, 并且在每个主Runloop结束时进行drain操作. 这是一种必要的延迟释放的方式, 因为我们有时候需要确保在方法内部初始化的生成的对象在被返回后别人还能使用, 而不是立即被释放掉.</p> <p>在Objective-C中, 建立一个自动释放池的语法很简单, 使用@autoreleasepool就行了. 如果你新建一个Objective-C项目, 可以看到main.m中就有我们刚才说到的整个项目的autoreleasepool:</p> <pre> <code class="language-swift">int main(int argc, char * argv[]) { @autoreleasepool { int retVal = UIApplicationMain ( argc, argv, nil, NSStringFromClass([AppDelegate class])); return retVal; } }</code></pre> <p>更进一步, 其实@autoreleasepool在编译时会被展开为NSAutoreleasPool, 并附带drain方法的调用.</p> <p>而在Swift项目中, 因为有了@UIApplicationMain, 我们不再需要main文件和main函数, 所以原来的整个程序的自动释放池就不存在了. 即时我们使用main.swift来作为程序的入口时, 也是不需要自己再添加自动释放池的.</p> <p>但是在一种情况下我们还是希望自动释放, 那就是在面对在一个方法作用域中要生成大量的autorelease对象的时候. 在Swift1.0时, 我们可以写这样的代码:</p> <pre> <code class="language-swift">func loadBigData() { if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") { for i in 1...10000 { let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil) NSThread.sleepForTimeInterval(0.5) } } }</code></pre> <p>dataWithContentsOfFile返回的是autorelease的对象, 因为我们一直处在循环中, 因此它们将一直没有机会被释放. 如果数量太多而且数据太大的时候, 很容易因为内存不足而崩溃. 在Instruments下可以看到内存alloc的情况:</p> <p>这显然是一幅很不妙的情景. 在面对这种情况的时候, 正确的处理方法是在其中加入一个自动释放池, 这样我们就可以在循环进行到某个特定的时候释放内存, 保证不会因为内存不足而导致应用崩溃. 在Swift中我们也是能使用autoreleasepool的 --虽然语法上略有不同. 相比于原来在Objective-C中的关键字, 现在它变成了一个接受闭包的方法:</p> <pre> <code class="language-swift">func autoreleasepool(code:() -> ())</code></pre> <p>利用尾随闭包的写法, 很容易就能在Swift中加入一个类似的自动释放池了:</p> <pre> <code class="language-swift">func loadBigData() { if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") { for i in 1...10000 { autoreleasepool { let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil) NSThread.sleepForTimeInterval(0.5) } } } }</code></pre> <p>这样改动以后, 内存分配就没有什么忧虑了:</p> <p>这里我们每一次循环都生成了一个自动释放池, 虽然可以保证内存使用达到最小, 但是释放过于频繁也会带来潜在的性能忧虑. 一个折中的方法是将循环分隔开加入自动释放池, 比如每10次循环对应一次自动释放, 这样能减少带来的性能损失.</p> <p>其实对于这个特定的例子, 我们并不一定需要加入自动释放. 在Swift中更提倡的是用初始化方法而不是用像上面那样的类方法来生成对象, 而且从Swift1.1开始, 因为加入了可以返回nil的初始化方法, 像上面例子中那样的工厂方法都已经从API中删除了. 今后我们都应该这样写:</p> <pre> <code class="language-swift">let data = NSData(contentsOfFile: path)</code></pre> <p>使用初始化方法的话, 我们就不需要面临自动释放的问题了, 每次在超过作用域后, 自动内存管理都将为我们处理好内存相关的事情.</p> <h3>Tip49 值类型和引用类型</h3> <p>Swift的类型分为值类型和引用类型两种, 值类型在传递和赋值时将进行复制, 而引用类型则只会使用引用对象的一个"指向". Swift中的struct和enum定义的类型是值类型, 使用class定义的为值类型. 很有意思的是, Swift中的所有的内建类型都是值类型, 不仅包括了传统意义像Int, Bool这些, 甚至连String, Array以及Dictionary都是值类型的. 这在程序设计上绝对算得上一个震撼的改动, 因为据我所知现在流行的编程语言中, 像数组和字典这样的类型, 机会清一色都是引用类型.</p> <p>那么使用值类型有什么好处呢? 相较于传统的引用类型来说, 一个很显而易见的优势就是减少了推上内存分配和回收的次数. 首先我们需要知道, Swift的值类型, 特别是数组和字典这样的容器, 在内存管理上经过了精心的设计. 值类型的一个特点是在传递和赋值时进行复制, 每次复制肯定会产生额外的开销, 但是在Swift中这个消耗被控制在了最小范围内, 在没有必要复制的时候, 值类型的复制都是不会发生的. 也就是说, 简单的赋值, 参数的传递等等普通操作, 虽然我们可能用不同的名字来回设置和传递值类型, 但是在内存上它们都是同一块内容. 比如下面这样的代码:</p> <pre> <code class="language-swift">func test(_ arr: [Int]) { for i in arr { print(i) } } var a = [1, 2, 3] var b = a let c = b test(a)</code></pre> <p>这么折腾一圈下来, 其实我们只在第一句a初始化赋值时发生了内存分配, 而之后的b, c甚至传递到test方法内的arr, 和最开始的a在物理内存上都是同一个东西. 而且这个a还只在栈空间上, 于是这个过程对于数组来说, 只发生了指针移动, 而完全没有堆内存的分配和释放的问题, 这样的运行效率可以说极高.</p> <p>值类型被复制的时机是值类型的内容发生改变时, 比如下面在b中又加入了一个数, 此时值复制就是必须的了:</p> <pre> <code class="language-swift">var a = [1, 2, 3] var b = a b.append(5) //此时a和b的内存地址不再相同</code></pre> <p>值类型在复制时, 会将存储在其中的值类型一并进行复制, 而对于其中的引用类型的话, 则只复制一份引用. 这是合理的行为, 因为我们不会希望引用类型莫名其妙地引用到了我们设定以外其他对象:</p> <pre> <code class="language-swift">class MyObject { var num = 0 } var myObject = MyObject() var a = [myObject] var b = a b.append(myObject) myObject.num = 100 print(b[0].num) print(b[1].num) // myObject的改动同时影响了b[0]和b[1]</code></pre> <p>虽然将数组和字典设计为值类型最大的考虑是为了线程安全, 但是这样的设计子啊存储元素或条目数量较少时, 给我们带来了另一个优点, 那就是非常高效, 因为"一旦赋值就不太会变化"这种使用情景在Cocoa框架中占有绝大多数的, 这有效的减少了内存的分配和回收. 但是在少数情况下, 我们显然也可能会在数组或者字典中存储非常多的东西, 并且还要对其中的内容进行添加或者删除. 在这时, Swift内建的值类型的容器类型在每次操作时都需要复制一遍, 即时是存储的都是引用类型, 在复制时我们还是需要存储大量的引用, 这个开销就变得不容忽视了. 幸好我们还有Cocoa种的引用类型的容器类来对应这种情况, 那就是NSMutableArray和NSMutableDictionary.</p> <p>所以, 在使用数组和字典时的最佳实践应该是, 按照具体的数据规模和操作特点到时是使用值类型的容器还是引用类型的容器: 在需要处理大量数据并且频繁操作(增减)其中元素时, 选择NSMutableAttay和NSMutableDictionary会更好, 而对于容器内条目小而容器本身数目多的情况, 应该使用Swift语言内建的Array和Dictionary.</p> <h3>Tip50 String 还是 NSString</h3> <p>既然像String这样的Swift的类型和Foundation的对应的类是可以无缝切换的, 那么我们在使用和选择的时候, 有没有什么需要特别注意的呢?</p> <p>简单来说, 没有特别需要注意的, 但是尽可能的话还是使用原生的String类型.</p> <p>原因有三.</p> <p>首先虽然String和NSString有着良好的互相转换的特性, 但是现在Cocoa所有的API都接受和返回String类型. 我们没有必要也不必给自己凭空添加麻烦去把框架中返回的字符串做一遍转换, 既然Cocoa鼓励使用String, 并且为我们提供了足够的操作String的方法, 那为什么不直接使用呢?</p> <p>其次, 因为在Swift中String是struct, 相比起NSObject的NSString类来说, 更切合字符串的"不变"这一特性. 通过配合常量赋值(let), 这种不变性在多线程编程时就非常重要了, 它从原理上将程序员从内存访问和操作顺序的担忧中解放出来. 另外, 在不触及NSString特有操作和动态特性的时候, 使用String的方法, 在性能上也会有所提升.</p> <p>最后, 因为String里的String.CharacterView实现了像CollectionType这样的协议, 因此有些Swift的语法特性只有String才能使用, 而NSString是没有的. 一个典型就是for...in的枚举, 我们可以写:</p> <pre> <code class="language-swift">let levels = "ABCDE" for i in levels.characters { print(i) } // 输出: // ABCDE</code></pre> <p>而如果转换为NSString的话, 是无法使用character并且进行枚举的.</p> <p>不过也有例外的情况. 有一些NSString的方法在String中并没有实现, 一个很有用的就是在iOS8中新加的containsString. 我们想使用这个API来简单地确定某个字符串包括一个子字符串时, 只能先将其转为NSString:</p> <pre> <code class="language-swift">if (levels as NSString).contains("BC") { print("包含字符串") } // 输出: // 包含字符串</code></pre> <p>Swift的String没有containsString是一件很奇怪的事情, 理论上应该不存在实现的难度, 希望只是Apple一时忘了这个新加的API把. 当然你也可以自行用扩展的方式在自己的代码库为String添加这个方法. 当然, 还有一些其他的像length和characterIndex: 这样的API也没有String的版本, 这主要是因为String和NSString在处理编码上的差异导致的.</p> <p>Swift3中Apple已经为String添加了contains方法. 现在我们可以直接写levels.contains("BC")这样的代码了.</p> <p>使用String唯一一个比较麻烦的地方在于它和Range的配合. 在NSString中, 我们在匹配字符串的时候通常使用NSRange来表征结果或者作为输入. 而在使用String的对应API时, NSRange也会被映射成它在Swift中且对应String的特殊版本: Range<String.Index>. 这有时候会让人非常讨厌:</p> <pre> <code class="language-swift">let levels = "ABCDE" let nsRange = NSMakeRange(1, 4) // 编译错误 // Cannot convert value of type `NSRanve` to expected argument type 'Range<Index>' // levels.replacingCharacters(in: nsRange, with: "AAAA") let indexPositionOne = levels.characters.index(levels.startIndex, offsetBy: 1) let swiftRange = indexPositionOne ..< levels.characters.index(levels.startIndex, offsetBy: 5) levels.replacingCharacters(in: swiftRange, with: "AAAA") // 输出: // AAAA</code></pre> <p>一般来说, 我们可能更愿意和基于Int的NSRange一起工作, 而不喜欢使用麻烦的Range<String.Index>. 这种情况下, 将String转为NSString也许是个不错的额选择:</p> <pre> <code class="language-swift">let nsRange = NSMakeRange(1, 4) (levels as NSString).replacingCharacters(in: nsRange, with: "AAAA")</code></pre> <h3>Tip51 UnsafePointer</h3> <p>Swift本身从设计上来说是一门非常安全的语言, 在Swift的思想中, 所有的引用或者变量的类型都是确定并且正确对应它们的实际类型的, 你应当无法进行任意的类型转换, 也不能直接通过指针做出一些出格的事情. 这种安全性在日常的程序开发中对于避免不必要的bug, 以及迅速而且稳定地找出代码错误是非常有帮助的. 但是凡事都有两面性, 在高安全的同时, Swift也相应地丧失了部分灵活性.</p> <p>现阶段想要完全抛弃C的一套东西还是相当困难的, 特别是在很多上古级别的C API框架还在使用(或者被间接使用). 开发者, 尤其是偏向较底层的框架的考法这不得不面临着与C API打交道的时候, 还是无法绕开指针的概念, 而指针在Swift中其实并不被鼓励, 语言标准中也是完全没有与指针完全等同的概念的. 为了与庞大的C系帝国进行合作, Swift定义了一套对C语言指针的访问和转换方法, 那就是UnsafePointer和它的一系列变体. 对于使用C API时如果遇到接受内存地址作为参数, 或者返回是内存地址的情况, 在Swift里会将它们转为UnsafePointer<Type>的类型, 比如说如果某个API在C中是这样的话:</p> <pre> <code class="language-swift">void menthod(const int *num) { printf("%d", *num) }</code></pre> <p>其对应的Swift方法应该是:</p> <pre> <code class="language-swift">func method(_ num: UnsafePointer<CInt>) { print(num.pointee) }</code></pre> <p>我们这个tip所说的UnsafePointer, 就是Swift中专门针对指针的转换. 对于其他的C中基础类型, 在Swift中对应的类型都遵循统一的命名规则: 在前面加上一个字母C并将原来的第一个字母大写: 比如int, bool和char的对应类型分别是CInt CBool和CChar. 在上面的C方法中, 我们接受一个int的指针, 转换到Swift里所对应的就是一个CInt的UnsafePointer类型. 这里原来的C API中已经指明了输入的num指针是不可变的(const), 因此在Swift中我们与之对应的是UnsafePointer这个不可变版本. 如果只是一个普通的可变指针的话, 我们可以使用UnsafeMutablePointer来对应:</p> <pre> <code class="language-swift">C API Swift API const Type * UnsafePointer Type * UnsafeMutablePointer</code></pre> <p>在C中, 对某个指针进行取值使用的是 * , 而在Swift中我们可以使用memory属性来读取相应内存中存储的内容. 通过传入指针地址进行方法调用的时候就都比较相似了, 都是在前面加上&符号, C的版本和Swift的版本只在声明变量的时候有所区别:</p> <pre> <code class="language-swift">// C int a = 123; method(&a); // 输出123 // Swift var a: CInt = 123 method(&a) // 输出123</code></pre> <p>遵守这些原则, 使用UnsafePointer在Swift中进行C API的调用应该就不会有很大问题了.</p> <p>另外一个重要的课题是如何在指针的内容和实际的值之间进行转换. 比如我们如果由于某种原因需要涉及到直接使用CFArray的方法来获取数组中元素的时候, 我们会用到这个方法:</p> <pre> <code class="language-swift">func CFArrayGetValueAtIndex(theArray: CFArray!, idx: CFIndex) -> UnsafePointer<Void></code></pre> <p>因为CFArray中是可以存放任意对象的, 所以这里的返回是一个任意对象的指针, 相当于C中的void *. 这显然不是我们想要的东西. Swift中为我们提供了一个强制转换的方法unsafeBitCast, 通过下面的代码, 我们可以看到应当如何使用类似这样的API, 将一个指针强制按位转成所需类型的对象:</p> <pre> <code class="language-swift">let arr = NSArray(object: "meow") let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), to: CFString.self) // str = "meow"</code></pre> <p>unsafeBitCast会将第一个参数的内容按照第二个参数的类型进行转换, 而不去关心实际是不是可行, 这也正是UnsafePointer的不安全所在, 因为我们不必遵守类型转换的检查, 而拥有了在指针层面直接操作内存的机会.</p> <p>其实说了这么多, Apple将直接的指针访问冠以Unsafe的前缀, 就是提醒我们: 这些东西不安全, 亲们能不用就别用了吧(作为Apple, 另一个重要的考虑是如果避免指针的话可以减少很多系统漏洞)! 在日常开发中, 我们确实不太需要经常和这些东西打交道(除了传入NSError指针这个历史遗留问题以外, 而且在Swift2.0中也已经使用异常机制替代了NSError). 总之, 尽可能地在高抽象层级编写代码, 会是高效和正确的有力保证. 无数先辈已经用血淋淋的教训告诉我们, 要避免去做这样的不安全的操作, 除非你确实知道你在做的是什么.</p> <h3>Tip52 C指针内存管理</h3> <p>C指针在Swift中被冠名以unsafe的另一个原因使无法对其进行自动的内存管理. 和Unsafe类的指针工作的时候, 我们需要像ARC时代之前那样手动地来申请和释放内存, 以保证程序不会出现泄露或是因为访问已释放内存而造成崩溃.</p> <p>我们如果想要声明, 初始化, 然后使用一个指针的话, 完整的做法是使用allocate和initialize来创建. 如果一不小心, 就很容易写成下面这样:</p> <pre> <code class="language-swift">class MyClass { var a = 1 deinit { print("deinit") } } var pointer: UnsafeMutablePointer<MyClass>! pointer = UnsafeMutablePointer<MyClass>.allocate(capacity: 1) pointer.initialize(to: MyClass()) print(pointer.pointee.a) // 1 pointer = nil</code></pre> <p>虽然我们最后将pointer置为nil, 但是由于UnsafeMutablePointer并不会自动进行内存管理, 因此其实pointer所指向的内存是没有被释放和回收的(这可以从MyClass的deinit没有被调用来加以证实) 这造成了内存泄露. 正确的做法是为pointer加入deinitialize和deallocate, 它们分别会释放指针指向的内存的对象以及指针自己本身:</p> <pre> <code class="language-swift">var pointer: UnsafeMutablePointer<MyClass>! pointer = UnsafeMutablePointer<MyClass>.allocate(capacity: 1) pointer.initialize(to: MyClass()) print(pointer.pointee.a) pointer.deinitialize() pointer.deallocate(capacity: 1) pointer = nil // 输出: // 1 // deinit</code></pre> <p>如果我们在deallocate之后再去访问pointer或者再次调用deallocate的话, 迎接我们的自然是崩溃. 这并不出意料之外, 相信有过手动管理经验的读者都会对这种场景非常熟悉了.</p> <p>在手动处理这类指针的内存管理时, 我们需要遵循的一个基本原则就是谁创建谁释放. deallocate与deinitialize应该要和allocate与initialize成对出现, 如果不是你创建的指针, 那么一般来说你就不需要去释放它. 一种最常见的例子就是如果我们是通过调用了某个方法得到的指针, 那么除非文档或者负责这个方法的开发者明确告诉你应该由使用者进行释放, 否则都不应该去试图管理它的内存状态:</p> <pre> <code class="language-swift">var x: UnsafeMutablePointer<tm>! var t = time_t() time(&t) x = localtime(&t) x = nil</code></pre> <p>最后, 虽然在本节的例子中使用的都是allocate和deallocate的情况, 但是指针的内存申请也可以使用malloc或者calloc来完成, 这种情况下再释放时我们需要对应使用free而不是deallocate.</p> <p>大概就那么多, 祝你好运!</p> <h3>Tip53 COpaquePointer 和 C convention</h3> <p>在C中有一类指针, 你在头文件中无法找到具体的定义, 只能拿到类型的名字, 而所有的实现细节都是隐藏的. 这类指针在C或C++中被叫做不透明指针(Opaque Pointer), 顾名思义, 它的实现和表意对使用者来说是不透明的.</p> <p>我们在这里不想过多讨论C中不透明指针的应用场景和特性, 毕竟这是一本关于Swift的书. 在Swift中对应这类不透明指针的类型是COPaquePointer, 它用来表示那些在Swift中无法进行类型描述的C指针. 那些能够确定类型的指针所表示的是其指向的内存是可以用某个Swift中的类型来描述的, 因此都使用更准确的UnsafePointer<T>来存储. 而对于另外那些Swift无法表述的指针, 就统一写为COpaquePointer, 以作补充.</p> <p>在Swift早期beta的时候, 曾经有不少API返回或者接受的是COpaquePointer类型. 但是随着Swift的逐渐完善, 大部分涉及到指针的API里的COpaquePointer都被正确地归类到了合适的Unsafe指针中, 因此现在在开发中可能很少能再看到COpaquePointer了. 最多的使用场景可能就是COpaquePointer和某个特定的Unsafe之间的转换了, 我们可以分别使用这两个类型的初始化方法将一个指针转换从某个类型强制地转为另一个类型:</p> <pre> <code class="language-swift">public struct UnsafeMutablePointer<Memory>: Equatable, Hashable{ //... init(_ other: COpaquePointer) { //... } } public struct COpaquePointer: Equatable, Hashable, NilLiteralConvertible { //... init<T>(_ source: UnsafePointer<T>) { //... } }</code></pre> <p>COpaquePointer在Swift中扮演的是指针转换的中间人的角色, 我们可以通过这个类型在不同指针类型间进行转换. 当然了, 这些转换都是不安全的, 除非你知道自己在干什么, 以及有十足的把握, 否则不要这么做!</p> <p>另外一种重要的指针形式是指向函数的指针, 在C中这种情况也并不少见, 即一块存储了某个函数实际所在的位置的内存空间. 从Swift2.0开始, 与这类指针可以被转化为闭包, 不过和其他普通闭包不同, 我们需要为它添加上@convention标注.</p> <p>举个例子, 如果我们在C中有这样一个函数:</p> <pre> <code class="language-swift">int cFunction(int (callback)(int x, int y)) { return callback(1, 2); }</code></pre> <p>这个函数接收一个callback, 这个callback有两个int类型的参数, cFunction本身返回这个callback的结果. 如果我们想在Swift中使用这个C函数的话, 应该这样写:</p> <pre> <code class="language-swift">let callback: @convention(c) (Int32, Int32) -> Int32 = { (x, y) -> Int32 in return x + y } let result = cFunction(callback) print(result) // 输出 // 3</code></pre> <p>在没有歧义的情况下, 我们甚至可以省掉这个标注, 而直接将它以一个Swift闭包的形式传递给C:</p> <pre> <code class="language-swift">let result = cFunction { (x, y) -> Int32 in return x + y } print(result) // 输出: // 3</code></pre> <p>完美, 你甚至感觉不到自己是在和C打交道!</p> <h3>Tip54 GCD 和延时调用</h3> <p>GCD是一种非常方便的使用多线程的方式. 通过使用GCD, 我们可以在确保尽量简单的语法的前提下进行灵活的多线程编程. 在"复杂必死"的多线程编程中, 保持简单就是避免错误的金科玉律. 好消息是在Swift中是可以无缝使用GCD的API的, 而且得益于闭包特性的加入, 使用起来比之前在Objective-C中更加简单方便. Swift3中更是抛弃了传统的基于C的GCD API, 采用了更为先进的书写方式. 在这里我不打算花费很多时间介绍GCD的语法和要素, 如果这么做的话就可以专门为GCD写上一节了. 在下面我给出了一个日常里最通常会使用到的例子(说这个例子能覆盖到日常的GCD使用的50%以上也不为过), 来展示一下Swift里的GCD调用会是什么样子:</p> <pre> <code class="language-swift">// 创建目标队列 let workingQueue = DispatchQueue(label: "my_queue") // 派发到刚创建的队列中, GCD会负责进行线程调度 workingQueue.async { // 在workingQueue中异步进行 print("努力工作") Thread.sleep(forTimeInterval: 2) // 模拟两秒的执行时间 DispatchQueue.main.async { // 返回到主线程更新UI print("结束工作, 更新UI") } }</code></pre> <p>因为UIKit是只能在主线程工作的, 如果我们在主线程进行繁重的工作的话, 就会导致app出现"卡死"的现象: UI不能更新, 用户输入无法响应等等, 是非常糟糕的用户体验. 为了避免这种情况的出现, 对于繁重(如图像加滤镜等)或会很长时间才能完成的(如从网络下载图片)处理, 我们应该把它们放到后台线程进行, 这样在用户看来UI还是可以交互的, 也不会出现卡顿. 在工作进行完成后, 我们需要更新UI的话, 必须回到主线程进行(牢记UI相关的工作都需要在主线程执行, 否则可能发生不可预知的错误).</p> <p>在日常的开发工作中, 我们经常会遇到这样的需求: 在xx秒后执行某个方法. 比如切换界面2秒后开始播一段动画, 或者提示框出现3秒后自动消失等等. 以前在Objective-C中, 我们可以使用一个NSObject的实例方法, -performSelector:withObject:afterDelay: 来指定在若干时间后执行某个selector. 在Swift2之前, 如果你新建一个Swift项目, 并且试图使用这个方法(或者这个方法的其他一切变形)的话, 会发现这个方法并不存在. 在Swift2中虽然这一系列performSelector的方法被加回了标准库, 但是由于Swift中创建一个selector并不是一件安全的事情(你需要通过字符串来创建, 这在之后代码改动时会很危险), 所以最好尽可能的话避免使用这个方法. 另外, 原来的performSelector: 这套东西在ARC下并不是安全的. ARC为了确保参数在方法运行期间的存在, 在无法准确确定参数内存的情况的时候, 会将输入参数在方法开始时先进行retain, 然后在最后release. 而对于performSelector: 这个方法我们并没有机会为被调用的方法指定参数, 于是被调用的selector的输入有可能会是指向未知的垃圾内存地址, 然后...HOHO, 要命的是这种崩溃还不能每次重现, 想调试? 见鬼去吧...</p> <p>但是如果不论如何, 我们都还想继续做延时调用的话应该怎么办呢? 最容易想到的是使用Timer来创建一个若干秒后调用一次的计时器. 但是这么做我们需要创建新的对象, 和一个本来并不相干的Timer类扯上关系, 同时也会用到Objective-C的运行时特性去查找方法等等, 总觉着有点笨重. 其实GCD里有一个很好用的延时调用我们可以加以利用写出很漂亮的方法俩, 那就是asyncAfter. 最简单的使用方法看起来是这样的:</p> <pre> <code class="language-swift">let time: TimeInterval = 2.0 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time) { print("2秒后输出") }</code></pre> <p>代码非常简单, 并没什么值得详细说明的. 只是每次写这么多的话也挺累的, 在这里我们可以稍微将它封装的好用一些, 最好再加上取消的功能. 在iOS8中GCD得到了惊人的进化, 现在我们可以通过将一个闭包封装到DispatchWorkItem对象中, 然后对其发送cancel, 来取消一个正在等待执行的block. 取消一个任务这样的特性, 这在以前是NSOperation的专利, 但是现在我们使用GCD也能达到同样的目的了. 这里我们不适用这个方式, 而是通过捕获一个cancel标识变量来实现delay call的取消, 整个封装也许有点长, 但是我还是推荐一读. 大家也可以把它当做练习材料检验一下自己的Swift基础语法的掌握和理解的情况:</p> <pre> <code class="language-swift">typealias Task = (_ cancel: Bool) -> Void func delay(_ time: TimeInterval, task: @escaping() -> ()) -> Task? { func dispatch_later(block: @escaping() -> ()) { let t = DispatchTime.now() + time DispatchQueue.main.asyncAfter(deadline: t, execute: block); } var closure: (() -> Void)? = task var result: Task? let delayedClosure: Task = { cancel in if let internalClosure = closure { if (cancel == false) { DispatchQueue.main.async(execute: internalClosure) } } closure = nil result = nil } result = delayedClosure dispatch_later { if let delayedClosure = result { delayedClosure(false) } } return result } func cancel(_ task: Task?) { task?(true) } // 使用的时候就很简单了, 我们想在2秒以后干点儿什么的话: delay(2) { print("2秒后输出")} // 想要取消的话, 我们可以先保留一个队Task的引用, 然后调用cancel: let task = delay(5) {print("拨打110")} // 仔细想一想.. // 还是取消为妙.. cancel(task)</code></pre> <p>这段代码过于冗长, 对于初入Swift的同学可能理解上有些不便, 我们一步步拆解来看:</p> <pre> <code class="language-swift">typealias Task = (_ cancel: Bool) -> Void</code></pre> <p>其实和Objective-C中 typedef void(^Task)(BOOL cancel); 是相同的. 可以理解为Task就是一个Block.</p> <pre> <code class="language-swift">func delay(_ time: TimeInterval, task: @escaping() -> ()) -> Task?</code></pre> <p>我们来看下这个延迟的函数, 函数有两个参数, 第一个参数是延迟时长, 第二个参数是延迟操作(类型是无参数无返回值的闭包) 函数的返回值就是之前定义的Task. 不确定是否有值所以定义为可选类型. @escaping是指 可逃逸的闭包, 也就是不受作用域的控制.</p> <pre> <code class="language-swift">func dispatch_later(block: @escaping() -> ()) { let t = DispatchTime.now() + time DispatchQueue.main.asyncAfter(deadline: t, execute: block); }</code></pre> <p>这个内部的函数接受一个无参数无返回值的闭包作为参数, 执行函数时主线程异步延迟执行传入闭包.</p> <pre> <code class="language-swift">var closure: (() -> Void)? = task // 定义了一个变量来接受传入的task操作 () -> Void与() -> () 本质上是相同的 var result: Task? // 定义了一个可选Task的变量 let delayedClosure: Task = { // 定义了一个Task类型的常量 以下为Task闭包操作 cancel in // cancel 为Task 传入Bool参数 if let internalClosure = closure { // 进行可选绑定 if (cancel == false) { // 判断是否取消执行 DispatchQueue.main.async(execute: internalClosure) 执行Task操作 } } closure = nil //置空防止线程以意外 result = nil //置空防止线程以意外 } result = delayedClosure // 把该Task赋值给result进行返回 dispatch_later { // 执行内部函数进行主线程异步延时 延时后执行下方代码 if let delayedClosure = result { // 可选绑定确保result 有值 delayedClosure(false) 执行常量函数 } }</code></pre> <pre> <code class="language-swift">外部函数, 取消执行. func cancel(_ task: Task?) { //取消执行 参数为之前函数的返回值. task?(true) 传入参数为true, result置为nil 停止线程. 停止执行. }</code></pre> <p>不管多么复杂的函数, 只要进行拆解都能通俗易懂, 加油了 各位!</p> <p> </p> <p>来自:http://www.jianshu.com/p/7fd963f47773</p> <p> </p>