iOS開發-Runloop程式設計指南

驢拉磨.png
題圖:瞭解Runloop的工作機制後,我腦中就浮現了“驢拉磨”的經典畫面,Runloop不僅像驢一樣不停拉磨轉圈,而且是頭會偷懶的驢,當磨盤上沒有麥子時,它還會主動休息;而加麥子,清理麥子的過程,就是事件源與Runloop的互動過程。
Runloop在iOS和macOS系統中都是基礎的執行機制,著名的網路庫AFNetworking就使用了Runloop來保證子執行緒沒有活動時直接退出,那麼還有哪些場景需要使用Runloop又如何使用Runloop呢?
本篇將解決以下三個問題, 讓你快速瞭解Runloop的使用:
- 何時使用Runloop?
- 如何啟動Runloop?
- 如何退出Runloop?
何時使用Runloop
Runloop之所以讓人感到熟悉又陌生,原因之一就是:事實上我們幾乎不需要手動建立它!
- 由於主執行緒本身就依賴Runloop機制,所以UIApplication啟動時就已經幫我們把主Runloop啟動好了;#呼叫堆疊截圖#
- 對於子執行緒,如果輸入輸出是確定的,就不需要使用Runloop了:比如開啟一個子執行緒處理jpg影象解碼,只是傳入jpg格式的壓縮資料,得到解碼的RGB陣列,就沒有必要使用Runloop。
Runloop適用於你需要與執行緒進行互動的情況,有以下4種情形:
1. 與其他執行緒通訊。
- 使用NSPort進行執行緒之間通訊,我們經常會看到如下程式碼
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
這行程式碼的目的是新增一個埠監聽這個埠的事件,使得當前執行緒不退出,NSPort是一種基於埠的執行緒間通訊方式。
- 自定義輸入源,實際上不常用,限於篇幅,這裡不展開說明。
2. 線上程上使用計時器。
使用NSTimer時,不要忘記將其加入Runloop:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
如果不加這句,timer在ScrollView滑動時,不會執行。
3. 使用任何performSelector ...方法。
在子執行緒中使用performSelector系列方法,如:
- (id)performSelector:(SEL)aSelector withObject:(id)object;
總是在當前執行緒執行方法,注意: 不是一定在主執行緒 ,如果是主執行緒,由於已經自動建立了主Runloop,則 aSelector 會被自動執行;而如果是子執行緒,需要自行建立Runloop, 否者不會執行
。
4. 保持執行緒以執行定期任務。
當子執行緒中的任務執行完畢後,執行緒會被立刻銷燬。如果需要經常在子執行緒中執行任務,頻繁的建立和銷燬執行緒,會造成資源的浪費。這時候使用Runloop保持執行緒存活,以方便定時檢查任務執行條件。
如何啟動Runloop
有三種方法可以啟動執行迴圈:
- 無條件啟動
- 設定時限
- 在特定模式下
1. 無條件啟動
[[NSRunLoop currentRunLoop] run];
無條件地啟動執行迴圈非常簡單,但是這會將執行緒置於永久迴圈中, run後面的程式碼都不會執行
,這使你幾乎無法控制執行迴圈本身,停止執行迴圈的唯一方法是終止它。無條件地啟動也無法指定Runloop執行模式。
2. 設定時限執行
- (void)runUntilDate:(NSDate *)limitDate;
最好的方法是使用超時值執行Runloop。使用超時值時,如果事件到達,則將該事件分派給處理程式進行處理,然後退出執行迴圈。然後,可以沒有顧慮的重新啟動執行迴圈以處理下一個事件。這樣你的程式碼就獲得一個機會做一些事件處理以外的事務,如判斷是否需要退出Runloop。
-
在特定模式下執行
除了超時值,還可以使用特定模式執行執行迴圈。
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
實際上模式和超時值是協同工作的,模式限制將事件傳遞到Runloop的源型別,同一模式下只能執行加入到該模式的事件,但注意NSRunLoopCommonModes預設包括預設,模態和事件跟蹤模式。
這樣,無論當前Runloop切換到任意一種模式,事件都可以被執行,可以參考前面 NSTimer 的例子。
如何退出Runloop
如果子執行緒開啟了Runloop,退出執行緒時必須退出Runloop,否則會由於資源沒有釋放導致記憶體洩漏。
有兩種方法可以使執行迴圈退出:
- 強制退出。
- 利用超時使Runloop自行退出。
1. 強制退出
表面上看,我們可以直接命令Runloop退出:
void CFRunLoopStop( CFRunLoopRef rl );
但實際上,上述方法只能終止上節提到的第3種方式啟動的Runloop,建議使用NSRunLoop啟動的Runloop,不要使用CRunLoopStop()來停止,因為這是兩套函式,會有潛在的風險。
2. 利用超時使Runloop自行退出
這是推薦的退出方式,指定超時值可讓執行迴圈完成所有正常處理,包括在退出之前傳送通知。下面是AFNetworking中的一段典型用法:
while (completionCount < dispatchTarget) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; }
這樣不但正常執行了Runloop事件,也給其它邏輯程式碼執行機會。
其它“可能”的退出方法
最後討論一個退出方法,通過刪除輸入源和Timer,雖然這可以使Runloop退出,但實際操作會遇到兩個問題:
一方面系統會有預設事件源加入到Runloop,這無法控制;
另一方面,當獲得Runloop時,無法通過Runloop列出它的輸入源,比如一個加入Runloop的Timer,想要操作必須在Runloop外持有,這造成了額外的程式設計負擔。
有一種特殊情況,如上文提到的保持執行緒不被退出的方法,就必須使用移除源的方法才能退出:
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
執行緒安全
最後說明一下,NSRunLoop類不是執行緒安全的,將輸入源或計時器新增到屬於不同執行緒的Runloop,可能會導致程式碼崩潰或以其它異常。如果使用NSRunLoop類來配置使用Runloop,最好的實踐就是 僅從擁有該執行迴圈的同一執行緒執行此操作
。
小結:
本文介紹了Runloop生命週期的方方面面,包括使用場景、啟動、退出的各種方法,希望通過本文能排除一些對Runloop的神祕感,實際上,Runloop是多執行緒程式設計中不可缺少的一環,只有加入了Runloop的執行緒,才有啟動、空閒、休眠、再啟動這些“常見”功能,如果你在使用Runloop中有什麼心得或疑惑,歡迎留言,我們一起探討。