RunLoop是 iOS 和 OSX 開發中很是基礎的一個概念,同時也是不少常見技術的幕後功臣。儘管在平時多數開發者不多直接使用RunLoop,可是理解RunLoop能夠幫助開發者更好的利用多線程編程模型,同時也能夠幫助開發者解答平常開發中的一些疑惑。本文將從RunLoop源碼着手,結合RunLoop的實際應用來逐步解開它的神祕面紗html
一般所說的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是純C的函數,而NSRunloop僅僅是CFRunloopRef的OC封裝,並未提供額外的其餘功能,所以下面主要分析CFRunloopRef,蘋果已經開源了CoreFoundation源代碼,所以很容易找到CFRunloop源代碼。 從代碼能夠看出CFRunloopRef其實就是 __CFRunloop 這個結構體指針,這個對象的運行纔是咱們一般意義上說的運行循環,核心方法是 __CFRunloopRun() ,貼出部分源代碼:git
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
CHECK_FOR_FORK();
//若是CFRunLoopRef被標記已釋放,返回
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
//CFRunLoopRef線程安全鎖
__CFRunLoopLock(rl);
//取出進入RunLoop時指定的RunLoopMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
int32_t result = kCFRunLoopRunFinished;
//若是RunLoop的observer監聽了kCFRunLoopEntry,通知observer即將進入runloop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//若是RunLoop的observer監聽了kCFRunLoopExit,通知observer即將退出runloop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
//釋放線程安全鎖
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return result;
}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
do{
// 通知將要處理timer和source
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理非延遲的主線程調用
__CFRunLoopDoBlocks(rl, rlm);
// 處理Source0事件
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
/// 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
goto handle_msg;
}
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (rlm->_observerMask & kCFRunLoopBeforeWaiting) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 等待內核mach_msg事件
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 等待。。。
// 從等待中醒來
__CFRunLoopUnsetSleeping(rl);
if (rlm->_observerMask & kCFRunLoopAfterWaiting) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 處理因timer的喚醒
if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}else if (livePort == dispatchPort){// 處理異步方法喚醒,如dispatch_async
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
}else{
CFRUNLOOP_WAKEUP_FOR_SOURCE();
__CFRunLoopDoSource1();// 處理Source1
}
// 再次確保是否有同步的方法須要調用
__CFRunLoopDoBlocks(rl, rlm);
} while (!stop && !timeout);
}
複製代碼
__CFRunLoopRun 內部實際上是一個 do while 循環,這也正是Runloop運行的本質。執行了這個函數之後就一直處於「等待-處理」的循環之中,直到循環結束。只是不一樣於咱們本身寫的循環它在休眠時幾乎不會佔用系統資源,固然這是因爲系統內核負責實現的,也是Runloop精華所在github
下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程,固然能夠查看官方The Run Loop Sequence of Events描述: 編程
整個流程並不複雜(須要注意的就是 黃色 區域的消息處理中並不包含source0,由於它在循環開始之初就會處理),整個流程其實就是一種Event Loop的實現,其餘平臺均有相似的實現,只是這裏叫作Runloop。可是既然RunLoop是一個消息循環,誰來管理和運行Runloop?那麼它接收什麼類型的消息?休眠過程是怎麼樣的?如何保證休眠時不佔用系統資源?如何處理這些消息以及什麼時候退出循環?還有一系列問題須要解開緩存
注意的是儘管CFRunLoopPerformBlock在上圖中做爲喚醒機制有所體現,但事實上執行CFRunLoopPerformBlock只是入隊,下次RunLoop運行纔會執行,而若是須要當即執行則必須調用CFRunLoopWakeUp安全
從源碼很容易看出,Runloop老是運行在某種特定的CFRunLoopModeRef下(每次運行 __CFRunLoopRun() 函數時必須指定Mode)。而經過CFRunloopRef對應結構體的定義能夠很容易知道每種Runloop均可以包含若干個Mode,每一個Mode又包含Source/Timer/Observer。每次調用Runloop的主函數__CFRunLoopRun()時必須指定一種Mode,這個Mode稱爲 _currentMode,當切換Mode時必須退出當前Mode,而後從新進入Runloop以保證不一樣Mode的Source/Timer/Observer互不影響bash
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
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 */
};
複製代碼
系統默認提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,須要切換到對應的Mode時只須要傳入對應的名稱便可。前者是系統默認的Runloop Mode,例如進入iOS程序默認不作任何操做就處於這種Mode中,此時滑動UIScrollView,主線程就切換Runloop到到UITrackingRunLoopMode,再也不接受其餘事件操做(除非你將其餘Source/Timer設置到UITrackingRunLoopMode下)。cookie
可是對於開發者而言常常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實這個並非某種具體的Mode,而是一種模式組合,在iOS系統中默認包含了 NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:並非說Runloop會運行在kCFRunLoopCommonModes這種模式下,而是至關於分別註冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。固然你也能夠經過調用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合多線程
CFRunLoopRef和CFRunloopMode、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關係以下圖: app
那麼CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef到底是什麼?它們在Runloop運行流程中起到什麼做用呢?
首先看一下官方Runloop結構圖(注意下圖的Input Source Port和前面流程圖中的Source0並不對應,而是對應Source1。Source1和Timer都屬於端口事件源,不一樣的是全部的Timer都共用一個端口「Mode Timer Port」,而每一個Source1都有不一樣的對應端口):
再結合前面RunLoop核心運行流程能夠看出Source0(負責App內部事件,由App負責管理觸發,例如UITouch事件)和Timer(又叫Timer Source,基於時間的觸發器,上層對應NSTimer)是兩個不一樣的Runloop事件源(固然Source0是Input Source中的一類,Input Source還包括Custom Input Source,由其餘線程手動發出),RunLoop被這些事件喚醒以後就會處理並調用事件處理方法(CFRunLoopTimerRef的回調指針和CFRunLoopSourceRef均包含對應的回調指針)。
可是對於CFRunLoopSourceRef除了Source0以外還有另外一個版本就是Source1,Source1除了包含回調指針外包含一個mach port,和Source0須要手動觸發不一樣,Source1能夠監聽系統端口和其餘線程相互發送消息,它可以主動喚醒RunLoop(由操做系統內核進行管理,例如CFMessagePort消息)。官方也指出能夠自定義Source,所以對於CFRunLoopSourceRef來講它更像一種協議,框架已經默認定義了兩種實現
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
複製代碼
相對來講CFRunloopObserverRef理解起來並不複雜,它至關於消息循環中的一個監聽器,隨時通知外部當前RunLoop的運行狀態(它包含一個函數指針 callout 將當前狀態及時告訴觀察者)。具體的Observer狀態以下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
{
kCFRunLoopEntry = (1UL << 0), // 進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將開始Timer處理
kCFRunLoopBeforeSources = (1UL << 2), // 即將開始Source處理
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //從休眠狀態喚醒
kCFRunLoopExit = (1UL << 7), //退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
在開發過程當中幾乎全部的操做都是經過Call out進行回調的(不管是Observer的狀態通知仍是Timer、Source的處理),而系統在回調時一般使用以下幾個函數進行回調(換句話說你的代碼其實最終都是經過下面幾個函數來負責調用的,即便你本身監聽Observer也會先調用下面的函數而後間接通知你,因此在調用堆棧中常常看到這些函數):
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
複製代碼
例如在控制器的touchBegin中打入斷點查看堆棧(因爲UIEvent是Source0,因此能夠看到一個Source0的Call out函數 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
調用):
其實對於Event Loop而言RunLoop最核心的事情就是保證線程在沒有消息時休眠以免佔用系統資源,有消息時可以及時喚醒。RunLoop的這個機制徹底依靠系統內核來完成,具體來講是蘋果操做系統核心組件Darwin中的Mach來完成的Darwin 能夠從下圖最底層Kernel中找到Mach:
Mach是Darwin的核心,能夠說是內核的核心,提供了進程間通訊(IPC)、處理器調度等基礎服務。在Mach中,進程、線程間的通訊是以消息的方式來完成的,消息在兩個Port之間進行傳遞(這也正是Source1之因此稱之爲Port-based Source的緣由,由於它就是依靠系統發送消息到指定的Port來觸發的)。消息的發送和接收使用 <mach/message.h> 中的 mach_msg() 函數(事實上蘋果提供的Mach API不多,並不鼓勵咱們直接調用這些API):
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
複製代碼
而mach_msg()的本質是一個調用mach_msg_trap(),這至關於一個系統調用,會觸發內核狀態切換。當程序靜止時,RunLoop停留在 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
,而這個函數內部就是調用了mach_msg讓程序處於休眠狀態
Runloop是基於pthread進行管理的,pthread是基於c的跨平臺多線程操做底層API。它是mach thread的上層封裝,和NSThread一一對應(而NSThread是一套面向對象的API,因此在iOS開發中咱們也幾乎不用直接使用pthread)
蘋果開發的接口中並無直接建立Runloop的接口,若是須要使用Runloop一般 CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 兩個方法來獲取(經過上面的源代碼也能夠看到,核心邏輯在_CFRunLoopGet_當中),經過代碼並不難發現其實只有當咱們使用線程的方法主動get Runloop時纔會在第一次建立該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop兩者一一對應),默認狀況下線程並不會建立Runloop(主線程的Runloop比較特殊,任何線程建立以前都會保證主線程已經存在Runloop),同時在線程結束的時候也會銷燬對應的Runloop
iOS開發過程當中對於開發者而言更多的使用的是NSRunloop,它默認提供了三個經常使用的run方法:
- (void)run;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
複製代碼
前面一直提到Timer Source做爲事件源,事實上它的上層對應就是NSTimer(其實就是CFRunloopTimerRef)這個開發者常常用到的定時器(底層基於使用mk_timer實現),甚至不少開發者接觸RunLoop仍是從NSTimer開始的。其實NSTimer定時器的觸發正是基於RunLoop運行的,因此使用NSTimer以前必須註冊到RunLoop,可是RunLoop爲了節省資源並不會在很是準確的時間點調用定時器,若是一個任務執行時間較長,那麼當錯過一個時間點後只能等到下一個時間點執行,並不會延後執行(NSTimer提供了一個tolerance屬性用於設置寬容度,若是確實想要使用NSTimer而且但願儘量的準確,則能夠設置此屬性)
NSTimer的建立一般有兩種方式,儘管都是類方法,一種是timerWithXXX,另外一種scheduedTimerWithXXX
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
複製代碼
兩者最大的區別就是後者除了建立一個定時器外會自動以NSDefaultRunLoopModeMode添加到當前線程RunLoop中,不添加到RunLoop中的NSTimer是沒法正常工做的。例以下面的代碼中若是timer2不加入到RunLoop中是沒法正常工做的。同時注意若是滾動UIScrollView(UITableView、UICollectionview是相似的)兩者是沒法正常工做的,可是若是將NSDefaultRunLoopMode改成NSRunLoopCommonModes則能夠正常工做,這也解釋了前面介紹的Mode內容
#import "ViewController1.h"
@interface ViewController1 ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,weak) NSTimer *timer2;
@end
@implementation ViewController1
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
// timer1建立後會自動以NSDefaultRunLoopMode默認模式添加到當前RunLoop中,因此能夠正常工做
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
// 若是不把timer2添加到RunLoop中是沒法正常工做的(注意若是想要在滾動UIScrollView時timer2能夠正常工做能夠將NSDefaultRunLoopMode改成NSRunLoopCommonModes)
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
CGRect rect = [UIScreen mainScreen].bounds;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
[self.view addSubview:scrollView];
UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
contentView.backgroundColor = [UIColor redColor];
[scrollView addSubview:contentView];
scrollView.contentSize = contentView.frame.size;
}
- (void)timeInterval:(NSTimer *)timer {
if (self.timer1 == timer) {
NSLog(@"timer1...");
} else {
NSLog(@"timer2...");
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:true completion:nil];
}
- (void)dealloc {
NSLog(@"ViewController1 dealloc...");
}
@end
複製代碼
注意上面代碼中UIViewController1對timer1和timer2並無強引用,對於普通的對象而言,執行完viewDidLoad方法以後(準確的說應該是執行完viewDidLoad方法後的的一個RunLoop運行結束)兩者應該會被釋放,但事實上兩者並無被釋放。緣由是:爲了確保定時器正常運轉,當加入到RunLoop之後系統會對NSTimer執行一次retain操做(特別注意:timer2建立時並沒直接賦值給timer2,緣由是timer2是weak屬性,若是直接賦值給timer2會被當即釋放,由於timerWithXXX方法建立的NSTimer默認並無加入RunLoop,只有後面加入RunLoop之後才能夠將引用指向timer2)。
可是即便使用了弱引用,上面的代碼中ViewController1也沒法正常釋放,緣由是在建立NSTimer2時指定了target爲self,這樣一來形成了timer1和timer2對ViewController1有一個強引用。解決這個問題的方法一般有兩種:一種是將target分離出來獨立成一個對象(在這個對象中建立NSTimer並將對象自己做爲NSTimer的target),控制器經過這個對象間接使用NSTimer;另外一種方式的思路仍然是轉移target,只是能夠直接增長NSTimer擴展(分類),讓NSTimer自身作爲target,同時能夠將操做selector封裝到block中。後者相對優雅,也是目前使用較多的方案(目前有大量相似的封裝,例如:NSTimer+Block)。顯然Apple也認識到了這個問題,若是你能夠確保代碼只在iOS 10下運行就可使用iOS 10新增的系統級block方案(上面的代碼中已經貼出這種方法)。
固然使用上面第二種方法能夠解決控制器沒法釋放的問題,可是會發現即便控制器被釋放了兩個定時器仍然正常運行,要解決這個問題就須要調用NSTimer的invalidate方法(注意:不管是重複執行的定時器仍是一次性的定時器只要調用invalidate方法則會變得無效,只是一次性的定時器執行完操做後會自動調用invalidate方法)。
修改後的代碼以下:
#import "ViewController1.h"
@interface ViewController1 ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,weak) NSTimer *timer2;
@end
@implementation ViewController1
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1...");
}];
NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2...");
}];
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
CGRect rect = [UIScreen mainScreen].bounds;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
[self.view addSubview:scrollView];
UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
contentView.backgroundColor = [UIColor redColor];
[scrollView addSubview:contentView];
scrollView.contentSize = contentView.frame.size;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:true completion:nil];
}
- (void)dealloc {
[self.timer1 invalidate];
[self.timer2 invalidate];
NSLog(@"ViewController1 dealloc...");
}
@end
複製代碼
其實和定時器相關的另外一個問題你們也常常碰到,那就是NSTimer不是一種實時機制,官方文檔明確說明在一個循環中若是RunLoop沒有被識別(這個時間大概在50-100ms)或者說當前RunLoop在執行一個長的call out(例如執行某個循環操做)則NSTimer可能就會存在偏差,RunLoop在下一次循環中繼續檢查並根據狀況肯定是否執行(NSTimer的執行時間老是固定在必定的時間間隔,例如1:00:00、1:00:0一、1:00:0二、1:00:05則跳過了第四、5次運行循環)。
要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啓動一個定時器,再在主線程啓動一個耗時任務來演示這個問,若是實際測試可能效果不會太明顯,由於如今的iPhone都是多核運算的,這樣一來這個問題會變得相對複雜,所以下面的例子選擇在同一個RunLoop中即加入定時器和執行耗時任務)
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,strong) NSThread *thread1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
// 因爲下面的方法沒法拿到NSThread的引用,也就沒法控制線程的狀態
//[NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil];
self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];
[self.thread1 start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.thread1 cancel];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc {
[self.timer1 invalidate];
NSLog(@"ViewController dealloc.");
}
- (void)performTask {
// 使用下面的方式建立定時器雖然會自動加入到當前線程的RunLoop中,可是除了主線程外其餘線程的RunLoop默認是不會運行的,必須手動調用
__weak typeof(self) weakSelf = self;
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
if ([NSThread currentThread].isCancelled) {
//[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];
//[NSThread exit];
[weakSelf.timer1 invalidate];
}
NSLog(@"timer1...");
}];
NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
// 區分直接調用和「performSelector:withObject:afterDelay:」區別,下面的直接調用不管是否運行RunLoop同樣能夠執行,可是後者則不行。
//[self caculate];
[self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];
// 取消當前RunLoop中註冊測selector(注意:只是當前RunLoop,因此也只能在當前RunLoop中取消)
// [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
// 非主線程RunLoop必須手動調用
[[NSRunLoop currentRunLoop] run];
NSLog(@"注意:若是RunLoop不退出(運行中),這裏的代碼並不會執行,RunLoop自己就是一個循環.");
}
- (void)caculate {
for (int i = 0;i < 9999;++i) {
NSLog(@"%i,%@",i,[NSThread currentThread]);
if ([NSThread currentThread].isCancelled) {
return;
}
}
}
@end
複製代碼
若是運行而且不退出上面的程序會發現,前兩秒NSTimer能夠正常執行,可是兩秒後因爲同一個RunLoop中循環操做的執行形成定時器跳過了中間執行的機會一直到caculator循環完畢,這也正說明了NSTimer不是實時系統機制的緣由。
可是以上程序還有幾點須要說明一下:
CADisplayLink是一個執行頻率(fps)和屏幕刷新相同(能夠修改preferredFramesPerSecond改變刷新頻率)的定時器,它也須要加入到RunLoop才能執行。與NSTimer相似,CADisplayLink一樣是基於CFRunloopTimerRef實現,底層使用mk_timer(能夠比較加入到RunLoop先後RunLoop中timer的變化)。和NSTimer相比它精度更高(儘管NSTimer也能夠修改精度),不過和NStimer相似的是若是遇到大任務它仍然存在丟幀現象。一般狀況下CADisaplayLink用於構建幀動畫,看起來相對更加流暢,而NSTimer則有更普遍的用處。
AutoreleasePool是另外一個與RunLoop相關討論較多的話題。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop並無直接的關係,之因此將兩個話題放到一塊兒討論最主要的緣由是由於在iOS應用啓動後會註冊兩個Observer管理和維護AutoreleasePool。不妨在應用程序剛剛啓動時打印 currentRunLoop 能夠看到系統默認註冊了不少個Observer,其中有兩個Observer的callout都是 _wrapRunLoopWithAutoreleasePoolHandler ,這兩個是和自動釋放池相關的兩個監聽。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
複製代碼
第一個Observer會監聽RunLoop的進入,它會回調objc_autoreleasePoolPush()向當前的AutoreleasePoolPage增長一個哨兵對象標誌建立自動釋放池。這個Observer的order是-2147483647優先級最高,確保發生在全部回調操做以前。
第二個Observer會監聽RunLoop的進入休眠和即將退出RunLoop兩種狀態,在即將進入休眠時會調用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據狀況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出RunLoop時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象。這個Observer的order是2147483647,優先級最低,確保發生在全部回調操做以後。
主線程的其餘操做一般均在這個AutoreleasePool以內(main函數中),以儘量減小內存維護操做(固然你若是須要顯式釋放【例如循環】時能夠本身建立AutoreleasePool不然通常不須要本身建立)。
其實在應用程序啓動後系統還註冊了其餘Observer和多個Source1(例如context爲CFMachPort的Source1用於接收硬件事件響應進而分發到應用程序一直到UIEvent),這裏再也不一一詳述。
若是打印App啓動以後的主線程RunLoop能夠發現另一個callout爲 ___ZN2CA11Transaction17observer_callbackEP19_CFRunLoopObservermPv 的Observer,這個監聽專門負責UI變化後的更新,好比修改了frame、調整了UI層級(UIView/CALayer)或者手動設置了setNeedsDisplay/setNeedsLayout以後就會將這些操做提交到全局容器。而這個Observer監聽了主線程RunLoop的即將進入休眠和退出狀態,一旦進入這兩種狀態則會遍歷全部的UI更新並提交進行實際繪製更新。 一般狀況下這種方式是完美的,由於除了系統的更新,還能夠利用setNeedsDisplay等方法手動觸發下一次RunLoop運行的更新。可是若是當前正在執行大量的邏輯運算可能UI的更新就會比較卡,所以誕生了異步繪製框架Texture來解決這個問題。Texture實際上是將UI排版和繪製運算儘量放到後臺,將UI的最終更新操做放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關屬性,儘量保證開發者的開發習慣。這個過程當中Texture在主線程RunLoop中增長了一個Observer監聽即將進入休眠和退出RunLoop兩種狀態,收到回調時遍歷隊列中的待處理任務一一執行
NSURLConnection一旦啓動之後就會不斷調用delegate方法接收數據,這樣一個連續的的動做正是基於RunLoop來運行。
一旦NSURLConnection設置了delegate會當即建立一個線程com.apple.NSURLConnectionLoader,同時內部啓動RunLoop並在NSDefaultMode模式下添加4個Source0。其中CFHTTPCookieStorage用於處理cookie ;CFMultiplexerSource負責各類delegate回調並在回調中喚醒delegate內部的RunLoop(一般是主線程)來執行實際操做。
早期版本的AFNetworking庫也是基於NSURLConnection實現,爲了可以在後臺接收delegate回調AFNetworking內部建立了一個空的線程並啓動了RunLoop,當須要使用這個後臺線程執行任務時AFNetworking經過performSelector: onThread: 將這個任務放到後臺線程的RunLoop中
在RunLoop的源代碼中能夠看到用到了GCD的相關內容,可是RunLoop自己和GCD並無直接的關係。當調用了 dispatch_async(dispatch_get_main_queue(), <^(void)block>)
時libDispatch會向主線程RunLoop發送消息喚醒RunLoop,RunLoop從消息中獲取block,而且在 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
回調裏執行這個block。不過這個操做僅限於主線程,其餘線程dispatch操做是所有由libDispatch驅動的。
前面看了不少RunLoop的系統應用和一些知名第三方庫使用,那麼除了這些究竟在實際開發過程當中咱們本身能不能適當的使用RunLoop幫咱們作一些事情呢?
思考這個問題其實只要看RunLoopRef的包含關係就知道了,RunLoop包含多個Mode,而它的Mode又是能夠自定義的,這麼推斷下來其實不管是Source一、Timer仍是Observer開發者均可以利用,可是一般狀況下不會自定義Timer,更不會自定義一個完整的Mode,利用更多的實際上是Observer和Mode的切換。
例如不少人都熟悉的使用perfromSelector在默認模式下設置圖片,防止UITableView滾動卡頓[[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode]
還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閒狀態下計算出UITableViewCell的高度並進行緩存。再有老譚的PerformanceMonitor關於iOS實時卡頓監控,一樣是利用Observer對RunLoop進行監視。