趁熱打個鐵,火燒眉毛想記錄新東西了html
通常來說,一個線程只能執行一次任務,執行完線程就會退出。若是咱們須要這樣一個機制,讓線程能隨時處理事件而不退出,一般的邏輯代碼以下:安全
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); } 複製代碼
這種模型一般叫作Event Loop
。這個模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以免佔用資源,在有消息到來時當即被喚醒。 因此,RunLoop實際是一個對象,該對象管理了其須要處理的事件和消息,並提供了入口函數來處理上面的Event Loop
的邏輯。線程執行這個函數後,就會一直處在函數內部「接收消息->等待->處理」的循環中,直到接收到退出消息(如quit),函數結束。bash
OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。 CFRunLoopRef 是在 Core Foundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。它是基於pthread的。 NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。服務器
蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:微信
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; /// 訪問 loopsDic 時的鎖 static CFSpinLock_t loopsLock; /// 獲取一個 pthread 對應的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次進入時,初始化全局Dic,並先爲主線程建立一個 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } /// 直接從 Dictionary 裏獲取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { /// 取不到時,建立一個 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); } 複製代碼
從上述代碼能夠看出,線程和RunLoop是一一對應的,其關係保存在一個全局的Dictionary裏。線程建立時是沒有RunLoop的,只有第一次主動獲取的時候纔會建立,不然會一直沒有,直到線程結束時銷燬。你只能在一個線程內部獲取其RunLoop對象(主線程除外)。markdown
在建立一個iOS程序後,會自動生成main.m文件,以下app
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } 複製代碼
其中UIApplicationMain
函數內部爲主線程開啓了RunLoop,邏輯代碼如Event Loop
模型所示。 下圖爲蘋果官方給出的RunLoop模型圖。 框架
Core Foundation框架下有關於RunLoop的5個類,以下:async
他們的關係以下圖: 函數
一個RunLoop對象(CFRunLoopRef)包含若干個運行模式(CFRunLoopModeRef)。每一個運行模式下又包含若干個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef),他們有以下特色: 1.每次RunLoop啓動時,只能選擇一個運行模式啓動,這個運行模式被稱爲CurrentMode。 2.若是要切換運行模式,只能退出RunLoop,並從新指定一個運行模式啓動。 3.這樣作是爲了使不一樣組的輸入源、定時源、觀察者互不影響。咱們能夠經過以下API來獲取Core Fundation中的CFRunLoopRef。
//獲取主線程的RunLoop
CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
//獲取當前線程的RunLoop
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
複製代碼
系統定義了多種運行模式:
- (void)viewDidLoad { [super viewDidLoad]; //將定時器加入到默認運行模式中(一旦用戶交互就不會響應) NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode]; //將定時器加入到交互運行模式中(一旦中止交互就不會響應) NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInTrackingMode) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode]; //將定時器加入到僞模式中(不管是否交互均可以響應) NSTimer *timer3 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInCommonMode) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes]; } - (void)runInDefaultMode { NSLog(@"我只有在默認模式下運行!"); } - (void)runInTrackingMode { NSLog(@"我只有在交互模式下運行!"); } - (void)runInCommonMode { NSLog(@"我在默認模式和交互模式下都能運行!"); } 複製代碼
咱們觀察到,沒有作操做時timer1
能正常運行,而timer2
無響應,用戶操做後timer1
中止運行,而timer2
正常運行,與此同時timer3
始終都能運行,這是爲何呢?緣由以下:
NSDefaultRunLoopMode
模式下,因此timer1
此時能穩定2秒運行。NSDefaultRunLoopMode
模式,並切換到UITrackingRunLoopMode
工做,因此timer1
不能繼續工做,轉而該模式下的timer2
開始工做。NSRunLoopCommonModes
不是一個真正的模式,並不是須要停止其餘模式再切換,只是使得能夠在標記了Common Modes的模式下運行,也就是NSDefaultRunLoopMode
和NSRunLoopCommonModes
,因此timer3
能一直工做。這裏咱們能夠看下CFRunLoopMode 和 CFRunLoop 的大體結構:
struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... }; 複製代碼
一個Mode能夠將本身標記爲「Common」屬性(將本身的ModelName添加到RunLoop中的_commonModes中)。每當RunLoop發生變化時,RunLoop會將_commonModeItems中的Source/Observer/Timer同步到全部標記了「Common」的Mode中。
另外,說到NSTimer,咱們平時使用的如下方法,是自動添加到了RunLoop對象的NSDefaultRunLoopMode
模式下,因此一旦交互是沒法響應的。
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
複製代碼
例如咱們點擊一個按鈕,攔截它的響應事件的函數調用棧,能夠看到
CFRunLoopObserverRef是觀察者,用來監聽RunLoop狀態的改變,能夠監聽的狀態有如下幾種:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒:64
kCFRunLoopExit = (1UL << 7), // 即將從Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 監聽所有狀態改變
};
複製代碼
咱們能夠經過以下代碼來監聽RunLoop狀態的改變
//建立監聽者 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"監聽到RunLoop發生改變---%zd", activity); }); //添加到當前的RunLoop中 CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode); //添加完後釋放 CFRelease(observer); 複製代碼
這裏監聽了全部的狀態,打印日誌能夠看到RunLoop的狀態不斷的改變,最終會變成32,也就是立刻會進入休眠狀態。
注:上面的 Source/Timer/Observer 被統稱爲 mode item,一個 item 能夠被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會重複生效的。若是一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
根據蘋果在文檔裏的說明,RunLoop 內部的邏輯大體以下:
在每次運行開啓RunLoop時,所在線程的RunLoop會自動處理以前未處理的事件,而且通知相關觀察者。注:RunLoop 的核心就是一個 mach_msg() ,RunLoop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裏跑起一個 iOS 的 App,而後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。
{ /// 1. 通知Observers,即將進入RunLoop /// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); do { /// 2. 通知 Observers: 即將觸發 Timer 回調。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); /// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回調。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 4. 觸發 Source0 (非基於port的) 回調。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 6. 通知Observers,即將進入休眠 /// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); /// 7. sleep to wait msg. mach_msg() -> mach_msg_trap(); /// 8. 通知Observers,線程被喚醒 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); /// 9. 若是是被Timer喚醒的,回調Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer); /// 9. 若是是被dispatch喚醒的,執行全部調用 dispatch_async 等方法放入main queue 的 block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); /// 9. 若是若是Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } while (...); /// 10. 通知Observers,即將退出RunLoop /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); } 複製代碼
AutoreleasePool用於在代碼塊結束時釋放全部在代碼塊中建立的對象,最重要的使用場景就是臨時建立了大量的對象,例如在循環中建立對象,能夠在循環體內使用AutoreleasePool,及時清理內存。
App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池只發生在其餘全部回調以後。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯式建立 Pool 了。
蘋果註冊了一個Source1(基於mach port)來接收系統事件,若是發生硬件事件(觸摸/鎖屏/搖晃等),Source1會觸發回調__IOHIDEventSystemClientQueueCallback() ,函數內而後觸發Source0,Source0再經過_UIApplicationHandleEventQueue() 分發到應用內部。 _UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。 注:網上對於點擊事件是Source0仍是Source1觸發有爭議,在 __IOHIDEventSystemClientQueueCallback 處下一個 Symbolic Breakpoint能夠看到,確實是如上述邏輯。
當上面的_UIApplicationHandleEventQueue() 接收到一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。 蘋果註冊了一個Observer來監聽BeforeWaiting(即將進入休眠),其回調函數內部會獲取全部剛纔標記了未處理的手勢,並觸發它們的回調。 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。 蘋果註冊了一個Observer來監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,並在回調函數裏遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。
NSTimer實際也就至關於CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。 若是某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就好比等公交,若是 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。 CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。
當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。 當調用 performSelector:onThread: 時,實際上其會建立一個Source0加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。()
RunLoop底層會用到GCD的東西,GCD的實現也用到了RunLoop,好比dispatch_async()函數。 當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch 處理的。
例如咱們須要在cell上展現分時圖,那麼在滾動的時候若是有一堆的分時圖須要重複的清空再計算繪製,就有可能形成卡頓。 首先cell複用分時圖須要使用到兩個方法clearTimeLine
和refreshTimeLine
,咱們能夠利用PerformSelector
調用refreshTimeLine
將其放在主線程的NSDefaultRunLoopMode
下,這樣避免滾動時還會觸發繪圖操做,減小計算和繪製,提升性能,同時也減小了內存佔用。
若是在實際開發中有大量的耗時操做須要在後臺完成,頻繁的新建子線程並非好的方案,咱們能夠選擇讓這條線程常駐內存。
- (void)viewDidLoad { [super viewDidLoad]; //強引用子線程,初始化該線程並啓動 self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [self.thread start]; //經過performSelector來在子線程中處理耗時操做,避免重複建立 [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO]; } - (void)run { //開啓當前線程的RunLoop,此處添加port是避免RunLoop退出 [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; } 複製代碼
因爲咱們絕大多數操做都是基於非port通訊的,也就是source0,因此咱們能夠經過使用子線程來檢測RunLoop中kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
兩個狀態之間的時間來判斷這一輪操做是否卡頓,並把當前線程的堆棧信息存儲到文件中,在某個合適的時機上傳到服務器。
大體步驟以下:
kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
兩個狀態。kCFRunLoopBeforeSources
記錄更新時間,而且記錄狀態爲NO,用於定時器區分狀態;在kCFRunLoopBeforeWaiting
時將狀態置爲YES。優化點:
相關代碼
#import "ViewController.h" static CGFloat lagTimeInterval = 0.5; @interface ViewController () //監聽子線程 @property (nonatomic, strong) NSThread *monitorThread; //是否進入休眠 @property (nonatomic, assign) BOOL isBeforeWaiting; //即將處理source0的時間 @property (nonatomic, strong) NSDate *beforeSource0Time; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //建立監聽子線程,打開其RunLoop _monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(openRunLoop) object:nil]; [_monitorThread start]; //添加定時器 [self performSelector:@selector(startMonitorTimer) onThread:_monitorThread withObject:nil waitUntilDone:NO]; //主線程RunLoop添加觀察者 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopBeforeSources: { _beforeSource0Time = [NSDate date]; _isBeforeWaiting = NO; } break; case kCFRunLoopBeforeWaiting: { _isBeforeWaiting = YES; } break; default: break; } }); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); CFRelease(observer); } //打開子線程的RunLoop對象 - (void)openRunLoop { @autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSPort port] forMode:NSRunLoopCommonModes]; [runLoop run]; } } //添加定時器到子線程的RunLoop中 - (void)startMonitorTimer { NSTimer *timer = [NSTimer timerWithTimeInterval:0.5*lagTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) { //若是_isBeforeWaiting狀態爲YES,表示主線程RunLoop即將進入休眠 if(!_isBeforeWaiting) { //獲取當前時間與記錄時間的差值 NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:_beforeSource0Time]; //若是大於卡頓時間,則打印出來 if(timeInterval >= lagTimeInterval) { NSLog(@"##############卡了"); [self logStack]; } else { NSLog(@"##############沒卡"); } } }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)logStack { NSLog(@"%@", [NSThread callStackSymbols]); } @end 複製代碼