一. 認識NSRunloop
概念
Runloop就像它的名字一樣,就是跑環.我的理解就是一個死循環.是一個可以隨時睡眠,隨時喚醒的死循環
大家可以想一下,手機app爲什麼會一直運行?而且在接收到用戶點擊等等操作時就會有所反映.這個離不開runloop.
iOS app啓動時就會啓動一個runloop,而且這種模式應該Android也有,所以纔會有了app能一直運行
每個線程都有一個runloop,但是隻有主線程的runloop是默認開啓的,其他子線程需要調用NSRunLoop *runloop = [NSRunLoop
currentRunLoop];
獲取runloop的同時就會創建runloop
一個線程可以創建多個runloop,但是隻能是嵌套模式.也就是一個線程只有一個根runloop
1.1 NSRunloop與程序運行
那麼具體什麼是NSRunLoop呢?其實NSRunLoop的本質是一個消息機制的處理模式。讓我們首先來看一下程序的入口——main.m文件,一個ios程序啓動後,只有短短的十行代碼居然能保持整個應用程序一直運行而沒有退出,是不是有點意思?程序之所以沒有直接退出是因爲UIApplicationMain這個函數內部默認啓動了一個跟主線程相關的NSRunloop對象,而UIApplicationMain這個函數一直執行沒有返回就保存程序一直運行的狀態。
1 #import <UIKit/UIKit.h> 2 3 #import "AppDelegate.h" 4 5 int main(int argc, char * argv[]) { 6 7 @autoreleasepool { 8 9 return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 10 11 } 12 13 }
文章之初我們暫且將NSRunloop理解爲實現這樣功能的一段代碼 , 這可以幫助我們更好的理解NSRunloop處理事件的過程(實際上遠比這複雜的多:))。
1 int main(int argc, char * argv[]) { 2 3 BOOL runnning =YES; 4 do{ 5 ... 6 //處理各種操作 各種事件 7 ... 8 }while(running); 9 10 return 0; 11 }
下面用官方提供的一幅非常經典的圖片,來認識NSRunloop循環處理時間的流程。
通過所有的“消息”都被添加到了NSRunLoop中去,而在這裏這些消息並分爲“input source”和“Timer source” 並在循環中檢查是不是有事件需要發生,如果需要那麼就調用相應的函數處理。由此形成了運行->檢測->休眠 ->運行 的循環狀態。
1.1 NSRunloop與線程之間關係的解析
簡單說,一條線程對應一個NSRunloop對象。主線程NSRunloop對象是默認開啓的,其他線程的NSRunloop對象需要手動獲取。其實NSRunloop對象是懶加載的,所以不需要實例化這個類,而是直接調用獲取線程Runloop的方法即可喚醒。Runloop在第一次獲取時創建,在線程結束時銷燬。保持NSRunloop一直存在的方法稍後介紹。
1.1.1 獲得NSRunloop對象的方法
iOS其實有兩套Api來訪問和使用Runloop , NSRunloop是對CFRunloopRef的進一步封裝,並且CFRunloopRef是線程安全的,而這一點NSRunloop並不能保證。
Foundation ->NSRunloop
獲得當前線程的Runloop的方法 [NSRunloop currentRunloop];
獲得主線程的Runloop的方法 [NSRunloop mainRunloop];
Core Foundation ->CFRunloopRef
CFRunloopGetCurrent();
CFRunloopGetMain();
由蘋果官方文檔可以看出線程和Runloop對象的對應關係。如果你仔細閱讀可以從代碼中可以看出runloop的存儲方式是字典,而且key是線程。
1 // should only be called by Foundation 2 // t==0 is a synonym for "main thread" that always works 3 4 //函數返回值爲CFRunLoopRef 形參類型爲pthread_t 根據線程創建runloop對象 5 6 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { 7 8 if (pthread_equal(t, kNilPthreadT)) { 9 10 t = pthread_main_thread_np(); 11 12 } 13 14 __CFLock(&loopsLock); 15 16 if (!__CFRunLoops) { 17 18 __CFUnlock(&loopsLock); 19 20 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); 21 22 //由此句可得出 調用其他線程NSRunloop對象也會首先創建主線程NSRunloop對象 23 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); 24 25 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); 26 27 if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { 28 29 CFRelease(dict); 30 31 } 32 33 CFRelease(mainLoop); 34 35 __CFLock(&loopsLock); 36 37 } 38 39 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); 40 41 __CFUnlock(&loopsLock); 42 43 if (!loop) { 44 45 CFRunLoopRef newLoop = __CFRunLoopCreate(t); 46 47 __CFLock(&loopsLock); 48 49 loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); 50 51 if (!loop) { 52 53 CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); 54 55 loop = newLoop; 56 57 } 58 59 // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it 60 61 __CFUnlock(&loopsLock); 62 63 CFRelease(newLoop); 64 65 } 66 67 if (pthread_equal(t, pthread_self())) { 68 69 _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); 70 71 if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { 72 73 _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); 74 75 } 76 } 77 return loop; 78 }
1.1.2 Mode
Mode中有三個非常重要的組成部分,Timer(定時器)、 Source(事件源) 以及Observor(觀察者)。一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。首先要指出的是一個runloop啓動時必須指定一個Mode , 並且這個Mode被稱爲currentMode 。如果要切換Mode,只能退出runloop重新進入。這樣做主要是爲了分隔開不同組的 Source/Timer/Observer,讓其互不影響。隨後我們會分別介紹每一類的具體作用與應用場景。
系統默認註冊的Mode有五種
kCFRunloopDefaultMode // App默認Mode 通常主線程是在這個mode下運行
UITrackingRunloopMode // 界面跟蹤Mode 用於scrollView追蹤觸摸 界面滑動時不受其他Mode影響
UIinitializationRunloopMode //在app一啓動進入的第一個Mode,啓動完成後就不再使用
GSEventRecieveRunloopMode //蘋果使用繪圖相關
NSRunLoopCommonModes //佔位模式
1.1.2.1 CFRunloopTimerRef 基於時間的觸發器
NSTimer
首先說一下NSTimer,一個NSRunloop可以創建多個Timer。因爲定時器只會運行在指定的Mode下 ,一旦Runloop進入其他模式, 定時器就不會工作了。
NSTimer的創建方法
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]
該方法默認添加到當前runloop,並且Mode爲kCFRunloopDefaultMode。
1 NSTimer * timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
2 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //手動添加到runloop 可以指定Mode
這樣聲明的NSTimer可以解決在滑動scrollView時NSTimer不工作的問題。forMode:NSRunLoopCommonModes的意思爲,定時器可以運行在標記爲common modes模式下。具體包括兩種: kCFRunloopDefaultMode 和 UITrackingRunloopMode。
GCD定時器
GCD定時器的優點有很多,首先不受Mode的影響,而NSTimer受Mode影響時常不能正常工作,除此之外GCD的精確度明顯高於NSTimer,這些優點讓我們有必要了解GCD定時器這種方法。
1.1.2.2 CFRunloopSourceRef 事件源(輸入源)
按照蘋果官方文檔,Source分類
Port-Based Sources 基於端口的 和其他線程 或者內核
Custom Input Sources
Cocoa Perform Selector Sources
按照函數調用棧來分類
Source0 : 非基於Port的
Source1: 基於port的,通過內核 和其他線程通信,接收、分發事件。
1.1.2.3 CFRunloopObservorRef 觀察者監聽runloop狀態改變
1 /* Run Loop Observer Activities */ 2 3 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 4 5 kCFRunLoopEntry = (1UL << 0), 6 7 kCFRunLoopBeforeTimers = (1UL << 1), 8 9 kCFRunLoopBeforeSources = (1UL << 2), 10 11 kCFRunLoopBeforeWaiting = (1UL << 5), 12 13 kCFRunLoopAfterWaiting = (1UL << 6), 14 15 kCFRunLoopExit = (1UL << 7), 16 17 kCFRunLoopAllActivities = 0x0FFFFFFFU 18 19 };
// 創建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
});
// 添加觀察者:監聽RunLoop的狀態
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
二 、實際應用
- 只在NSRUnloopDefaultModes 下顯示圖片
上面再舉例NSTimer中已經闡述其中原理了,在此不再重複舉例了。
2. 常駐線程
NSThread * thread = [NSThread alloc ]initWithTarget selector
[thread start];
通常執行完方法後線程就銷燬了,那麼現在有這樣的需求,需要一條子線程一直存在,等待處理任務,與主線程之間互不干擾 (可以類比主線程存在原理,即添加消息循環Runloop)
1 // 通過添加port 或者timer 2 3 [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; 4 5 [[NSRunLoop currentRunLoop] run];
Ps:Runloop運行首先判斷Mode是否爲空,如果爲空則退出循環,還可以通過removePort來移除端口。本例用添加port來實現,其他方法請讀者自己多嘗試。:)
3. 關於自動釋放池
關於自動釋放池,子線程開啓runloop時要開啓針對當前線程的autoreleasepool,在每次NSRunloop休眠前清理自動釋放池。
關於自動釋放池的具體用法本文暫時不進行描述,待日後在整理修改本帖。