iOS底层原理探究-Runloop
Runloop
1. 概述
一般來(lái)說(shuō),一個(gè)線程只能執(zhí)行一個(gè)任務(wù),執(zhí)行完就會(huì)退出,如果我們需要一種機(jī)制,讓線程能隨時(shí)處理時(shí)間但并不退出,那么 RunLoop 就是這樣的一個(gè)機(jī)制。Runloop是事件接收和分發(fā)機(jī)制的一個(gè)實(shí)現(xiàn)。
RunLoop實(shí)際上是一個(gè)對(duì)象,這個(gè)對(duì)象在循環(huán)中用來(lái)處理程序運(yùn)行過(guò)程中出現(xiàn)的各種事件(比如說(shuō)觸摸事件、UI刷新事件、定時(shí)器事件、Selector事件),從而保持程序的持續(xù)運(yùn)行;而且在沒(méi)有事件處理的時(shí)候,會(huì)進(jìn)入睡眠模式,從而節(jié)省CPU資源,提高程序性能。
簡(jiǎn)單的說(shuō)run loop是事件驅(qū)動(dòng)的一個(gè)大循環(huán),如下代碼所示:
int main(int argc, char * argv[]) {//程序一直運(yùn)行狀態(tài)while (AppIsRunning) {//睡眠狀態(tài),等待喚醒事件id whoWakesMe = SleepForWakingUp();//得到喚醒事件id event = GetEvent(whoWakesMe);//開(kāi)始處理事件HandleEvent(event);}return 0; } 復(fù)制代碼2. Runloop 基本作用
2.1 保持程序持續(xù)運(yùn)行
程序一啟動(dòng)就會(huì)開(kāi)一個(gè)主線程,主線程一開(kāi)起來(lái)就會(huì)跑一個(gè)主線程對(duì)應(yīng)的Runloop, Runloop保證主線程不會(huì)被銷毀,也就保證了程序的持續(xù)運(yùn)行。不光iOS,在其他的編程平臺(tái),Android, Windows等都有一個(gè)類似Runloop的機(jī)制保證程序的持續(xù)運(yùn)行。
2.2 處理App中的各類事件
系統(tǒng)級(jí)別
GCD, mach kernel, block, pthread
應(yīng)用層
NSTimer, UIEvent, Autorelease, NSObject(NSDelayedPerforming), NSObject(NSThreadPerformAddition), CADisplayLink, CATransition, CAAnimation, dispatch_get_main_queue() (GCD 中dispatch到main queue的block會(huì)被dispatch到main Runloop中執(zhí)行), NSPort, NSURLConnection, AFNetworking(這個(gè)第三方網(wǎng)絡(luò)請(qǐng)求框架使用在開(kāi)啟新線程中添加自己到Runloop監(jiān)聽(tīng)事件)
2.3 節(jié)省CPU資源,提高程序性能
程序運(yùn)行起來(lái)時(shí),當(dāng)什么操作都沒(méi)有做的時(shí)候,Runloop告訴CPU, 現(xiàn)在沒(méi)有事情做,我要去休息, 這時(shí)CPU就會(huì)將資源釋放出來(lái)去做其他的事情,當(dāng)有事情做的時(shí)候Runloop就會(huì)立馬起來(lái)去做事情。
3. Runloop 的開(kāi)啟
程序入口
iOS 程序的入口是 main 函數(shù)
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));} } 復(fù)制代碼程序主線程一開(kāi)起來(lái),就會(huì)跑一個(gè)和主線程對(duì)應(yīng)的Runloop, 那么Runloop一定是在程序的入口main函數(shù)中開(kāi)啟。
在main thread 堆棧中所處的位置
堆棧最底層是start(dyld),往上依次是main,UIApplication(main.m) -> GSEventRunModal(Graphic Services) -> RunLoop(包含CFRunLoopRunSpecific,__CFRunLoopRun,__CFRunLoopDoSouces0,CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION) -> Handle Touch Event
4. Runloop 原理
CFRunLoop開(kāi)源代碼:http://opensource.apple.com/source/CF/CF-855.17/
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); } 復(fù)制代碼我們發(fā)現(xiàn)RunLoop確實(shí)是do while通過(guò)判斷result的值實(shí)現(xiàn)的。因此,我們可以把RunLoop看成一個(gè)死循環(huán)。如果沒(méi)有RunLoop,UIApplicationMain函數(shù)執(zhí)行完畢之后將直接返回,也就沒(méi)有程序持續(xù)運(yùn)行一說(shuō)了。
執(zhí)行順序的偽代碼:
int32_t __CFRunLoopRun() {// 通知即將進(jìn)入runloop__CFRunLoopDoObservers(KCFRunLoopEntry);do{// 通知將要處理timer和source__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);__CFRunLoopDoObservers(kCFRunLoopBeforeSources);// 處理非延遲的主線程調(diào)用__CFRunLoopDoBlocks();// 處理Source0事件__CFRunLoopDoSource0();if (sourceHandledThisLoop) {__CFRunLoopDoBlocks();}/// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個(gè) Source1 然后跳轉(zhuǎn)去處理消息。if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort();if (hasMsg) goto handle_msg;}/// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}// GCD dispatch main queueCheckIfExistMessagesInMainDispatchQueue();// 即將進(jìn)入休眠_(dá)_CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);// 等待內(nèi)核mach_msg事件mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();// 等待。。。// 從等待中醒來(lái)__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);// 處理因timer的喚醒if (wakeUpPort == timerPort)__CFRunLoopDoTimers();// 處理異步方法喚醒,如dispatch_asyncelse if (wakeUpPort == mainDispatchQueuePort)__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()// 處理Source1else__CFRunLoopDoSource1();// 再次確保是否有同步的方法需要調(diào)用__CFRunLoopDoBlocks();} while (!stop && !timeout);// 通知即將退出runloop__CFRunLoopDoObservers(CFRunLoopExit); } 復(fù)制代碼5. Runloop 對(duì)象
RunLoop對(duì)象包括Fundation中的NSRunLoop對(duì)象和CoreFoundation中的CFRunLoopRef對(duì)象。因?yàn)镕undation框架是基于CFRunLoopRef的封裝,因此我們學(xué)習(xí)RunLoop還是要研究CFRunLoopRef 源碼。
獲得Runloop 對(duì)象
//Foundation [NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對(duì)象 [NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對(duì)象//Core Foundation CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對(duì)象 CFRunLoopGetMain(); // 獲得主線程的RunLoop對(duì)象 復(fù)制代碼值的注意的是子線程中的runloop不是默認(rèn)開(kāi)啟的,需要手動(dòng)開(kāi)啟,當(dāng)調(diào)用 [NSRunLoop currentRunLoop] 時(shí),若已存在當(dāng)前線程的runloop返回,若不存在創(chuàng)建一個(gè)新的runloop對(duì)象再返回。
6. Runloop 和 線程
6.1 Runloop 和 線程 之間的關(guān)系
6.2 主線程想關(guān)聯(lián)的Runloop創(chuàng)建
CFRunloopRef 源碼
// 創(chuàng)建字典CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);// 創(chuàng)建主線程 根據(jù)傳入的主線程創(chuàng)建主線程對(duì)應(yīng)的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());// 保存主線程 將主線程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); 復(fù)制代碼6.3 創(chuàng)建與子線程想關(guān)聯(lián)的Runloop
Apple 不允許直接創(chuàng)建Runloop, 它只提供了兩個(gè)自動(dòng)獲取的函數(shù): CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 CFRunLoopRef源碼:
/// 用DefaultMode啟動(dòng) void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); }/// 用指定的Mode啟動(dòng),允許設(shè)置RunLoop超時(shí)時(shí)間 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }/// RunLoop的實(shí)現(xiàn) int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {/// 首先根據(jù)modeName找到對(duì)應(yīng)modeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);/// 如果mode里沒(méi)有source/timer/observer, 直接返回。if (__CFRunLoopModeIsEmpty(currentMode)) return;/// 1. 通知 Observers: RunLoop 即將進(jìn)入 loop。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);/// 內(nèi)部函數(shù),進(jìn)入loop__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO;int retVal = 0;do {/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);/// 執(zhí)行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);/// 執(zhí)行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個(gè) Source1 然后跳轉(zhuǎn)去處理消息。if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)if (hasMsg) goto handle_msg;}/// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進(jìn)入休眠, 直到被下面某一個(gè)事件喚醒。/// ? 一個(gè)基于 port 的Source 的事件。/// ? 一個(gè) Timer 到時(shí)間了/// ? RunLoop 自身的超時(shí)時(shí)間到了/// ? 被其他什么調(diào)用者手動(dòng)喚醒__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg}/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);/// 收到消息,處理消息。handle_msg:/// 9.1 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)。if (msg_is_timer) {__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} /// 9.2 如果有dispatch到main_queue的block,執(zhí)行block。else if (msg_is_dispatch) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);} /// 9.3 如果一個(gè) Source1 (基于port) 發(fā)出事件了,處理這個(gè)事件else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);if (sourceHandledThisLoop) {mach_msg(reply, MACH_SEND_MSG, reply);}}/// 執(zhí)行加入到Loop的block__CFRunLoopDoBlocks(runloop, currentMode);if (sourceHandledThisLoop && stopAfterHandle) {/// 進(jìn)入loop時(shí)參數(shù)說(shuō)處理完事件就返回。retVal = kCFRunLoopRunHandledSource;} else if (timeout) {/// 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間了retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(runloop)) {/// 被外部調(diào)用者強(qiáng)制停止了retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {/// source/timer/observer一個(gè)都沒(méi)有了retVal = kCFRunLoopRunFinished;}/// 如果沒(méi)超時(shí),mode里沒(méi)空,loop也沒(méi)被停止,那繼續(xù)loop。} while (retVal == 0);}/// 10. 通知 Observers: RunLoop 即將退出。__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); } 復(fù)制代碼可以看出,線程和 RunLoop 之間是一一對(duì)應(yīng)的,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒(méi)有 RunLoop,如果你不主動(dòng)獲取,那它一直都不會(huì)有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop 的銷毀是發(fā)生在線程結(jié)束時(shí)。你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程除外)。
[NSRunLoop currentRunLoop];方法調(diào)用時(shí),會(huì)先看一下字典里有沒(méi)有存子線程相對(duì)用的RunLoop,如果有則直接返回RunLoop,如果沒(méi)有則會(huì)創(chuàng)建一個(gè),并將與之對(duì)應(yīng)的子線程存入字典中。
7. Runloop 相關(guān)類
Core Foundation中關(guān)于RunLoop的5個(gè)類:
CFRunLoopRef //獲得當(dāng)前RunLoop和主RunLoop CFRunLoopModeRef //運(yùn)行模式,只能選擇一種,在不同模式中做不同的操作 CFRunLoopSourceRef //事件源,輸入源 CFRunLoopTimerRef //定時(shí)器時(shí)間 CFRunLoopObserverRef //觀察者 復(fù)制代碼7.1 CFRunLoopModeRef
一個(gè)Runloop包含若干個(gè)Mode, 每個(gè)Mode又包含若干個(gè)Source / Timer / Observer. 每次調(diào)用Runloop 的主函數(shù)時(shí),只能指定其中一個(gè)Mode, 這個(gè)Mode被稱作 CurrentMode. 如果需要切換Mode, 只能退出Loop, 再重新指定一個(gè)Mode進(jìn)入。這樣做主要是為了分隔開(kāi)不同組的 Source/Timer/Observer, 讓其互不影響。
系統(tǒng)默認(rèn)注冊(cè)了 5 個(gè)Mode, 其中常見(jiàn)的有第 1,2 種:
1. kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行 2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響 3. UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用 4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到 5. kCFRunLoopCommonModes: 這是一個(gè)占位用的Mode,作為標(biāo)記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode 復(fù)制代碼上面的Source/Timer/Observer 被統(tǒng)稱為 model item, 一個(gè)item 可以被同時(shí)加入多個(gè) Mode. 但一個(gè)item被重復(fù)加入同一個(gè)mode時(shí)是不會(huì)有效果的。如果一個(gè)mode中一個(gè)item都沒(méi)有,則Runloop會(huì)直接退出,不進(jìn)入循環(huán)。
Mode 間切換 我們平時(shí)在開(kāi)發(fā)中一定遇到過(guò),當(dāng)我們使用NSTimer每一段時(shí)間執(zhí)行一些事情時(shí)滑動(dòng)UIScrollView,NSTimer就會(huì)暫停,當(dāng)我們停止滑動(dòng)以后,NSTimer又會(huì)重新恢復(fù)的情況,我們通過(guò)一段代碼來(lái)看一下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];// 加入到RunLoop中才可以運(yùn)行// 1. 把定時(shí)器添加到RunLoop中,并且選擇默認(rèn)運(yùn)行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];// 當(dāng)textFiled滑動(dòng)的時(shí)候,timer失效,停止滑動(dòng)時(shí),timer恢復(fù)// 原因:當(dāng)textFiled滑動(dòng)的時(shí)候,RunLoop的Mode會(huì)自動(dòng)切換成UITrackingRunLoopMode模式,因此timer失效,當(dāng)停止滑動(dòng),RunLoop又會(huì)切換回NSDefaultRunLoopMode模式,因此timer又會(huì)重新啟動(dòng)了// 2. 當(dāng)我們將timer添加到UITrackingRunLoopMode模式中,此時(shí)只有我們?cè)诨瑒?dòng)textField時(shí)timer才會(huì)運(yùn)行// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];// 3. 那個(gè)如何讓timer在兩個(gè)模式下都可以運(yùn)行呢?// 3.1 在兩個(gè)模式下都添加timer 是可以的,但是timer添加了兩次,并不是同一個(gè)timer// 3.2 使用站位的運(yùn)行模式 NSRunLoopCommonModes標(biāo)記,凡是被打上NSRunLoopCommonModes標(biāo)記的都可以運(yùn)行,下面兩種模式被打上標(biāo)簽//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}// 因此也就是說(shuō)如果我們使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下運(yùn)行[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];NSLog(@"%@",[NSRunLoop mainRunLoop]); } -(void)show {NSLog(@"-------"); } 復(fù)制代碼由上述代碼可以看出,NSTimer不管用是因?yàn)镸ode的切換,因?yàn)槿绻覀冊(cè)谥骶€程使用定時(shí)器,此時(shí)RunLoop的Mode為kCFRunLoopDefaultMode,即定時(shí)器屬于kCFRunLoopDefaultMode,那么此時(shí)我們滑動(dòng)ScrollView時(shí),RunLoop的Mode會(huì)切換到UITrackingRunLoopMode,因此在主線程的定時(shí)器就不在管用了,調(diào)用的方法也就不再執(zhí)行了,當(dāng)我們停止滑動(dòng)時(shí),RunLoop的Mode切換回kCFRunLoopDefaultMode,所有NSTimer就又管用了。
使用GCD也可以創(chuàng)建計(jì)時(shí)器,而且更為精確:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {//創(chuàng)建隊(duì)列dispatch_queue_t queue = dispatch_get_global_queue(0, 0);//1.創(chuàng)建一個(gè)GCD定時(shí)器/*第一個(gè)參數(shù):表明創(chuàng)建的是一個(gè)定時(shí)器第四個(gè)參數(shù):隊(duì)列*/dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);// 需要對(duì)timer進(jìn)行強(qiáng)引用,保證其不會(huì)被釋放掉,才會(huì)按時(shí)調(diào)用block塊// 局部變量,讓指針強(qiáng)引用self.timer = timer;//2.設(shè)置定時(shí)器的開(kāi)始時(shí)間,間隔時(shí)間,精準(zhǔn)度/*第1個(gè)參數(shù):要給哪個(gè)定時(shí)器設(shè)置第2個(gè)參數(shù):開(kāi)始時(shí)間第3個(gè)參數(shù):間隔時(shí)間第4個(gè)參數(shù):精準(zhǔn)度 一般為0 在允許范圍內(nèi)增加誤差可提高程序的性能GCD的單位是納秒 所以要*NSEC_PER_SEC*/dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);//3.設(shè)置定時(shí)器要執(zhí)行的事情dispatch_source_set_event_handler(timer, ^{NSLog(@"---%@--",[NSThread currentThread]);});// 啟動(dòng)dispatch_resume(timer); } 復(fù)制代碼7.2 CFRunLoopSourceRef
Source分為兩種:
Source0:非基于Port的 用于用戶主動(dòng)觸發(fā)的事件(點(diǎn)擊button 或點(diǎn)擊屏幕) Source1:基于Port的 通過(guò)內(nèi)核和其他線程相互發(fā)送消息(與內(nèi)核相關(guān)) 注意:Source1在處理的時(shí)候會(huì)分發(fā)一些操作給Source0去處理
7.3 CFRunLoopTimer
NSTimer是對(duì)RunLoopTimer的封裝
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel; - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;復(fù)制代碼7.4 CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽(tīng)RunLoop的狀態(tài)改變。 我們直接來(lái)看代碼,給RunLoop添加監(jiān)聽(tīng)者,監(jiān)聽(tīng)其運(yùn)行狀態(tài):
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {//創(chuàng)建監(jiān)聽(tīng)者/*第一個(gè)參數(shù) CFAllocatorRef allocator:分配存儲(chǔ)空間 CFAllocatorGetDefault()默認(rèn)分配第二個(gè)參數(shù) CFOptionFlags activities:要監(jiān)聽(tīng)的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽(tīng)所有狀態(tài)第三個(gè)參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽(tīng) NO:不持續(xù)第四個(gè)參數(shù) CFIndex order:優(yōu)先級(jí),一般填0即可第五個(gè)參數(shù) :回調(diào) 兩個(gè)參數(shù)observer:監(jiān)聽(tīng)者 activity:監(jiān)聽(tīng)的事件*//*所有事件typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入RunLoopkCFRunLoopBeforeTimers = (1UL << 1), // 即將處理TimerkCFRunLoopBeforeSources = (1UL << 2), // 即將處理SourcekCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒kCFRunLoopExit = (1UL << 7),// 即將退出RunLoopkCFRunLoopAllActivities = 0x0FFFFFFFU};*/CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry:NSLog(@"RunLoop進(jìn)入");break;case kCFRunLoopBeforeTimers:NSLog(@"RunLoop要處理Timers了");break;case kCFRunLoopBeforeSources:NSLog(@"RunLoop要處理Sources了");break;case kCFRunLoopBeforeWaiting:NSLog(@"RunLoop要休息了");break;case kCFRunLoopAfterWaiting:NSLog(@"RunLoop醒來(lái)了");break;case kCFRunLoopExit:NSLog(@"RunLoop退出了");break;default:break;}});// 給RunLoop添加監(jiān)聽(tīng)者/*第一個(gè)參數(shù) CFRunLoopRef rl:要監(jiān)聽(tīng)哪個(gè)RunLoop,這里監(jiān)聽(tīng)的是主線程的RunLoop第二個(gè)參數(shù) CFRunLoopObserverRef observer 監(jiān)聽(tīng)者第三個(gè)參數(shù) CFStringRef mode 要監(jiān)聽(tīng)RunLoop在哪種運(yùn)行模式下的狀態(tài)*/CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);/*CF的內(nèi)存管理(Core Foundation)凡是帶有Create、Copy、Retain等字眼的函數(shù),創(chuàng)建出來(lái)的對(duì)象,都需要在最后做一次releaseGCD本來(lái)在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經(jīng)納入到了ARC中,所以我們不需要管了*/CFRelease(observer); } 復(fù)制代碼運(yùn)行結(jié)果:
8. Runloop 退出
9. 一些有關(guān)Runloop的問(wèn)題
9.1 基于NSTimer的輪播器什么情況下會(huì)被頁(yè)面滾動(dòng)暫停,怎樣可以不被暫停,為什么?
NSTimer不管用是因?yàn)镸ode的切換,因?yàn)槿绻覀冊(cè)谥骶€程使用定時(shí)器,此時(shí)RunLoop的Mode為kCFRunLoopDefaultMode,即定時(shí)器屬于kCFRunLoopDefaultMode,那么此時(shí)我們滑動(dòng)ScrollView時(shí),RunLoop的Mode會(huì)切換到UITrackingRunLoopMode,因此在主線程的定時(shí)器就不在管用了,調(diào)用的方法也就不再執(zhí)行了,當(dāng)我們停止滑動(dòng)時(shí),RunLoop的Mode切換回kCFRunLoopDefaultMode,所有NSTimer就又管用了。若想定時(shí)器繼續(xù)執(zhí)行,需要將NSTimer 注冊(cè)為 kCFRunLoopCommonModes 。
9.2 延遲執(zhí)行performSelecter相關(guān)方法是怎樣被執(zhí)行的?在子線程中也是一樣的嗎?
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒(méi)有 RunLoop,則這個(gè)方法會(huì)失效。 當(dāng)調(diào)用 performSelector:onThread: 時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒(méi)有 RunLoop 該方法也會(huì)失效。
9.3 事件響應(yīng)和手勢(shì)識(shí)別底層處理是一致的嗎,為什么?
事件響應(yīng): 蘋果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。 當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。隨后蘋果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。 _UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點(diǎn)擊、touchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的。
手勢(shì)識(shí)別: 當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。 蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。 當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí),這個(gè)回調(diào)都會(huì)進(jìn)行相應(yīng)處理。
9.4 界面刷新時(shí),是在什么時(shí)候會(huì)真正執(zhí)行刷新,為什么會(huì)刷新不及時(shí)?
當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。
蘋果注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個(gè)很長(zhǎng)的函數(shù):_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。所以說(shuō)界面刷新并不一定是在setNeedsLayout相關(guān)的代碼執(zhí)行后立刻進(jìn)行的。
9.5 項(xiàng)目程序運(yùn)行中,總是伴隨著多次自動(dòng)釋放池的創(chuàng)建和銷毀,這些是在什么時(shí)候發(fā)生的呢?
系統(tǒng)就是通過(guò)@autoreleasepool {}這種方式來(lái)為我們創(chuàng)建自動(dòng)釋放池的,一個(gè)線程對(duì)應(yīng)一個(gè)runloop,系統(tǒng)會(huì)為每一個(gè)runloop隱式的創(chuàng)建一個(gè)自動(dòng)釋放池,所有的autoreleasePool構(gòu)成一個(gè)棧式結(jié)構(gòu),在每個(gè)runloop結(jié)束時(shí),當(dāng)前棧頂?shù)腶utoreleasePool會(huì)被銷毀,而且會(huì)對(duì)其中的每一個(gè)對(duì)象做一次release(嚴(yán)格來(lái)說(shuō),是你對(duì)這個(gè)對(duì)象做了幾次autorelease就會(huì)做幾次release,不一定是一次),特別指出,使用容器的block版本的枚舉器的時(shí)候,系統(tǒng)會(huì)自動(dòng)添加一個(gè)autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { // 這里被一個(gè)局部@autoreleasepool包圍著 }]; 復(fù)制代碼9.6 當(dāng)我們?cè)谧泳€程上需要執(zhí)行代理方法或者回調(diào)時(shí),怎么確保當(dāng)前線程沒(méi)有被銷毀?
首先引入一個(gè)概念:Event_loop,一般一個(gè)線程執(zhí)行完任務(wù)后就會(huì)退出,當(dāng)需要保證該線程不退出,可以通過(guò)類似以下方式:
function do_loop() {initialize();do {var message = get_next_message();process_message(message);} while (message != quit); } 復(fù)制代碼開(kāi)啟一個(gè)循環(huán),保證線程不退出,這就是Event_loop模型。這是在很多操作系統(tǒng)中都使用的模型,例如OS/iOS中的RunLoop。這種模型最大的作用就是管理事件/消息,在有新消息到來(lái)時(shí)立刻喚醒處理,沒(méi)有待處理消息時(shí)線程休眠,避免資源浪費(fèi)。
10 Runloop 使用
10.1 AFNetworking
使用NSOperation+NSURLConnection并發(fā)模型都會(huì)面臨NSURLConnection下載完成前線程退出導(dǎo)致NSOperation對(duì)象接收不到回調(diào)的問(wèn)題。AFNetWorking解決這個(gè)問(wèn)題的方法是按照官方的guid NSURLConnection 上寫(xiě)的NSURLConnection的delegate方法需要在connection發(fā)起的線程runloop中調(diào)用,于是AFNetWorking直接借鑒了Apple自己的一個(gè)Demo的實(shí)現(xiàn)方法單獨(dú)起一個(gè)global thread,內(nèi)置一個(gè)runloop,所有的connection都由這個(gè)runloop發(fā)起,回調(diào)也是它接收,不占用主線程,也不耗CPU資源。
+ (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:selfselector:@selector(networkRequestThreadEntryPoint:)object:nil];[_networkRequestThread start];});return _networkRequestThread; } 復(fù)制代碼類似的可以用這個(gè)方法創(chuàng)建一個(gè)常駐服務(wù)的線程。
10.2 TableView中實(shí)現(xiàn)平滑滾動(dòng)延遲加載圖片
利用CFRunLoopMode的特性,可以將圖片的加載放到NSDefaultRunLoopMode的mode里,這樣在滾動(dòng)UITrackingRunLoopMode這個(gè)mode時(shí)不會(huì)被加載而影響到。
UIImage *downloadedImage = ...; [self.imageView performSelector:@selector(setImage:)withObject:downloadedImageafterDelay:0inModes:@[NSDefaultRunLoopMode]]; 復(fù)制代碼10.3 接到程序崩潰時(shí)的信號(hào)進(jìn)行自主處理例如彈出提示等
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop)); while (1) {for (NSString *mode in allModes) {CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);} } 復(fù)制代碼10.4 異步測(cè)試
- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout {__block Boolean fulfilled = NO;void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {fulfilled = block();if (fulfilled) {CFRunLoopStop(CFRunLoopGetCurrent());}};CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// Run!CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);CFRelease(observer);return fulfilled; } 復(fù)制代碼總結(jié)
以上是生活随笔為你收集整理的iOS底层原理探究-Runloop的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 如何做梦梦到春梦
- 下一篇: Spark 键值对RDD操作