RunLoop 總結:RunLoop的應用情境(一),runloop情境
參考資料
好的書籍都是值得反覆看的,那好的文章,好的資料也值得我們反覆看。我們在不同的階段來相同的文章或資料或書籍都能有不同的收穫,那它就是好文章,好書籍,好資料。
關於iOS 中的RunLoop資料非常的少,以下這些資料都是非常好的。
- CF架構源碼(這是一份很重要的源碼,可以看到CF架構的每一次迭代,我們可以下載最新的版本來分析,或與以下文章對比學習。目前最新的是CF-1153.18.tar.gz)
- RunLoop官方文檔(學習iOS的任何技術,官方文檔都是入門或深入的極好手冊;我們也可以在Xcode--->Help--->Docementation and API Reference --->搜尋RunLoop---> Guides(59)--->《Threading Programming Guide:Run Loops》這篇即是)
- 深入理解RunLoop(不要看到右邊捲軸很長,其實文章占篇幅2/5左右,下面有很多的評論,可見這篇文章的火熱)
- RunLoop個人小結 (這是一篇總結的很通俗容易理解的文章)
- sunnyxx線下分享RunLoop(這是一份關於線下分享與討論RunLoop的視頻,備用地址:https://pan.baidu.com/s/1pLm4Vf9)
- iPhonedevwiki中的CFRunLoop(commonModes中其實包含了三種Mode,我們通常知道兩種,還有一種是啥,你知道嗎?)
- 維基百科中的Event loop(可以看看這篇文章瞭解一下事件迴圈)
說明
因為RunLoop 裡有很多新的平時基本很難接觸到的概念或者對象,所以如果從RunLoop是啥,裡麵包含啥,為什麼是這樣講起,難免太迷茫,太晦澀難懂。大多數關於RunLoop 的文章也是從基礎講起的,文章也比較長,可能看了三分之一,就已經懵了,沒了技術看下去的動力。所以我決定先從RunLoop的使用情境和用法講起,看到了一些用法和現象,再去看它的實現就要容易理解的多了。
文章中的範例程式碼,我會在文章末提供一個關於RunLoop的樣本Demo。
RunLoop的使用情境
下面介紹一下,可以使用RunLoop的幾個使用情境(本想一篇寫完,無奈一個使用情境就讓文章很長了,還是分幾篇來講吧)。
1.保證線程的長時間存活
在iOS開發過程中,有時候我們不希望一些花費時間比較長的操作阻塞主線程,導致介面卡頓,那麼我們就會建立一個子線程,然後把這些花費時間比較長的操作放在子線程中來處理。可是當子線程中的任務執行完畢後,子線程就會被銷毀掉。
怎麼來驗證上面這個結論呢?
首先,我們建立一個HLThread類,繼承自NSThread,然後重寫dealloc 方法。
@interface HLThread : NSThread@end@implementation HLThread- (void)dealloc{ NSLog(@"%s",__func__);}@end
然後,在控制器中用HLThread建立一個線程,執行一個任務,觀察任務執行完畢後,線程是否被銷毀。
- (void)viewDidLoad { [super viewDidLoad]; // 1.測試線程的銷毀 [self threadTest];}- (void)threadTest{ HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil]; [subThread start];}- (void)subThreadOpetion{ NSLog(@"%@----子線程任務開始",[NSThread currentThread]); [NSThread sleepForTimeInterval:3.0]; NSLog(@"%@----子線程任務結束",[NSThread currentThread]);}
控制台輸出的結果如下:
2016-12-01 16:44:25.559 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務開始2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務結束2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] -[HLThread dealloc]
當子線程中的任務執行完畢後,線程就被立刻銷毀了。如果程式中,需要經常在子線程中執行任務,頻繁的建立和銷毀線程,會造成資源的浪費。這時候我們就可以使用RunLoop來讓該線程長時間存活而不被銷毀。
我們將上面的範例程式碼修改一下,修改後的代碼過程為,建立一個子線程,當子線程啟動後,啟動runloop,點擊視圖,會在子線程中執行一個耗時3秒的任務(其實就是讓線程睡眠3秒)。
修改後的代碼如下:
@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; // 1.測試線程的銷毀 [self threadTest];}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];}- (void)threadTest{ HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil]; [subThread setName:@"HLThread"]; [subThread start]; self.subThread = subThread;}/** 子線程啟動後,啟動runloop */- (void)subThreadEntryPoint{ NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; //如果注釋了下面這一行,子線程中的任務並不能正常執行 [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; NSLog(@"啟動RunLoop前--%@",runLoop.currentMode); [runLoop run];}/** 子線程任務 */- (void)subThreadOpetion{ NSLog(@"啟動RunLoop後--%@",[NSRunLoop currentRunLoop].currentMode); NSLog(@"%@----子線程任務開始",[NSThread currentThread]); [NSThread sleepForTimeInterval:3.0]; NSLog(@"%@----子線程任務結束",[NSThread currentThread]);}@end
先看控制台輸出結果:
2016-12-01 17:22:44.396 RunLoopDemo[4733:369202] 啟動RunLoop前--(null)2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] 啟動RunLoop後--kCFRunLoopDefaultMode2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務開始2016-12-01 17:22:52.359 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務結束2016-12-01 17:22:55.244 RunLoopDemo[4733:369202] 啟動RunLoop後--kCFRunLoopDefaultMode2016-12-01 17:22:55.245 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務開始2016-12-01 17:22:58.319 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務結束
有幾點需要注意:
1.擷取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];
2.即使RunLoop開始運行,如果RunLoop 中的 modes 為空白,或者要執行的mode裡沒有item,那麼RunLoop會直接在當前loop中返回,並進入睡眠狀態。
3.自己建立的Thread中的任務是在kCFRunLoopDefaultMode這個mode中執行的。
注意點一解釋
RunLoop官方文檔中的第二段中就已經說明了,我們的應用程式並不需要自己建立RunLoop,而是要在合適的時間啟動runloop。
CF架構源碼中有CFRunLoopGetCurrent(void)
和 CFRunLoopGetMain(void)
,查看源碼可知,這兩個API中,都是先從全域字典中取,如果沒有與該線程對應的RunLoop,那麼就會幫我們建立一個RunLoop(建立RunLoop的過程在函數_CFRunLoopGet0(pthread_t t)
中)。
注意點二解釋
這一點,可以將範例程式碼中的[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
,可以看到注釋掉後,無論我們如何點擊視圖,控制台都不會有任何的輸出,那是因為mode 中並沒有item任務。經過NSRunLoop封裝後,只可以往mode中添加兩類item任務:NSPort(對應的是source)、NSTimer,如果使用CFRunLoopRef
,則可以使用C語言API,往mode中添加source、timer、observer。
如果不添加 [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
,我們把runloop的資訊輸出,可以看到:
添加port前的RunLoop
如果我們添加上[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
,再把RunLoop的資訊輸出,可以看到:
添加port後的RunLoop
注意點三解釋
怎麼確認自己建立的子線程上的任務是在kCFRunLoopDefaultMode這個mode中執行的呢?
我們只需要在執行任務的時候,列印出該RunLoop的currentMode即可。
因為RunLoop執行任務是會在mode間切換,只執行該mode上的任務,每次切換到某個mode時,currentMode就會更新。源碼請下載:CF架構源碼
CFRunLoopRun()
方法中會調用CFRunLoopRunSpecific()
方法,而CFRunLoopRunSpecific()
方法中有這麼兩行關鍵代碼:
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);......這中間還有好多邏輯代碼CFRunLoopModeRef previousMode = rl->_currentMode;rl->_currentMode = currentMode;...... 這中間也有一堆的邏輯rl->_currentMode = previousMode;
我測試後,控制台輸出的是:
2016-12-02 11:09:47.909 RunLoopDemo[5479:442560] 啟動RunLoop後--kCFRunLoopDefaultMode2016-12-02 11:09:47.910 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務開始2016-12-02 11:09:50.984 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務結束
AFNetworking中的RunLoop案例
在AFNetworking 2.6.3之前的版本,使用的還是NSURLConnection,可以在AFURLConnectionOperation
中找到使用RunLoop的源碼:
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }}+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread;}
AFNetworking都是通過調用 [NSObject performSelector:onThread:..] 將這個任務扔到了後台線程的 RunLoop 中。
- (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock];}
我們在使用NSURLConnection
或者NSStream
時,也需要考慮到RunLoop問題,因為預設情況下這兩個類的對象產生後,都是在當前線程的NSDefaultRunLoopMode
模式下執行任務。如果是在主線程,那麼就會出現滾動ScrollView以及其子視圖時,主線程的RunLoop切換到UITrackingRunLoopMode
模式,那麼NSURLConnection
或者NSStream
的回調就無法執行了。
要解決這個問題,有兩種方式:
第一種方式是建立出NSURLConnection
對象或者NSStream
對象後,再調用 - (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSRunLoopMode)mode
,設定RunLoopMode即可。需要注意的是NSURLConnection
必須使用其初始化構造方法- (nullable instancetype)initWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate startImmediately:(BOOL)startImmediately
來建立對象,設定Mode才會起作用。
第二種方式,就是所有的任務都在子線程中執行,並保證子線程的RunLoop正常運行即可(即上面AFNetworking的做法,因為主線程的RunLoop切換到UITrackingRunLoopMode
,並不影響其他線程執行哪個mode中的任務,電腦CPU是在每一個時間片切換到不同的線程去跑一會,呈現出的多線程效果)。
文中的範例程式碼都來自:RunLoopDemos中的RunLoopDemo01