Swift 輕松掌握喵神高階心法 X
Tip48 @autoreleasepool
Swift在內存管理上使用的是自動引用計數(ARC)的一套方法, 在ARC中雖然不需要手動地調用像是retain, release或者是autorelease這樣的方法來管理引用計數, 但是這些方法還是都會被調用的 --只不過是編譯器在編譯時在合適的地方幫我們加入了而已. 其中retain和release都很直接, 就是將對象的引用計數加一或者減一. 但是autorelease就比較特殊一些, 它會將接受該消息的對象放到一個預先建立的自動釋放池(auto release pool)中, 并在自動釋放池收到drain消息時將這些對象的引用計數減一, 然后將它們從池子中移除(這一過程形象地稱為"抽干池子").
在app中, 整個主線程其實是跑在一個自動釋放池里的, 并且在每個主Runloop結束時進行drain操作. 這是一種必要的延遲釋放的方式, 因為我們有時候需要確保在方法內部初始化的生成的對象在被返回后別人還能使用, 而不是立即被釋放掉.
在Objective-C中, 建立一個自動釋放池的語法很簡單, 使用@autoreleasepool就行了. 如果你新建一個Objective-C項目, 可以看到main.m中就有我們剛才說到的整個項目的autoreleasepool:
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = UIApplicationMain (
argc,
argv,
nil,
NSStringFromClass([AppDelegate class]));
return retVal;
}
}
更進一步, 其實@autoreleasepool在編譯時會被展開為NSAutoreleasPool, 并附帶drain方法的調用.
而在Swift項目中, 因為有了@UIApplicationMain, 我們不再需要main文件和main函數, 所以原來的整個程序的自動釋放池就不存在了. 即時我們使用main.swift來作為程序的入口時, 也是不需要自己再添加自動釋放池的.
但是在一種情況下我們還是希望自動釋放, 那就是在面對在一個方法作用域中要生成大量的autorelease對象的時候. 在Swift1.0時, 我們可以寫這樣的代碼:
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)
}
}
}
dataWithContentsOfFile返回的是autorelease的對象, 因為我們一直處在循環中, 因此它們將一直沒有機會被釋放. 如果數量太多而且數據太大的時候, 很容易因為內存不足而崩潰. 在Instruments下可以看到內存alloc的情況:
這顯然是一幅很不妙的情景. 在面對這種情況的時候, 正確的處理方法是在其中加入一個自動釋放池, 這樣我們就可以在循環進行到某個特定的時候釋放內存, 保證不會因為內存不足而導致應用崩潰. 在Swift中我們也是能使用autoreleasepool的 --雖然語法上略有不同. 相比于原來在Objective-C中的關鍵字, 現在它變成了一個接受閉包的方法:
func autoreleasepool(code:() -> ())
利用尾隨閉包的寫法, 很容易就能在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)
}
}
}
}
這樣改動以后, 內存分配就沒有什么憂慮了:
這里我們每一次循環都生成了一個自動釋放池, 雖然可以保證內存使用達到最小, 但是釋放過于頻繁也會帶來潛在的性能憂慮. 一個折中的方法是將循環分隔開加入自動釋放池, 比如每10次循環對應一次自動釋放, 這樣能減少帶來的性能損失.
其實對于這個特定的例子, 我們并不一定需要加入自動釋放. 在Swift中更提倡的是用初始化方法而不是用像上面那樣的類方法來生成對象, 而且從Swift1.1開始, 因為加入了可以返回nil的初始化方法, 像上面例子中那樣的工廠方法都已經從API中刪除了. 今后我們都應該這樣寫:
let data = NSData(contentsOfFile: path)
使用初始化方法的話, 我們就不需要面臨自動釋放的問題了, 每次在超過作用域后, 自動內存管理都將為我們處理好內存相關的事情.
Tip49 值類型和引用類型
Swift的類型分為值類型和引用類型兩種, 值類型在傳遞和賦值時將進行復制, 而引用類型則只會使用引用對象的一個"指向". Swift中的struct和enum定義的類型是值類型, 使用class定義的為值類型. 很有意思的是, Swift中的所有的內建類型都是值類型, 不僅包括了傳統意義像Int, Bool這些, 甚至連String, Array以及Dictionary都是值類型的. 這在程序設計上絕對算得上一個震撼的改動, 因為據我所知現在流行的編程語言中, 像數組和字典這樣的類型, 機會清一色都是引用類型.
那么使用值類型有什么好處呢? 相較于傳統的引用類型來說, 一個很顯而易見的優勢就是減少了推上內存分配和回收的次數. 首先我們需要知道, Swift的值類型, 特別是數組和字典這樣的容器, 在內存管理上經過了精心的設計. 值類型的一個特點是在傳遞和賦值時進行復制, 每次復制肯定會產生額外的開銷, 但是在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>
這么折騰一圈下來, 其實我們只在第一句a初始化賦值時發生了內存分配, 而之后的b, c甚至傳遞到test方法內的arr, 和最開始的a在物理內存上都是同一個東西. 而且這個a還只在棧空間上, 于是這個過程對于數組來說, 只發生了指針移動, 而完全沒有堆內存的分配和釋放的問題, 這樣的運行效率可以說極高.
值類型被復制的時機是值類型的內容發生改變時, 比如下面在b中又加入了一個數, 此時值復制就是必須的了:
var a = [1, 2, 3]
var b = a
b.append(5)
//此時a和b的內存地址不再相同
值類型在復制時, 會將存儲在其中的值類型一并進行復制, 而對于其中的引用類型的話, 則只復制一份引用. 這是合理的行為, 因為我們不會希望引用類型莫名其妙地引用到了我們設定以外其他對象:
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>
雖然將數組和字典設計為值類型最大的考慮是為了線程安全, 但是這樣的設計子啊存儲元素或條目數量較少時, 給我們帶來了另一個優點, 那就是非常高效, 因為"一旦賦值就不太會變化"這種使用情景在Cocoa框架中占有絕大多數的, 這有效的減少了內存的分配和回收. 但是在少數情況下, 我們顯然也可能會在數組或者字典中存儲非常多的東西, 并且還要對其中的內容進行添加或者刪除. 在這時, Swift內建的值類型的容器類型在每次操作時都需要復制一遍, 即時是存儲的都是引用類型, 在復制時我們還是需要存儲大量的引用, 這個開銷就變得不容忽視了. 幸好我們還有Cocoa種的引用類型的容器類來對應這種情況, 那就是NSMutableArray和NSMutableDictionary.
所以, 在使用數組和字典時的最佳實踐應該是, 按照具體的數據規模和操作特點到時是使用值類型的容器還是引用類型的容器: 在需要處理大量數據并且頻繁操作(增減)其中元素時, 選擇NSMutableAttay和NSMutableDictionary會更好, 而對于容器內條目小而容器本身數目多的情況, 應該使用Swift語言內建的Array和Dictionary.
Tip50 String 還是 NSString
既然像String這樣的Swift的類型和Foundation的對應的類是可以無縫切換的, 那么我們在使用和選擇的時候, 有沒有什么需要特別注意的呢?
簡單來說, 沒有特別需要注意的, 但是盡可能的話還是使用原生的String類型.
原因有三.
首先雖然String和NSString有著良好的互相轉換的特性, 但是現在Cocoa所有的API都接受和返回String類型. 我們沒有必要也不必給自己憑空添加麻煩去把框架中返回的字符串做一遍轉換, 既然Cocoa鼓勵使用String, 并且為我們提供了足夠的操作String的方法, 那為什么不直接使用呢?
其次, 因為在Swift中String是struct, 相比起NSObject的NSString類來說, 更切合字符串的"不變"這一特性. 通過配合常量賦值(let), 這種不變性在多線程編程時就非常重要了, 它從原理上將程序員從內存訪問和操作順序的擔憂中解放出來. 另外, 在不觸及NSString特有操作和動態特性的時候, 使用String的方法, 在性能上也會有所提升.
最后, 因為String里的String.CharacterView實現了像CollectionType這樣的協議, 因此有些Swift的語法特性只有String才能使用, 而NSString是沒有的. 一個典型就是for...in的枚舉, 我們可以寫:
let levels = "ABCDE"
for i in levels.characters {
print(i)
}
// 輸出:
// ABCDE
而如果轉換為NSString的話, 是無法使用character并且進行枚舉的.
不過也有例外的情況. 有一些NSString的方法在String中并沒有實現, 一個很有用的就是在iOS8中新加的containsString. 我們想使用這個API來簡單地確定某個字符串包括一個子字符串時, 只能先將其轉為NSString:
if (levels as NSString).contains("BC") {
print("包含字符串")
}
// 輸出:
// 包含字符串</code></pre>
Swift的String沒有containsString是一件很奇怪的事情, 理論上應該不存在實現的難度, 希望只是Apple一時忘了這個新加的API把. 當然你也可以自行用擴展的方式在自己的代碼庫為String添加這個方法. 當然, 還有一些其他的像length和characterIndex: 這樣的API也沒有String的版本, 這主要是因為String和NSString在處理編碼上的差異導致的.
Swift3中Apple已經為String添加了contains方法. 現在我們可以直接寫levels.contains("BC")這樣的代碼了.
使用String唯一一個比較麻煩的地方在于它和Range的配合. 在NSString中, 我們在匹配字符串的時候通常使用NSRange來表征結果或者作為輸入. 而在使用String的對應API時, NSRange也會被映射成它在Swift中且對應String的特殊版本: Range<String.Index>. 這有時候會讓人非常討厭:
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>
一般來說, 我們可能更愿意和基于Int的NSRange一起工作, 而不喜歡使用麻煩的Range<String.Index>. 這種情況下, 將String轉為NSString也許是個不錯的額選擇:
let nsRange = NSMakeRange(1, 4)
(levels as NSString).replacingCharacters(in: nsRange, with: "AAAA")
Tip51 UnsafePointer
Swift本身從設計上來說是一門非常安全的語言, 在Swift的思想中, 所有的引用或者變量的類型都是確定并且正確對應它們的實際類型的, 你應當無法進行任意的類型轉換, 也不能直接通過指針做出一些出格的事情. 這種安全性在日常的程序開發中對于避免不必要的bug, 以及迅速而且穩定地找出代碼錯誤是非常有幫助的. 但是凡事都有兩面性, 在高安全的同時, Swift也相應地喪失了部分靈活性.
現階段想要完全拋棄C的一套東西還是相當困難的, 特別是在很多上古級別的C API框架還在使用(或者被間接使用). 開發者, 尤其是偏向較底層的框架的考法這不得不面臨著與C API打交道的時候, 還是無法繞開指針的概念, 而指針在Swift中其實并不被鼓勵, 語言標準中也是完全沒有與指針完全等同的概念的. 為了與龐大的C系帝國進行合作, Swift定義了一套對C語言指針的訪問和轉換方法, 那就是UnsafePointer和它的一系列變體. 對于使用C API時如果遇到接受內存地址作為參數, 或者返回是內存地址的情況, 在Swift里會將它們轉為UnsafePointer<Type>的類型, 比如說如果某個API在C中是這樣的話:
void menthod(const int *num) {
printf("%d", *num)
}
其對應的Swift方法應該是:
func method(_ num: UnsafePointer<CInt>) {
print(num.pointee)
}
我們這個tip所說的UnsafePointer, 就是Swift中專門針對指針的轉換. 對于其他的C中基礎類型, 在Swift中對應的類型都遵循統一的命名規則: 在前面加上一個字母C并將原來的第一個字母大寫: 比如int, bool和char的對應類型分別是CInt CBool和CChar. 在上面的C方法中, 我們接受一個int的指針, 轉換到Swift里所對應的就是一個CInt的UnsafePointer類型. 這里原來的C API中已經指明了輸入的num指針是不可變的(const), 因此在Swift中我們與之對應的是UnsafePointer這個不可變版本. 如果只是一個普通的可變指針的話, 我們可以使用UnsafeMutablePointer來對應:
C API Swift API
const Type * UnsafePointer
Type * UnsafeMutablePointer
在C中, 對某個指針進行取值使用的是 * , 而在Swift中我們可以使用memory屬性來讀取相應內存中存儲的內容. 通過傳入指針地址進行方法調用的時候就都比較相似了, 都是在前面加上&符號, C的版本和Swift的版本只在聲明變量的時候有所區別:
// C
int a = 123;
method(&a); // 輸出123
// Swift
var a: CInt = 123
method(&a) // 輸出123</code></pre>
遵守這些原則, 使用UnsafePointer在Swift中進行C API的調用應該就不會有很大問題了.
另外一個重要的課題是如何在指針的內容和實際的值之間進行轉換. 比如我們如果由于某種原因需要涉及到直接使用CFArray的方法來獲取數組中元素的時候, 我們會用到這個方法:
func CFArrayGetValueAtIndex(theArray: CFArray!, idx: CFIndex) -> UnsafePointer<Void>
因為CFArray中是可以存放任意對象的, 所以這里的返回是一個任意對象的指針, 相當于C中的void *. 這顯然不是我們想要的東西. Swift中為我們提供了一個強制轉換的方法unsafeBitCast, 通過下面的代碼, 我們可以看到應當如何使用類似這樣的API, 將一個指針強制按位轉成所需類型的對象:
let arr = NSArray(object: "meow")
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), to: CFString.self)
// str = "meow"
unsafeBitCast會將第一個參數的內容按照第二個參數的類型進行轉換, 而不去關心實際是不是可行, 這也正是UnsafePointer的不安全所在, 因為我們不必遵守類型轉換的檢查, 而擁有了在指針層面直接操作內存的機會.
其實說了這么多, Apple將直接的指針訪問冠以Unsafe的前綴, 就是提醒我們: 這些東西不安全, 親們能不用就別用了吧(作為Apple, 另一個重要的考慮是如果避免指針的話可以減少很多系統漏洞)! 在日常開發中, 我們確實不太需要經常和這些東西打交道(除了傳入NSError指針這個歷史遺留問題以外, 而且在Swift2.0中也已經使用異常機制替代了NSError). 總之, 盡可能地在高抽象層級編寫代碼, 會是高效和正確的有力保證. 無數先輩已經用血淋淋的教訓告訴我們, 要避免去做這樣的不安全的操作, 除非你確實知道你在做的是什么.
Tip52 C指針內存管理
C指針在Swift中被冠名以unsafe的另一個原因使無法對其進行自動的內存管理. 和Unsafe類的指針工作的時候, 我們需要像ARC時代之前那樣手動地來申請和釋放內存, 以保證程序不會出現泄露或是因為訪問已釋放內存而造成崩潰.
我們如果想要聲明, 初始化, 然后使用一個指針的話, 完整的做法是使用allocate和initialize來創建. 如果一不小心, 就很容易寫成下面這樣:
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>
雖然我們最后將pointer置為nil, 但是由于UnsafeMutablePointer并不會自動進行內存管理, 因此其實pointer所指向的內存是沒有被釋放和回收的(這可以從MyClass的deinit沒有被調用來加以證實) 這造成了內存泄露. 正確的做法是為pointer加入deinitialize和deallocate, 它們分別會釋放指針指向的內存的對象以及指針自己本身:
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>
如果我們在deallocate之后再去訪問pointer或者再次調用deallocate的話, 迎接我們的自然是崩潰. 這并不出意料之外, 相信有過手動管理經驗的讀者都會對這種場景非常熟悉了.
在手動處理這類指針的內存管理時, 我們需要遵循的一個基本原則就是誰創建誰釋放. deallocate與deinitialize應該要和allocate與initialize成對出現, 如果不是你創建的指針, 那么一般來說你就不需要去釋放它. 一種最常見的例子就是如果我們是通過調用了某個方法得到的指針, 那么除非文檔或者負責這個方法的開發者明確告訴你應該由使用者進行釋放, 否則都不應該去試圖管理它的內存狀態:
var x: UnsafeMutablePointer<tm>!
var t = time_t()
time(&t)
x = localtime(&t)
x = nil
最后, 雖然在本節的例子中使用的都是allocate和deallocate的情況, 但是指針的內存申請也可以使用malloc或者calloc來完成, 這種情況下再釋放時我們需要對應使用free而不是deallocate.
大概就那么多, 祝你好運!
Tip53 COpaquePointer 和 C convention
在C中有一類指針, 你在頭文件中無法找到具體的定義, 只能拿到類型的名字, 而所有的實現細節都是隱藏的. 這類指針在C或C++中被叫做不透明指針(Opaque Pointer), 顧名思義, 它的實現和表意對使用者來說是不透明的.
我們在這里不想過多討論C中不透明指針的應用場景和特性, 畢竟這是一本關于Swift的書. 在Swift中對應這類不透明指針的類型是COPaquePointer, 它用來表示那些在Swift中無法進行類型描述的C指針. 那些能夠確定類型的指針所表示的是其指向的內存是可以用某個Swift中的類型來描述的, 因此都使用更準確的UnsafePointer<T>來存儲. 而對于另外那些Swift無法表述的指針, 就統一寫為COpaquePointer, 以作補充.
在Swift早期beta的時候, 曾經有不少API返回或者接受的是COpaquePointer類型. 但是隨著Swift的逐漸完善, 大部分涉及到指針的API里的COpaquePointer都被正確地歸類到了合適的Unsafe指針中, 因此現在在開發中可能很少能再看到COpaquePointer了. 最多的使用場景可能就是COpaquePointer和某個特定的Unsafe之間的轉換了, 我們可以分別使用這兩個類型的初始化方法將一個指針轉換從某個類型強制地轉為另一個類型:
public struct UnsafeMutablePointer<Memory>: Equatable, Hashable{
//...
init(_ other: COpaquePointer) {
//...
}
}
public struct COpaquePointer: Equatable, Hashable, NilLiteralConvertible {
//...
init<T>(_ source: UnsafePointer<T>) {
//...
}
}</code></pre>
COpaquePointer在Swift中扮演的是指針轉換的中間人的角色, 我們可以通過這個類型在不同指針類型間進行轉換. 當然了, 這些轉換都是不安全的, 除非你知道自己在干什么, 以及有十足的把握, 否則不要這么做!
另外一種重要的指針形式是指向函數的指針, 在C中這種情況也并不少見, 即一塊存儲了某個函數實際所在的位置的內存空間. 從Swift2.0開始, 與這類指針可以被轉化為閉包, 不過和其他普通閉包不同, 我們需要為它添加上@convention標注.
舉個例子, 如果我們在C中有這樣一個函數:
int cFunction(int (callback)(int x, int y)) {
return callback(1, 2);
}
這個函數接收一個callback, 這個callback有兩個int類型的參數, cFunction本身返回這個callback的結果. 如果我們想在Swift中使用這個C函數的話, 應該這樣寫:
let callback: @convention(c) (Int32, Int32) -> Int32 = {
(x, y) -> Int32 in
return x + y
}
let result = cFunction(callback)
print(result)
// 輸出
// 3</code></pre>
在沒有歧義的情況下, 我們甚至可以省掉這個標注, 而直接將它以一個Swift閉包的形式傳遞給C:
let result = cFunction {
(x, y) -> Int32 in
return x + y
}
print(result)
// 輸出:
// 3</code></pre>
完美, 你甚至感覺不到自己是在和C打交道!
Tip54 GCD 和延時調用
GCD是一種非常方便的使用多線程的方式. 通過使用GCD, 我們可以在確保盡量簡單的語法的前提下進行靈活的多線程編程. 在"復雜必死"的多線程編程中, 保持簡單就是避免錯誤的金科玉律. 好消息是在Swift中是可以無縫使用GCD的API的, 而且得益于閉包特性的加入, 使用起來比之前在Objective-C中更加簡單方便. Swift3中更是拋棄了傳統的基于C的GCD API, 采用了更為先進的書寫方式. 在這里我不打算花費很多時間介紹GCD的語法和要素, 如果這么做的話就可以專門為GCD寫上一節了. 在下面我給出了一個日常里最通常會使用到的例子(說這個例子能覆蓋到日常的GCD使用的50%以上也不為過), 來展示一下Swift里的GCD調用會是什么樣子:
// 創建目標隊列
let workingQueue = DispatchQueue(label: "my_queue")
// 派發到剛創建的隊列中, GCD會負責進行線程調度
workingQueue.async {
// 在workingQueue中異步進行
print("努力工作")
Thread.sleep(forTimeInterval: 2) // 模擬兩秒的執行時間
DispatchQueue.main.async {
// 返回到主線程更新UI
print("結束工作, 更新UI")
}
}</code></pre>
因為UIKit是只能在主線程工作的, 如果我們在主線程進行繁重的工作的話, 就會導致app出現"卡死"的現象: UI不能更新, 用戶輸入無法響應等等, 是非常糟糕的用戶體驗. 為了避免這種情況的出現, 對于繁重(如圖像加濾鏡等)或會很長時間才能完成的(如從網絡下載圖片)處理, 我們應該把它們放到后臺線程進行, 這樣在用戶看來UI還是可以交互的, 也不會出現卡頓. 在工作進行完成后, 我們需要更新UI的話, 必須回到主線程進行(牢記UI相關的工作都需要在主線程執行, 否則可能發生不可預知的錯誤).
在日常的開發工作中, 我們經常會遇到這樣的需求: 在xx秒后執行某個方法. 比如切換界面2秒后開始播一段動畫, 或者提示框出現3秒后自動消失等等. 以前在Objective-C中, 我們可以使用一個NSObject的實例方法, -performSelector:withObject:afterDelay: 來指定在若干時間后執行某個selector. 在Swift2之前, 如果你新建一個Swift項目, 并且試圖使用這個方法(或者這個方法的其他一切變形)的話, 會發現這個方法并不存在. 在Swift2中雖然這一系列performSelector的方法被加回了標準庫, 但是由于Swift中創建一個selector并不是一件安全的事情(你需要通過字符串來創建, 這在之后代碼改動時會很危險), 所以最好盡可能的話避免使用這個方法. 另外, 原來的performSelector: 這套東西在ARC下并不是安全的. ARC為了確保參數在方法運行期間的存在, 在無法準確確定參數內存的情況的時候, 會將輸入參數在方法開始時先進行retain, 然后在最后release. 而對于performSelector: 這個方法我們并沒有機會為被調用的方法指定參數, 于是被調用的selector的輸入有可能會是指向未知的垃圾內存地址, 然后...HOHO, 要命的是這種崩潰還不能每次重現, 想調試? 見鬼去吧...
但是如果不論如何, 我們都還想繼續做延時調用的話應該怎么辦呢? 最容易想到的是使用Timer來創建一個若干秒后調用一次的計時器. 但是這么做我們需要創建新的對象, 和一個本來并不相干的Timer類扯上關系, 同時也會用到Objective-C的運行時特性去查找方法等等, 總覺著有點笨重. 其實GCD里有一個很好用的延時調用我們可以加以利用寫出很漂亮的方法倆, 那就是asyncAfter. 最簡單的使用方法看起來是這樣的:
let time: TimeInterval = 2.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time) {
print("2秒后輸出")
}
代碼非常簡單, 并沒什么值得詳細說明的. 只是每次寫這么多的話也挺累的, 在這里我們可以稍微將它封裝的好用一些, 最好再加上取消的功能. 在iOS8中GCD得到了驚人的進化, 現在我們可以通過將一個閉包封裝到DispatchWorkItem對象中, 然后對其發送cancel, 來取消一個正在等待執行的block. 取消一個任務這樣的特性, 這在以前是NSOperation的專利, 但是現在我們使用GCD也能達到同樣的目的了. 這里我們不適用這個方式, 而是通過捕獲一個cancel標識變量來實現delay call的取消, 整個封裝也許有點長, 但是我還是推薦一讀. 大家也可以把它當做練習材料檢驗一下自己的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>
這段代碼過于冗長, 對于初入Swift的同學可能理解上有些不便, 我們一步步拆解來看:
typealias Task = (_ cancel: Bool) -> Void
其實和Objective-C中 typedef void(^Task)(BOOL cancel); 是相同的. 可以理解為Task就是一個Block.
func delay(_ time: TimeInterval, task: @escaping() -> ()) -> Task?
我們來看下這個延遲的函數, 函數有兩個參數, 第一個參數是延遲時長, 第二個參數是延遲操作(類型是無參數無返回值的閉包) 函數的返回值就是之前定義的Task. 不確定是否有值所以定義為可選類型. @escaping是指 可逃逸的閉包, 也就是不受作用域的控制.
func dispatch_later(block: @escaping() -> ()) {
let t = DispatchTime.now() + time
DispatchQueue.main.asyncAfter(deadline: t, execute: block);
}
這個內部的函數接受一個無參數無返回值的閉包作為參數, 執行函數時主線程異步延遲執行傳入閉包.
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) 執行常量函數
}
}
外部函數, 取消執行.
func cancel(_ task: Task?) { //取消執行 參數為之前函數的返回值.
task?(true) 傳入參數為true, result置為nil 停止線程. 停止執行.
}
不管多么復雜的函數, 只要進行拆解都能通俗易懂, 加油了 各位!
來自:http://www.jianshu.com/p/7fd963f47773