
runloop和線程一一對應
runloop包含多個mode, mode包含多個 mode item(sources,timers,observers)
runloop一次只能運行在一個model下:
切換mode:停止loop -> 設(shè)置mode -> 重啟runloop
runloop通過切換mode來篩選要處理的事件,讓其互不影響
iOS運行流暢的關(guān)鍵

/// 核心函數(shù)
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
/// 通知 Observers: 即將處理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 通知 Observers: 即將處理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
/// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
/// 處理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
/// 處理sources0返回為YES
if (sourceHandledThisLoop) {
/// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
/// 判斷有無端口消息(Source1)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
/// 處理消息
goto handle_msg;
}
/// 通知 Observers: 即將進入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
/// 等待被喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
/// 通知 Observers: 被喚醒,結(jié)束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer喚醒) {
/// 處理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD喚醒) {
/// 處理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1喚醒) {
/// 被Source1喚醒,處理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 處理block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}

事件響應
當一個硬件事件(觸摸/鎖屏/搖晃/加速)發(fā)生后,首先IOKit.framework 生成一個IOHIDEvent事件并有SprintBoard接收,之后有mach_port轉(zhuǎn)發(fā)給需要的App進程。
蘋果注冊了一個Source1來接收系統(tǒng)事件,通過回調(diào)函數(shù)觸發(fā)Source0 (所以Event實際上是基于Source0的),調(diào)用_UIApplicationHandleEventQueue() 進行應用內(nèi)部的分發(fā)。_UIApplicationHandleEventQueue() 會把IOHIDEvent 處理并包裝成UIEvent 進行處理或分發(fā),其中包括識別UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給UIWindow等。
手勢識別
當上面的_UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用Cancel 將當前的touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應的UIGestureRecognizer 標記為待處理。
蘋果注冊了一個Observer監(jiān)測BeforeWaiting(Loop即將進入休眠)事件,這個Observer的回調(diào)函數(shù)是_UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有被標記為待處理的GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
當有UIGestureRecognizer的變化(創(chuàng)建、銷毀、狀態(tài)改變)時,這個回調(diào)都會進行相應的處理。
界面刷新
當UI發(fā)生改變時(Frame變化,UIView/CALayer的結(jié)構(gòu)變化)時,或手動調(diào)用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,這個UIView/CALayer就被標記為待處理。
蘋果注冊了一個用來監(jiān)聽BeforeWaiting和Exit的Observer,在他的回調(diào)函數(shù)里會遍歷所有待處理的UIView/CALayer來執(zhí)行實際的繪制和調(diào)整,并更新UI界面。
AutoreleasePool
主線程Runloop注冊了兩個Observers,其回調(diào)都是_wrapRunloopWithAutoreleasePoolHandler
Observers1 監(jiān)聽Entry事件: 優(yōu)先級最高,確保在所有的回調(diào)前創(chuàng)建釋放池,回調(diào)內(nèi)調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池
Observers2監(jiān)聽BeforeWaiting 和Exit事件: 優(yōu)先級最低,保證在所有回調(diào)后釋放釋放池。BeforeWaiting事件:調(diào)用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()釋放舊池并創(chuàng)建新池,Exit事件: 調(diào)用_objc_autoreleasePoolPop(),釋放自動釋放池
Timer不被ScrollView的滑動影響
+timerWithTimerInterval ... 創(chuàng)建timer
[[NSRunLoop currentRunLoop] addTimer: timer forMode:NSRunLoopCommonModes] 把timer加到當前runloop,使用占位模式
runloop run/runUntilData 手動開啟子線程
使用GCD創(chuàng)建定時器,GCD創(chuàng)建的定時器不會受RunLoop 的影響。
// 獲得隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設(shè)置定時器的各種屬性(幾時開始任務,每隔多長時間執(zhí)行一次)
// GCD的時間參數(shù),一般是納秒(1秒 == 10的9次方納秒)
// 比當前時間晚1秒開始執(zhí)行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒執(zhí)行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 設(shè)置回調(diào)
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 啟動定時器
dispatch_resume(self.timer);
GCD
dispatch_async(dispatch_get_main_queue)使用到了RunLoop
libDispatch向主線程的Runloop發(fā)送消息將其喚醒,并從消息中取得block,并在回調(diào)CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里執(zhí)行這個block
NSURLConnection
使用 NSURLConnection 時,你會傳入一個 Delegate,當調(diào)用了 [connection start] 后,這個 Delegate就會不停收到事件回調(diào)。
start 這個函數(shù)的內(nèi)部會會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。 CFMultiplexerSource 是負責各種 Delegate 回調(diào)的,CFHTTPCookieStorage 是處理各種 Cookie 的。
當開始網(wǎng)絡(luò)傳輸時,我們可以看到 NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內(nèi)部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate。
AFNetworking
使用runloop開啟常駐線程
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
給 runloop 添加NSMachPort port使runloop不退出,實際并沒有給這個port發(fā)消息
AsyncDisplayKit
仿照 QuartzCore/UIKit 框架的模式,實現(xiàn)了一套類似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer,監(jiān)聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調(diào)時,遍歷所有之前放入隊列的待處理的任務,然后一一執(zhí)行。
卡頓檢測
- dispatch_semaphore_t 是一個信號量機制,信號量到達、或者 超時會繼續(xù)向下進行,否則等待,如果超時則返回的結(jié)果必定不為0,信號量到達結(jié)果為0。GCD信號量-dispatch_semaphore_t
通過監(jiān)聽mainRunloop的狀態(tài)和信號量阻塞線程的特點來檢測卡頓,通過kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting的間隔時長超過自定義閥值則記錄堆棧信息。
FPS 檢測
創(chuàng)建CADisplayLink對象的時候會指定一個selector,把創(chuàng)建的CADisplayLink對象加入runloop,所以就實現(xiàn)了以屏幕刷新的頻率調(diào)用某個方法。
在調(diào)用的方法中計算執(zhí)行的次數(shù),用次數(shù)除以時間,就算出了FPS。
注:iOS正常刷新率為每秒60次。
@implementation ViewController {
UILabel *_fpsLbe;
CADisplayLink *_link;
NSTimeInterval _lastTime;
float _fps;
}
- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
self.count = 0;
_fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}
防崩潰處理
NSSetUncaughtExceptionHandler(&HandleException);監(jiān)聽異常信號SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGV,SIGFPE
回調(diào)方法內(nèi)創(chuàng)建一個Runloop,將主線程的所有Runmode都拿過來跑,作為應用程序主Runloop的替代
// 我的處理
LHLExceptionHelper *exceptionHandler = [LHLExceptionHelper new];
[exceptionHandler performSelectorOnMainThread:@selector(makeException:)
withObject:exceptionTemp waitUntilDone:
- (void)makeException:(NSException *)exception {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);
while (captor.needKeepAlive) {
for (NSString *mode in (__bridge NSArray *)allModesRef) {
if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
continue;
}
CFStringRef modeRef = (__bridge CFStringRef)mode;
CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
}
}
常駐線程
可以把自己創(chuàng)建的線程添加到Runloop中,做一些頻繁處理的任務,例如:檢測網(wǎng)絡(luò)狀態(tài),定時上傳一些信息等。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
@autoreleasepool{
/*如果不加這句,會發(fā)現(xiàn)runloop創(chuàng)建出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
下面的方法給runloop添加一個NSport,就是添加一個事件源,也可以添加一個定時器,或者observer,讓runloop不會掛掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2,3實現(xiàn)的效果相同,讓runloop無限期運行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
參考:
實時卡頓監(jiān)控
玩轉(zhuǎn)Runloop - 代碼示例使用Source, Observer, Timer
一文看完Runloop
深入理解Runloop
RunLoop實戰(zhàn):實時卡頓監(jiān)控
簡單監(jiān)測iOS卡頓的demo
關(guān)于dispatch_semaphore的使用