RunLoop是iOS事件響應與任務處理最核心的機制,它貫穿iOS整個系統,自動釋放池,延遲處理,觸摸事件,屏幕刷新都是通過RunLoop實現的.Foundation中的NSRunLoop和Core Foundation中CFRunLoop 是RunLoop的主要實現.
基礎實現
RunLoop通過do-while循環保持整個App的持續運行,同時能在運行和睡眠狀態之間切換,節省CPU資源.Android中的Looper跟iOS中的RunLoop類似,接收異步消息,控制應用程序的生命周期.一般情況一個線程只能執行一個任務,執行完成后就會退出.我們可以通過Runloop保證線程能隨時處理事件,并不退出.
Apple不允許直接創建 RunLoop,提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent().
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
pthread_self獲取當前線程,pthread_main_thread_np獲取主線程,通過線程獲取當前的Runloop.線程和Runloop被保存在全局字典__CFRunLoops中,如果字典中存在則會取出,如果不存在則會創建.
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
Runloop運行:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
運行機制
RunLoop是線程中的一個循環,在循環中不斷檢測通過Input sources(輸入源)和Timer sources(定時源)兩種來源等待接受事件,然后對接受到的事件通知線程進行處理,并在沒有事件的時候進行睡眠.
RunLoop與線程之間的關系密不可分:
1.線程與RunLoop是一一對應的,一個線程對應一個RunLoop對象,根RunLoop可以嵌套子RunLoop.
2.主線程的RunLoop在應用啟動的時候會自動創建,非主線程的RunLoop需要在該線程自己啟動.
3.RunLoop對象在第一次獲取RunLoop時創建,銷毀則是在線程結束的時候.
4.不能自己創建RunLoop.
5.RunLoop并不是線程安全的,只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop,同時也需要避免在其他線程上調用當前線程的RunLoop.
一個 RunLoop 對象包含若干個 Mode 對象,每個 Mode 又包含若干個 Source/Timer/Observer,RunLoop 一次運行只能在一個 Mode 之下,如果需要切換 Mode,需要退出 Loop 才能重新指定一個 Mode.這樣做主要是為了分隔開不同組Source/Timer/Observer,讓其互不影響.
一個 Source 對象是一個事件,Source 有兩個版本:Source0 和 Source1,Source0 只包含一個函數指針,并不能主動觸發,需要將 Source0 標記為待處理,在 RunLoop 運轉的時候,才會處理這個事件(如果 RunLoop 處于休眠狀態,則不會被喚醒去處理),而 Source1 包含了一個 mach_port 和一個函數指針,mach_port 是 iOS 系統提供的基于端口的輸入源,可用于線程或進程間通訊。而 RunLoop 支持的輸入源類型中就包括基于端口的輸入源,可以做到對 mach_port 端口源事件的監聽。所以監聽到 source1 端口的消息時,RunLoop 就會自己醒來去執行 Source1 事件(也能稱為被消息喚醒)。也就是 Source0 是直接添加給 RunLoop 處理的事件,而 Source1 是基于端口的,進程或線程之間傳遞消息觸發的事件.
Timer 是基于時間的觸發器,CFRunLoopTimerRef 和 NSTimer 可以通過 Toll-free bridging 技術混用,Toll-free bridging 是一種允許某些 ObjC 類與其對應的 CoreFoundation 類之間可以互換使用的機制,當將 Timer 加入到 RunLoop 時,RunLoop 會注冊對應的時間點,當時間點到時,RunLoop 會被喚醒以執行 Timer 回調.
__CFRunLoopMode定義如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
基礎知識
RunLoop運行狀態通過CFRunLoopActivity可以查看:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- kCFRunLoopEntry -- 進入runloop循環
- kCFRunLoopBeforeTimers -- 處理定時調用前回調
- kCFRunLoopBeforeSources -- 處理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前調用
- kCFRunLoopAfterWaiting -- runloop喚醒后調用
- kCFRunLoopExit -- 退出runloop
CoreFoundation中關于RunLoop的5個重要的類:
- CFRunLoopRef:運行循環對象,也就是它自身.
- CFRunLoopModeRef:指定runloop的運行模式.給事件源分組,避免互相影響.一個runLoop可以有很多個Mode,1個Mode可以有很多個Source,Observer,Timer,但是在同一時刻只能同時執行一種Mode.
- CFRunLoopSourceRef:輸入源.
- CFRunLoopTimerRef:定時源,定時器,必須加入到runloop.
- CFRunLoopObserverRef(觀察者,觀察是否有事件).
關于五個主要的類可以描述為一個RunLoop對象中包含若干個運行模式(CFRunLoopModeRef),而每一個運行模式下又包含若干個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef).
單次Runloop可以處理Source1(觸摸/鎖屏/搖晃),Source0事件(需要手動觸發),Timer事件和觀察者事件.
Runloop 模式
系統默認定義了多種運行模式(CFRunLoopModeRef),常見的有五種:
kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個 Mode 下運行的.
UITrackingRunLoopMode:界面跟蹤 Mode,用于ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode 影響.
UIInitializationRunLoopMode:在剛啟動 App 時第進入的第一個Mode,啟動完成之后就不再使用.
GSEventReceiveRunLoopMode:接收系統事件的內部 Mode,通常用不到.
kCFRunLoopCommonModes:占位用的 Mode,不是一種真正的 Mode.
RunLoop與自動釋放池:
自動釋放池寄生于Runloop,程序啟動后,主線程注冊了兩個Observer監聽runloop的進出和睡眠.一個最高優先級OB監測Entry狀態,一個最低優先級OB監聽BeforeWaiting狀態和Exit狀態.
RunLoop 實戰
1.維護線程的生命周期,讓線程不自動退出,isFinished為Yes時退出.
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
2.創建常駐線程,執行一些會一直存在的任務,該線程的生命周期跟App相同:
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
創建常駐線程最經典的例子是AFNetWorking 2.x版本中代碼:
+ (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;
}
3.在一定時間內監聽某種事件,或執行某種任務的線程
如下代碼,在30分鐘內,每隔30s執行onTimerFired.這種場景一般會出現在,如我需要在應用啟動之后,在一定時間內持續更新某項數據,如果用來監控屏幕的卡頓也可以.
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
target:self
selector:@selector(onTimerFired:)
userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
4.UITableView滾動加載圖片
當tableView的cell上有需要從網絡獲取的圖片的時候,滾動tableView,異步線程回去加載圖片,加載完成后主線程會設置cell的圖片,但是會造成卡頓。可以設置圖片的任務在CFRunloopDefaultMode下進行,當滾動tableView的時候,Runloop切換到UITrackingRunLoopMode,不去設置圖片,而是而是當停止的時候,再去設置圖片.
[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];
5.NSTimer失效
如果頁面有計時器同時有滑動視圖的時候,需要注意NSTimer的模式,視圖滑動的過程會切換至UITrackingMode模式下,造成Timer短暫失效,將Timer的模式設置為CommonMode即可.
self.upTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(upTimeUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.upTimer forMode:NSRunLoopCommonModes];
[self.upTimer fire];
self.bottomTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(bottomTimeUpdate) userInfo:nil repeats:YES];
參考資料:
CF框架源碼
RunLoop 原理和核心機制
CoreFoundation
深入理解RunLoop
滑動卡頓優化
官方文檔
CF源碼
http://blog.raozhizhen.com/post/2016-08-18