runloopDemophp
runloop
啥時候開啓runloop
對象是怎麼存儲的runloop
怎麼跑起來的,又是怎麼退出的Runloop do-while
作了什麼Runloop
的狀態runloop
和performSelector
runloop
和autoreleasepool
runloop
啥時候開啓從app
的main
函數中的UIApplicationMain
走進去,就一直在裏面循環了,NSLog(@"會走嗎");
是不會被調用的github
這裏我就有一個疑惑:那爲啥這個main還要return int類型呢?既然都死循環,return不了緩存
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); } int i = UIApplicationMain(argc, argv, nil, appDelegateClassName); NSLog(@"會走嗎"); return i; } 複製代碼
進入UIApplicationMain
後,就會接着調用application:didFinishLaunchingWithOptions:
方法,在這個方法裏就開啓runloop
,經過監聽runloop
狀態,在***即將進入runloop***回調打上斷點,看堆棧便可得知性能優化
runloop
對象是怎麼存儲的讓runloop
跑起來,得先獲取runloop
對象,咱們從CFRunloop.c
的源碼中,找到CFRunLoopGetCurrent
bash
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()); } 複製代碼
從這兩個方法,獲取Runloop
的入參是線程對象,能夠斷定,線程與runloop
一一對應的關係,具體,咱們再看下_CFRunLoopGet0
的實現markdown
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { // 若是參數爲空,那麼就默認是主線程 if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); // static CFMutableDictionaryRef __CFRunLoops = NULL; // 存放Runloop對象的字典 // 先判斷這個Runloop字典存在不,不存在就建立一個,並加主線程Runloop加進入 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); } // 根據線程去這個字段取Runloop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); // 若是不存在,就建立一個Runloop,並加到字典中 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; } 複製代碼
大概的代碼邏輯就是網絡
實現思路 1.先判斷這個全局字典存不存在,不存在,建立一個,並將主線程的runloop加進去 2.直接去字典裏取這個loop 3.若是loop不存在,就建立一個loop加入到全局字典中 // 僞代碼 if(!__CFRunLoops) { 1.建立全局字典 2.建立主線程loop,並加入到全局字典中 } 根據線程pthread_t爲key,去字典取對應的loop if(!loop) { 1.建立loop 2.加入到字典中 } return loop 複製代碼
因此:app
runloop
對象和線程是一一對應的關係runloop
對象是儲存在一個全局字典中的,這個全局字段的key
是線程對象,value
是runloop
對象runloop
怎麼跑起來的,又是怎麼退出的先說下runloop
跑一圈是作了什麼事情框架
首先runloop
有六個狀態變化
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
複製代碼
因此,當啓動runloop
的時候,就是監聽輸入源(端口port、source0、source1)、定時器、若是有事件,處理事件,沒有就休眠
可是實際上並非這樣的,而是一直在重複的進入runloop
(使用run方法啓動runloop的狀況)
咱們先從開啓runloop
的函數入手,從CFRunLoopRun
函數,咱們看到了runloop
確實是一個do-while
操做,那麼裏面的CFRunLoopRunSpecific
每走一次,就算runloop
迭代一次,那麼這個runloop
迭代一次後,會退出runloop
,退出runloop
後,由於CFRunLoopRun
函數有do-while
操做,因此又會從新進入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); } 複製代碼
CFRunLoopRunSpecific
中作了一些前置判斷,好比判斷當前Mode
爲空,直接return
,這個也能夠說明一點***啓動runloop以前,runloop中必定要有輸入源或者定時器***
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ ... // 前置判斷,好比判斷當前`Mode`爲空,直接`return` if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { Boolean did = false; if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } ... // 回調即將進入runloop if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 進入runloop result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 即將退出runloop if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); ... return result; } 複製代碼
接下來再看下__CFRunLoopRun
函數
// 簡化代碼,詳細直接搜源碼 static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { do { // 監聽source、timer if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 處理source0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 即將進入休眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); ... // 退出runloop的條件 if (sourceHandledThisLoop && stopAfterHandle) { // 處理完source後sourceHandledThisLoop會爲YES // stopAfterHandle,若是是CFRunloop調用的話,是爲NO // 能夠回頭看下CFRunLoopRun函數 // retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { // 自身超時時間到了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { // 被外部調用CFRunloop中止 __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { // 被 _CFRunLoopStopMode 中止 rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 檢查上一個 mode 有沒有執行完全部事件源 retVal = kCFRunLoopRunFinished; } } while(0 = retVal); } 複製代碼
退出runloop
有四個條件
stopAfterHandle
爲YES的時候,那麼處理完source
就會退出runloop
CFRunloop
中止_CFRunLoopStopMode
中止CFRunLoopRun
指定stopAfterHandle
爲NO
,說明使用run
方法開啓runloop
,處理完source
後不會退出runloop
若是是使用CFRunLoopRunInMode
則能夠指定是否須要處理完source
後就退出runloop
Runloop do-while
作了什麼do-while
的過程當中,作了如下操做
當咱們觸發了事件(觸摸/鎖屏/搖晃等)後,由IOKit.framework生成一個 IOHIDEvent事件,而IOKit是蘋果的硬件驅動框架,由它進行底層接口的抽象封裝與系統進行交互傳遞硬件感應的事件,並專門處理用戶交互設備,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的,它會將事件封裝成IOHIDEvents對象,接着用mach port轉發給須要的App進程,隨後 Source1就會接收IOHIDEvent,以後再回調__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()內觸發Source0,Source0 再觸發 _UIApplicationHandleEventQueue()。因此觸摸事件看到是在 Source0 內的。
總結:觸摸事件先經過 mach port 發送,封裝爲 source1,以後又轉換爲 source0
1.一個runloop
對應一個線程,多個mode
,一個mode
下對應多個source
、observer
、timer
struct __CFRunLoop {
pthread_t _pthread; // 線程對象
CFMutableSetRef _commonModes; //
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
// 簡化
};
struct __CFRunLoopMode {
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
// 簡化
};
複製代碼
2.常見有五種mode
除了以上5個mode,還有其餘mode,可是不多碰見這裏
4.子線程不自動開啓runloop
,手動開啓runloop
前,必須得有輸入源和定時器(輸入源就是經過監聽端口,能夠獲取不一樣的事件),經過CFRunloop
源碼中的CFRunLoopRunSpecific
函數,其中判斷了當mode
爲null
或者modeItem
爲空,直接return
runloop
有六大狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
複製代碼
能夠經過這就代碼監聽這六個狀態
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
複製代碼
其中的參數分別爲
CFRunLoopObserverCreate參數
1.不懂
2.監聽runloop變化狀態
3.是否重複監聽
4.不懂,傳0
5.回調的函數指針(須要本身寫一個函數)
6.CFRunLoopObserverContext對象
定義函數指針
static void runLoopOserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { //void *info正是咱們要用來與OC傳值的,這邊能夠轉成OC對象,前面咱們傳進來的時候是self RunloopObserver *target = (__bridge RunloopObserver *)(info);//void *info便是咱們前面傳遞的self(ViewController) if (target.callback) { target.callback(observer, activity); } } 複製代碼
定義CFRunLoopObserverContext
對象,其實這個參數是用於通訊的
// 從CFRunLoopObserverRef點進去找
typedef struct {
CFIndex version; // 傳0,不知道是什麼
void * info; // 數據傳遞用的,void *,指就是能夠接受全部指針
const void *(*retain)(const void *info); // 引用
void (*release)(const void *info); // 回收
CFStringRef (*copyDescription)(const void *info); // 描述,沒用到
} CFRunLoopObserverContext;
複製代碼
建立監聽
//建立一個監聽
static CFRunLoopObserverRef observer;
// CFRunLoopObserverCreate參數。1.不懂 2.監聽runloop變化狀態 3.是否重複監聽 4.不懂,傳0 5.函數指針 6.CFRunLoopObserverContext對象
observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopOserverCallBack, &context);
//註冊監聽
CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopCommonModes);
//銷燬
CFRelease(observer);
複製代碼
先說下performSelector
和子線程的,perform...AfterDelay
和perform..onThread
都須要在開啓runloop
的線程執行
由於其實現原理,都是往runloop
添加一個不重複的定時器
- (void)test1 { [self.myThread setName:@"StopRunloopThread"]; self.myRunloop = [NSRunLoop currentRunLoop]; // performSelector:afterDelay:的原理是往runloop添加不重複執行的定時器 [self performSelector:@selector(performSelAferDelayClick) withObject:nil afterDelay:1]; [self.myRunloop run]; NSLog(@"我會走嗎"); } 複製代碼
若是將開啓runloop
的代碼,寫到perform
前,那麼會開啓不成功,由於開啓runloop
須要有輸入源或者定時器的狀況才能夠開啓
獲取runloop會調用CFRunLoopRunSpecific
函數(具體搜下CFRunloop.c
)
從這個函數中找到如下代碼,當currentMode爲空的時候,(也就沒有輸入源或者定時器),直接return kCFRunLoopRunFinished
,開啓失敗
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { Boolean did = false; if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } 複製代碼
原理就是往當前線程的runloop
中添加一個端口,讓其監聽這個端口(理解爲監聽某個端口的輸入源,好比系統內核端口,監聽一些系統事件),由於能夠一直監聽這個端口,那麼runloop
就不會退出
其實就是保持runloop不退出,就達到常駐線程的效果了,那麼要讓runloop不退出,就得有輸入源或者重複的定時器讓其監聽
- (void)test2 { [self.myThread setName:@"StopRunloopThread"]; self.myRunloop = [NSRunLoop currentRunLoop]; self.myPort = [NSMachPort port]; [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode]; [self.myRunloop run]; // 由於run以後,這個線程就一直在作do-while操做 // 至關上面的代碼被do-while包圍了,那麼如下代碼不會走 NSLog(@"我會走嗎"); } 複製代碼
runloop
對象嗎?不是的,調用獲取當前runloop
的方法,內部實現:若是當前runloop
不存在就建立一個,存在就返回當前runloop
因此走這句代碼self.myRunloop = [NSRunLoop currentRunLoop];
就生成當前線程對應的runloop
1.要銷燬常駐線程,首先得先把runloop退出?
當沒有輸入源或者定時器能夠監聽的時候,退出
runloop
若是咱們調用[NSThread exit];
,這時候線程是銷燬了,可是線程中的代碼仍是不會執行,好比NSLog(@"我會走嗎");
,
說明這時候的runloop
並無退出,那麼這樣會致使一些問題,例如如下代碼
- (void)test2 { [self.myThread setName:@"TestThread"]; self.myRunloop = [NSRunLoop currentRunLoop]; self.myPort = [NSMachPort port]; // 添加監聽NSMachPort的端口(這個端口能夠理解爲輸入源,由於能夠一直監聽這個,因此這時候的runloop不會退出,會一直在作do-while) [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode]; [self.myRunloop run]; // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源 [self.testPtr release]; } 複製代碼
由於runloop
沒有退出,[self.testPtr release];
不會執行,那麼就會致使testPtr
對象無法釋放
2.怎麼退出runloop呢
若是常駐線程是經過監聽端口實現的,那麼就調用[self.myRunloop removePort:self.myPort forMode:NSDefaultRunLoopMode];
,移除端口,就能夠銷燬了
其實這時候還不必定能成功銷燬,由於可能系統加入了一些其餘源的監聽
若是是經過添加劇復定時器,實現常駐線程(這種方式不可取,由於比添加監聽端口耗性能,須要一次又一次的喚醒runloop)
- (void)test11 { [self.myThread setName:@"TestThread"]; self.myRunloop = [NSRunLoop currentRunLoop]; self.myPort = [NSMachPort port]; // 首先,須要有一點,當runloop監視輸入源或者定時器的時候,纔不會退出 // 開啓runloop以前,須要有輸入源或者定時器 // 定時器(若是是添加定時器,不重複,那麼監聽一次就退出了) NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer執行"); }]; [self.myRunloop addTimer:timer forMode:NSDefaultRunLoopMode]; [self.myRunloop run]; // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源 NSLog(@"我會走嗎"); } 複製代碼
若是NSTimer
的repeats
是NO
,那麼執行一次timer
的事件後,就會退出runloop
以上,若是經過移除端口,結束timer,反正以移除已知的輸入源或者定時器來退出runloop
都是不太靠譜的,由於系統內部有可能會在當前線程的runloop中添加一些輸入源,也就是還有未知的輸入源,咱們沒有移除
3.使用CFRunLoopStop退出Runloop
- (void)test3 { [self.myThread setName:@"TestThread"]; self.myRunloop = [NSRunLoop currentRunLoop]; self.myPort = [NSMachPort port]; // 添加監聽NSMachPort的端口(這個端口能夠理解爲輸入源,由於能夠一直監聽這個,因此這時候的runloop不會退出,會一直在作do-while) [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode]; [self performSelector:@selector(runloopStop) withObject:nil afterDelay:1]; [self.myRunloop run]; // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源 NSLog(@"我會走嗎"); } - (void)runloopStop { NSLog(@"執行stop"); CFRunLoopStop(self.myRunloop.getCFRunLoop); } 複製代碼
輸出:
2020-05-03 20:10:12.614130+0800 Runloop[60465:2827474] 即將進入Loop,
2020-05-03 20:10:12.614465+0800 Runloop[60465:2827474] 即將處理 Timer,
2020-05-03 20:10:12.615214+0800 Runloop[60465:2827474] 即將處理 Source,
2020-05-03 20:10:12.615634+0800 Runloop[60465:2827474] 即將進入休眠,
2020-05-03 20:10:13.615638+0800 Runloop[60465:2827474] 剛從休眠中喚醒,
2020-05-03 20:10:13.616005+0800 Runloop[60465:2827474] 執行stop
2020-05-03 20:10:13.616194+0800 Runloop[60465:2827474] 即將退出Loop,
2020-05-03 20:10:13.616360+0800 Runloop[60465:2827474] 即將進入Loop,
2020-05-03 20:10:13.616511+0800 Runloop[60465:2827474] 即將處理 Timer,
2020-05-03 20:10:13.616648+0800 Runloop[60465:2827474] 即將處理 Source,
2020-05-03 20:10:13.616765+0800 Runloop[60465:2827474] 即將進入休眠,
複製代碼
確實是退出了runloop
,可是又立刻進入了
緣由是:
開啓線程有三種方式
// 不會退出runloop - (void)run; // 超時時候到退出runloop - (void)runUntilDate:(NSDate *)limitDate; // 處理完source會退出或者時間到也會退出 - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; // 上三個方法分別對應CFRunloop void CFRunLoopRun(void) SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) // returnAfterSourceHandled爲NO SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) // returnAfterSourceHandled爲YES 複製代碼
run
和runUntilDate:
都會重複的調runMode:beforeDate:
具體的解釋看NSRunLoop的退出方式
因此剛纔stop
以後,確實是退出runloop
了,可是由於咱們是用run
啓動的,因此會重複的調用runMode:beforeDate:
又啓動了
3.用runMode:beforeDate:
啓動runloop
,再用CFRunLoopStop
退出runloop試試
將上一段代碼[self.myRunloop run];
替換成[self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
成功退出runloop
而且線程run
後的代碼也走了,這時候經過打個暫停斷點,看堆棧,發現咱們的線程不在了,說明已經被銷燬了(runloop退出後,線程沒有任務,天然就銷燬了)
2020-05-03 20:21:30.330067+0800 Runloop[60593:2834891] 即將進入Loop,
2020-05-03 20:21:30.330303+0800 Runloop[60593:2834891] 即將處理 Timer,
2020-05-03 20:21:30.330639+0800 Runloop[60593:2834891] 即將處理 Source,
2020-05-03 20:21:30.330906+0800 Runloop[60593:2834891] 即將進入休眠,
2020-05-03 20:21:31.330956+0800 Runloop[60593:2834891] 剛從休眠中喚醒,
2020-05-03 20:21:31.331329+0800 Runloop[60593:2834891] 執行stop
2020-05-03 20:21:31.331591+0800 Runloop[60593:2834891] 即將退出Loop,
2020-05-03 20:21:31.331783+0800 Runloop[60593:2834891] 我會走嗎
複製代碼
雖然使用self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
能夠成功退出runloop
,可是仍是有問題,當runloop
處理完source
後,就退出runloop
了,並且這時候,也不會想調用run
方法那樣,從新進入runloop
因此這種方式仍是不行
最後一個最佳方式,既能手動退出runloop
,有不會處理完source
就退出runloop
,再也不進來
BOOL shouldKeepRunning = YES; // global NSRunLoop *theRL = [NSRunLoop currentRunLoop]; while (shouldKeepRunning) { // runMode是有返回值的,當啓動runloop後,是不會返回的,因此不會一直在調這個方法,runloop退出了,纔會再調 [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]] } 複製代碼
當想退出runloop
的時候,將shouldKeepRunning
置爲NO
就能夠了
runloop
和performSelector
performSelector:withObject:afterDelay:
原理,往runloop
添加一個不重複的定時器
因此子線程調用這個方法,是須要開啓runloop
纔有效的
順便看看performSelector:onThread:withObject:waitUntilDone:
// myThread是常駐線程 self.myThread = [[PermanentThread alloc] initWithTarget:self selector:@selector(myThreadStart) object:nil]; [self.myThread start]; NSLog(@"1"); [self performSelector:@selector(performWait) onThread:self.myThread withObject:nil waitUntilDone:NO]; NSLog(@"2"); - (void)performWait { NSLog(@"3"); } 複製代碼
若是waitUntilDone
爲NO,那麼就是不等待sel執行完,才往下走
輸出爲一、二、3
若是爲YES,那麼就是會卡住當前線程,等待sel執行完才走
輸出爲一、三、2
需求描述:
給你一個接口,這個接口是網絡請求,回調是主線程回來的,如今要求調用這個接口後,須要等待回調回來後,後面的代碼才能夠接着往下走
- (void)netRequestComplete:(void(^)(void))complete { // 模擬網絡請求,主線程回調 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{ dispatch_async(dispatch_get_main_queue(), ^{ if (complete) { complete(); } }); }); } 複製代碼
使用信號量,會致使死鎖
dispatch_semaphore_t sema = dispatch_semaphore_create(0); [self netRequestComplete:^{ NSLog(@"3"); // 由於主線程被卡住,這裏不會走了,因此死鎖 dispatch_semaphore_signal(sema); }]; // 卡住主線程 dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC))); NSLog(@"2"); 複製代碼
正確方式使用CFRunloopRun
[self netRequestComplete:^{ NSLog(@"3"); // stop,退出runloop,主線程runloop退出後,又會自動加入,就像前面講的,開啓runloop是使用run的方法 CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop); }]; // CFRunLoopRun()至關加了do-while,這時候下面的代碼執行不了 CFRunLoopRun(); NSLog(@"2"); 複製代碼
這個點是從這個文章得知的UITableView性能優化-中級篇
作了一個實驗
首先,用perform
確實能夠滑動tableview滾動的時候,不加載圖片,達到優化的效果
可是經過這個實驗發現,當我中止滾動的時候,前面滑過的indexPath,都會觸發logIndexRow:
方法
若是這時候是加載圖片,那麼是多餘的了,由於cell都劃出界面了,沒有必要加載
由這個現象,能夠大概的判斷,performSelector:inModes
,若是是在default
mode下調用,雖然如今是在滾動,不會觸發方法,可是perform
就往runloop的defaultMode
添加輸入源,但滾動結束的時候,切換回defaultMode,這些輸入源都會被觸發
// 這個selector,能夠是loadImg的方法 [self performSelector:@selector(logIndexRow:) withObject:indexPath afterDelay:0 inModes:@[NSDefaultRunLoopMode]]; 複製代碼
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"123"]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"123"]; } // 滑動的時候,不會調用logIndexRow:,由於這時候mode是滑動,可是perform也是屬於輸入源,這些事件會被積累在NSDefaultRunLoopMode下,當切換到NSDefaultRunLoopMode下後,就會執行這些輸入源事件 [self performSelector:@selector(logIndexRow:) withObject:indexPath afterDelay:0 inModes:@[NSDefaultRunLoopMode]]; cell.textLabel.text = @"123"; cell.textLabel.textColor = [UIColor redColor]; return cell; } 複製代碼
引用源碼__CFRunLoopRun
分析中的說法
從 kCFRunLoopBeforeSources 爲起點到 kCFRunLoopBeforeWaiting 休眠前,這其中處理了大量的工做————執行 block,處理 source0,更新界面…作完這些以後 RunLoop 就休眠了,直到 RunLoop 被 timer、source、libdispatch 喚醒,喚醒後會發送休眠結束的 kCFRunLoopAfterWaiting 通知。咱們知道屏幕的刷新率是 60fps,即 1/60s ≈ 16ms,假如一次 RunLoop 超過了這個時間,UI 線程有可能出現了卡頓,BeforeSources 到 AfterWaiting 能夠粗略認爲是一次 RunLoop 的起止。至於其餘狀態,譬如 BeforeWaiting,它在更新完界面以後有可能休眠了,此時 APP 已經是不活躍的狀態,不太可能形成卡頓;而 kCFRunLoopExit,它在 RunLoop 退出以後觸發,主線程的 RunLoop 除了換 mode 又不太可能主動退出,這也不能用做卡頓檢測。
監聽***即將處理source***,到***結束睡眠***,若是這個過程超過一幀的時間,就可能出現丟幀的狀況(丟幀就會致使卡頓)
那麼爲何這個過程若是一幀的時間,就可能卡頓呢?
首先咱們要理解屏幕顯示原理,大概就是CPU計算文本、佈局、繪製、圖片解碼,以後就提交位圖到GPU,GPU就進行渲染,渲染完成後,根據V-sync信號,更新緩衝區,同時,視頻控制器的指針,也會根據V-sync信號去緩衝區讀取一幀的緩存,顯示到屏幕上
也就是說從cpu繪製->GPU渲染,要在16ms內完成,才能保證在指定時間內,給視頻控制器讀取,不然,視頻控制器就會讀到上一幀的畫面,這就致使卡頓了
因此在即將處理source,到結束睡眠這段時間內,若是CPU一直在處理一件任務,若是超過了16ms,那麼可能就來不及在16ms內完成一幀畫面的渲染
runloop
和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,優先級最低,保證其釋放池子發生在其餘全部回調以後。
設置_wrapRunLoopWithAutoreleasePoolHandler
符號斷點,能夠從彙編代碼,看到autoreleasepush、pop
不知道怎麼證實。。。。
參考文章