iOS底層原理探究-Runloop

Runloop

1. 概述

一般來說,一個線程只能執行一個任務,執行完就會退出,如果我們需要一種機制,讓線程能隨時處理時間但并不退出,那么 RunLoop 就是這樣的一個機制。Runloop是事件接收和分發機制的一個實現。

RunLoop實際上是一個對象,這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件),從而保持程序的持續運行;而且在沒有事件處理的時候,會進入睡眠模式,從而節省CPU資源,提高程序性能。

簡單的說run loop是事件驅動的一個大循環,如下代碼所示:

int main(int argc, char * argv[]) {//程序一直運行狀態while (AppIsRunning) {//睡眠狀態,等待喚醒事件id whoWakesMe = SleepForWakingUp();//得到喚醒事件id event = GetEvent(whoWakesMe);//開始處理事件HandleEvent(event);}return 0;
}
復制代碼

2. Runloop 基本作用

2.1 保持程序持續運行

程序一啟動就會開一個主線程,主線程一開起來就會跑一個主線程對應的Runloop, Runloop保證主線程不會被銷毀,也就保證了程序的持續運行。不光iOS,在其他的編程平臺,Android, Windows等都有一個類似Runloop的機制保證程序的持續運行。

2.2 處理App中的各類事件

系統級別

GCD, mach kernel, block, pthread

應用層

NSTimer, UIEvent, Autorelease, NSObject(NSDelayedPerforming), NSObject(NSThreadPerformAddition), CADisplayLink, CATransition, CAAnimation, dispatch_get_main_queue() (GCD 中dispatch到main queue的block會被dispatch到main Runloop中執行), NSPort, NSURLConnection, AFNetworking(這個第三方網絡請求框架使用在開啟新線程中添加自己到Runloop監聽事件)

2.3 節省CPU資源,提高程序性能

程序運行起來時,當什么操作都沒有做的時候,Runloop告訴CPU, 現在沒有事情做,我要去休息, 這時CPU就會將資源釋放出來去做其他的事情,當有事情做的時候Runloop就會立馬起來去做事情。

3. Runloop 的開啟

程序入口

iOS 程序的入口是 main 函數

int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}
}
復制代碼

程序主線程一開起來,就會跑一個和主線程對應的Runloop, 那么Runloop一定是在程序的入口main函數中開啟。

在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開源代碼: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);
}
復制代碼

我們發現RunLoop確實是do while通過判斷result的值實現的。因此,我們可以把RunLoop看成一個死循環。如果沒有RunLoop,UIApplicationMain函數執行完畢之后將直接返回,也就沒有程序持續運行一說了。

執行順序的偽代碼:

int32_t __CFRunLoopRun()
{// 通知即將進入runloop__CFRunLoopDoObservers(KCFRunLoopEntry);do{// 通知將要處理timer和source__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);__CFRunLoopDoObservers(kCFRunLoopBeforeSources);// 處理非延遲的主線程調用__CFRunLoopDoBlocks();// 處理Source0事件__CFRunLoopDoSource0();if (sourceHandledThisLoop) {__CFRunLoopDoBlocks();}/// 如果有 Source1 (基于port) 處于 ready 狀態,直接處理這個 Source1 然后跳轉去處理消息。if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort();if (hasMsg) goto handle_msg;}/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}// GCD dispatch main queueCheckIfExistMessagesInMainDispatchQueue();// 即將進入休眠__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);// 等待內核mach_msg事件mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();// 等待。。。// 從等待中醒來__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);// 處理因timer的喚醒if (wakeUpPort == timerPort)__CFRunLoopDoTimers();// 處理異步方法喚醒,如dispatch_asyncelse if (wakeUpPort == mainDispatchQueuePort)__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()// 處理Source1else__CFRunLoopDoSource1();// 再次確保是否有同步的方法需要調用__CFRunLoopDoBlocks();} while (!stop && !timeout);// 通知即將退出runloop__CFRunLoopDoObservers(CFRunLoopExit);
}
復制代碼

5. Runloop 對象

RunLoop對象包括Fundation中的NSRunLoop對象和CoreFoundation中的CFRunLoopRef對象。因為Fundation框架是基于CFRunLoopRef的封裝,因此我們學習RunLoop還是要研究CFRunLoopRef 源碼。

獲得Runloop 對象

//Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象//Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
復制代碼

值的注意的是子線程中的runloop不是默認開啟的,需要手動開啟,當調用 [NSRunLoop currentRunLoop] 時,若已存在當前線程的runloop返回,若不存在創建一個新的runloop對象再返回。

6. Runloop 和 線程

6.1 Runloop 和 線程 之間的關系

  1. 每條線程都有唯一的一個與之對應的Runloop 對象
  2. 主線程的Runloop已經自動創建好了,子線程的Runloop需要手動創建
  3. Runloop在第一次獲取時創建,在線程結束時銷毀
  4. Thread 包含一個CFRunloop, 一個CFRunloop 包含一種CFRunloopMode, model 包含 CFRunloopSource, CFRunloopTimer, CFRunloopObserver.

6.2 主線程想關聯的Runloop創建

CFRunloopRef 源碼

// 創建字典CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);// 創建主線程 根據傳入的主線程創建主線程對應的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());// 保存主線程 將主線程-key和RunLoop-Value保存到字典中CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
復制代碼

6.3 創建與子線程想關聯的Runloop

Apple 不允許直接創建Runloop, 它只提供了兩個自動獲取的函數: CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 CFRunLoopRef源碼:

/// 用DefaultMode啟動
void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}/// 用指定的Mode啟動,允許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {/// 首先根據modeName找到對應modeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);/// 如果mode里沒有source/timer/observer, 直接返回。if (__CFRunLoopModeIsEmpty(currentMode)) return;/// 1. 通知 Observers: RunLoop 即將進入 loop。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);/// 內部函數,進入loop__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO;int retVal = 0;do {/// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);/// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);/// 執行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 4. RunLoop 觸發 Source0 (非port) 回調。sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);/// 執行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 5. 如果有 Source1 (基于port) 處于 ready 狀態,直接處理這個 Source1 然后跳轉去處理消息。if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)if (hasMsg) goto handle_msg;}/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}/// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。/// ? 一個基于 port 的Source 的事件。/// ? 一個 Timer 到時間了/// ? RunLoop 自身的超時時間到了/// ? 被其他什么調用者手動喚醒__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 如果一個 Timer 到時間了,觸發這個Timer的回調。if (msg_is_timer) {__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} /// 9.2 如果有dispatch到main_queue的block,執行block。else if (msg_is_dispatch) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);} /// 9.3 如果一個 Source1 (基于port) 發出事件了,處理這個事件else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);if (sourceHandledThisLoop) {mach_msg(reply, MACH_SEND_MSG, reply);}}/// 執行加入到Loop的block__CFRunLoopDoBlocks(runloop, currentMode);if (sourceHandledThisLoop && stopAfterHandle) {/// 進入loop時參數說處理完事件就返回。retVal = kCFRunLoopRunHandledSource;} else if (timeout) {/// 超出傳入參數標記的超時時間了retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(runloop)) {/// 被外部調用者強制停止了retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {/// source/timer/observer一個都沒有了retVal = kCFRunLoopRunFinished;}/// 如果沒超時,mode里沒空,loop也沒被停止,那繼續loop。} while (retVal == 0);}/// 10. 通知 Observers: RunLoop 即將退出。__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
復制代碼

可以看出,線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 里。線程剛創建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。

[NSRunLoop currentRunLoop];方法調用時,會先看一下字典里有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop,如果沒有則會創建一個,并將與之對應的子線程存入字典中。

7. Runloop 相關類

Core Foundation中關于RunLoop的5個類:

CFRunLoopRef  //獲得當前RunLoop和主RunLoop
CFRunLoopModeRef  //運行模式,只能選擇一種,在不同模式中做不同的操作
CFRunLoopSourceRef  //事件源,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者
復制代碼

7.1 CFRunLoopModeRef

一個Runloop包含若干個Mode, 每個Mode又包含若干個Source / Timer / Observer. 每次調用Runloop 的主函數時,只能指定其中一個Mode, 這個Mode被稱作 CurrentMode. 如果需要切換Mode, 只能退出Loop, 再重新指定一個Mode進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer, 讓其互不影響。

系統默認注冊了 5 個Mode, 其中常見的有第 1,2 種:

1. kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個占位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode
復制代碼

上面的Source/Timer/Observer 被統稱為 model item, 一個item 可以被同時加入多個 Mode. 但一個item被重復加入同一個mode時是不會有效果的。如果一個mode中一個item都沒有,則Runloop會直接退出,不進入循環。

Mode 間切換 我們平時在開發中一定遇到過,當我們使用NSTimer每一段時間執行一些事情時滑動UIScrollView,NSTimer就會暫停,當我們停止滑動以后,NSTimer又會重新恢復的情況,我們通過一段代碼來看一下:

-(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中才可以運行// 1. 把定時器添加到RunLoop中,并且選擇默認運行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];// 當textFiled滑動的時候,timer失效,停止滑動時,timer恢復// 原因:當textFiled滑動的時候,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式,因此timer失效,當停止滑動,RunLoop又會切換回NSDefaultRunLoopMode模式,因此timer又會重新啟動了// 2. 當我們將timer添加到UITrackingRunLoopMode模式中,此時只有我們在滑動textField時timer才會運行// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];// 3. 那個如何讓timer在兩個模式下都可以運行呢?// 3.1 在兩個模式下都添加timer 是可以的,但是timer添加了兩次,并不是同一個timer// 3.2 使用站位的運行模式 NSRunLoopCommonModes標記,凡是被打上NSRunLoopCommonModes標記的都可以運行,下面兩種模式被打上標簽//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}// 因此也就是說如果我們使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下運行[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{NSLog(@"-------");
}
復制代碼

由上述代碼可以看出,NSTimer不管用是因為Mode的切換,因為如果我們在主線程使用定時器,此時RunLoop的Mode為kCFRunLoopDefaultMode,即定時器屬于kCFRunLoopDefaultMode,那么此時我們滑動ScrollView時,RunLoop的Mode會切換到UITrackingRunLoopMode,因此在主線程的定時器就不在管用了,調用的方法也就不再執行了,當我們停止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode,所有NSTimer就又管用了。

使用GCD也可以創建計時器,而且更為精確:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{//創建隊列dispatch_queue_t queue = dispatch_get_global_queue(0, 0);//1.創建一個GCD定時器/*第一個參數:表明創建的是一個定時器第四個參數:隊列*/dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);// 需要對timer進行強引用,保證其不會被釋放掉,才會按時調用block塊// 局部變量,讓指針強引用self.timer = timer;//2.設置定時器的開始時間,間隔時間,精準度/*第1個參數:要給哪個定時器設置第2個參數:開始時間第3個參數:間隔時間第4個參數:精準度 一般為0 在允許范圍內增加誤差可提高程序的性能GCD的單位是納秒 所以要*NSEC_PER_SEC*/dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);//3.設置定時器要執行的事情dispatch_source_set_event_handler(timer, ^{NSLog(@"---%@--",[NSThread currentThread]);});// 啟動dispatch_resume(timer);
}
復制代碼

7.2 CFRunLoopSourceRef

Source分為兩種:

Source0:非基于Port的 用于用戶主動觸發的事件(點擊button 或點擊屏幕) Source1:基于Port的 通過內核和其他線程相互發送消息(與內核相關) 注意:Source1在處理的時候會分發一些操作給Source0去處理

7.3 CFRunLoopTimer

NSTimer是對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;復制代碼

7.4 CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變。 我們直接來看代碼,給RunLoop添加監聽者,監聽其運行狀態:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{//創建監聽者/*第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配第二個參數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態第三個參數 Boolean repeats:YES:持續監聽 NO:不持續第四個參數 CFIndex order:優先級,一般填0即可第五個參數 :回調 兩個參數observer:監聽者 activity:監聽的事件*//*所有事件typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),   //   即將進入RunLoopkCFRunLoopBeforeTimers = (1UL << 1), // 即將處理TimerkCFRunLoopBeforeSources = (1UL << 2), // 即將處理SourcekCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠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進入");break;case kCFRunLoopBeforeTimers:NSLog(@"RunLoop要處理Timers了");break;case kCFRunLoopBeforeSources:NSLog(@"RunLoop要處理Sources了");break;case kCFRunLoopBeforeWaiting:NSLog(@"RunLoop要休息了");break;case kCFRunLoopAfterWaiting:NSLog(@"RunLoop醒來了");break;case kCFRunLoopExit:NSLog(@"RunLoop退出了");break;default:break;}});// 給RunLoop添加監聽者/*第一個參數 CFRunLoopRef rl:要監聽哪個RunLoop,這里監聽的是主線程的RunLoop第二個參數 CFRunLoopObserverRef observer 監聽者第三個參數 CFStringRef mode 要監聽RunLoop在哪種運行模式下的狀態*/CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);/*CF的內存管理(Core Foundation)凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最后做一次releaseGCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經納入到了ARC中,所以我們不需要管了*/CFRelease(observer);
}
復制代碼

運行結果:

8. Runloop 退出

  1. 主線程銷魂Runloop退出
  2. Mode中有一些Timer, Source, Observer, 這些保證Mode不為空時保證Runloop沒有空轉并且是在運行的,當Mode中為空的時候,Runloop會立刻退出。
  3. 我們在啟動Runloop的時候可以設置什么時候停止。
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
復制代碼

9. 一些有關Runloop的問題

9.1 基于NSTimer的輪播器什么情況下會被頁面滾動暫停,怎樣可以不被暫停,為什么?

NSTimer不管用是因為Mode的切換,因為如果我們在主線程使用定時器,此時RunLoop的Mode為kCFRunLoopDefaultMode,即定時器屬于kCFRunLoopDefaultMode,那么此時我們滑動ScrollView時,RunLoop的Mode會切換到UITrackingRunLoopMode,因此在主線程的定時器就不在管用了,調用的方法也就不再執行了,當我們停止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode,所有NSTimer就又管用了。若想定時器繼續執行,需要將NSTimer 注冊為 kCFRunLoopCommonModes 。

9.2 延遲執行performSelecter相關方法是怎樣被執行的?在子線程中也是一樣的嗎?

當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。 當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。

9.3 事件響應和手勢識別底層處理是一致的嗎,為什么?

事件響應: 蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。 當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發回調,并調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。 _UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

手勢識別: 當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨后系統將對應的 UIGestureRecognizer 標記為待處理。 蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,并執行GestureRecognizer的回調。 當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。

9.4 界面刷新時,是在什么時候會真正執行刷新,為什么會刷新不及時?

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。

蘋果注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數里會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。所以說界面刷新并不一定是在setNeedsLayout相關的代碼執行后立刻進行的。

9.5 項目程序運行中,總是伴隨著多次自動釋放池的創建和銷毀,這些是在什么時候發生的呢?

系統就是通過@autoreleasepool {}這種方式來為我們創建自動釋放池的,一個線程對應一個runloop,系統會為每一個runloop隱式的創建一個自動釋放池,所有的autoreleasePool構成一個棧式結構,在每個runloop結束時,當前棧頂的autoreleasePool會被銷毀,而且會對其中的每一個對象做一次release(嚴格來說,是你對這個對象做了幾次autorelease就會做幾次release,不一定是一次),特別指出,使用容器的block版本的枚舉器的時候,系統會自動添加一個autoreleasePool

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 
// 這里被一個局部@autoreleasepool包圍著 
}];
復制代碼

9.6 當我們在子線程上需要執行代理方法或者回調時,怎么確保當前線程沒有被銷毀?

首先引入一個概念:Event_loop,一般一個線程執行完任務后就會退出,當需要保證該線程不退出,可以通過類似以下方式:

function do_loop() {initialize();do {var message = get_next_message();process_message(message);} while (message != quit);
}
復制代碼

開啟一個循環,保證線程不退出,這就是Event_loop模型。這是在很多操作系統中都使用的模型,例如OS/iOS中的RunLoop。這種模型最大的作用就是管理事件/消息,在有新消息到來時立刻喚醒處理,沒有待處理消息時線程休眠,避免資源浪費。

10 Runloop 使用

10.1 AFNetworking

使用NSOperation+NSURLConnection并發模型都會面臨NSURLConnection下載完成前線程退出導致NSOperation對象接收不到回調的問題。AFNetWorking解決這個問題的方法是按照官方的guid NSURLConnection 上寫的NSURLConnection的delegate方法需要在connection發起的線程runloop中調用,于是AFNetWorking直接借鑒了Apple自己的一個Demo的實現方法單獨起一個global thread,內置一個runloop,所有的connection都由這個runloop發起,回調也是它接收,不占用主線程,也不耗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;
}
復制代碼

類似的可以用這個方法創建一個常駐服務的線程。

10.2 TableView中實現平滑滾動延遲加載圖片

利用CFRunLoopMode的特性,可以將圖片的加載放到NSDefaultRunLoopMode的mode里,這樣在滾動UITrackingRunLoopMode這個mode時不會被加載而影響到。

UIImage *downloadedImage = ...;
[self.imageView performSelector:@selector(setImage:)withObject:downloadedImageafterDelay:0inModes:@[NSDefaultRunLoopMode]];
復制代碼

10.3 接到程序崩潰時的信號進行自主處理例如彈出提示等

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {for (NSString *mode in allModes) {CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);}
}
復制代碼

10.4 異步測試

- (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;
}
復制代碼

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/390460.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/390460.shtml
英文地址,請注明出處:http://en.pswp.cn/news/390460.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

p2020開發_2020年最佳開發者社區

p2020開發If you want to grow as a developer, I cant over-emphasize the benefits of joining a developer community. There are many advantages, from peer-programming to sharing knowledge, mentorship, sharing support, sharing tools, code reviews, answering que…

leetcode 1838. 最高頻元素的頻數

元素的 頻數 是該元素在一個數組中出現的次數。 給你一個整數數組 nums 和一個整數 k 。在一步操作中&#xff0c;你可以選擇 nums 的一個下標&#xff0c;并將該下標對應元素的值增加 1 。 執行最多 k 次操作后&#xff0c;返回數組中最高頻元素的 最大可能頻數 。 示例 1&…

Deepin系統手動安裝oracle jdk8詳細教程

Deepin系統手動安裝oracle jdk8詳細教程 oracle官網下載jdk壓縮包&#xff0c;使用 sudo tar -zxf jdk***解壓文件&#xff0c;我放在在了home/diy/java/jdk路徑下。 jdk文件路徑&#xff1a;/home/diy/java/jdk/jdk1.8.0_152 JDK環境變量配置 修改配置文件 sudo vi /etc/profi…

Spark 鍵值對RDD操作

https://www.cnblogs.com/yongjian/p/6425772.html 概述 鍵值對RDD是Spark操作中最常用的RDD&#xff0c;它是很多程序的構成要素&#xff0c;因為他們提供了并行操作各個鍵或跨界點重新進行數據分組的操作接口。 創建 Spark中有許多中創建鍵值對RDD的方式&#xff0c;其中包括…

無服務器架構_如何開始使用無服務器架構

無服務器架構Traditionally, when you wanted to build a web app or API, you’d usually have to spend significant time and effort managing servers and ensuring your app scales up to handle large request volumes. Serverless is a cloud computing model which let…

WPF中的動畫——(一)基本概念

原文:WPF中的動畫——&#xff08;一&#xff09;基本概念WPF的一個特點就是支持動畫&#xff0c;我們可以非常容易的實現漂亮大方的界面。首先&#xff0c;我們來復習一下動畫的基本概念。計算機中的動畫一般是定格動畫&#xff0c;也稱之為逐幀動畫&#xff0c;它通過每幀不同…

cloud 異步遠程調用_異步遠程工作的意外好處-以及如何擁抱它們

cloud 異步遠程調用In this article, Ill discuss the positive aspects of being a little out of sync with your team.在本文中&#xff0c;我將討論與您的團隊有點不同步的積極方面。 So you’ve started working from home.因此&#xff0c;您已經開始在家工作。 There …

linux 問題一 apt-get install 被 lock

問題&#xff1a; sudo apt-get install vim E: Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavailable)E: Unable to lock the administration directory (/var/lib/dpkg/), is another process using it? 解決&#xff1a; sudo rm /var/cac…

工信部高級軟件工程師_作為新軟件工程師的信

工信部高級軟件工程師Dear Self, 親愛的自我&#xff0c; You just graduated and you are ready to start your career in the IT field. I cannot spoil anything, but I assure you it will be an interesting ride. 您剛剛畢業&#xff0c;就可以開始在IT領域的職業了。 我…

Python高級網絡編程系列之基礎篇

一、Socket簡介 1、不同電腦上的進程如何通信&#xff1f; 進程間通信的首要問題是如何找到目標進程&#xff0c;也就是操作系統是如何唯一標識一個進程的&#xff01; 在一臺電腦上是只通過進程號PID&#xff0c;但在網絡中是行不通的&#xff0c;因為每臺電腦的IP可能都是不一…

多線程編程和單線程編程_生活與編程的平行線程

多線程編程和單線程編程I’m convinced our deepest desire is, by paying the cost of time, to be shown a glimmer of some fundamental truth about the universe. To hear it whisper its lessons and point towards its purpose.我堅信&#xff0c;我們最深切的愿望是通過…

劍指 Offer 67. 把字符串轉換成整數

寫一個函數 StrToInt&#xff0c;實現把字符串轉換成整數這個功能。不能使用 atoi 或者其他類似的庫函數。 首先&#xff0c;該函數會根據需要丟棄無用的開頭空格字符&#xff0c;直到尋找到第一個非空格的字符為止。 當我們尋找到的第一個非空字符為正或者負號時&#xff0c…

搭建MSSM框架(Maven+Spring+Spring MVC+MyBatis)

https://github.com/easonjim/ssm-framework 先欠著&#xff0c;后續再進行講解&#xff1a; 一、Spring內核集成 二、Spring MVC集成 三、MyBatis集成 四、代碼生成工具集成 >如有問題&#xff0c;請聯系我&#xff1a;easonjim#163.com&#xff0c;或者下方發表評論。<…

4.RabbitMQ Linux安裝

這里使用的Linux是CentOS6.2 將/etc/yum.repo.d/目錄下的所有repo文件刪除 先下載epel源 # wget -O /etc/yum.repos.d/epel-erlang.repo http://repos.fedorapeople.org/repos/peter/erlang/epel-erlang.repo 修改epel-erlang.repo文件&#xff0c;如下圖 添加CentOS 的下載源…

freecodecamp_如何對freeCodeCamp文章提供反饋

freecodecampWe at the freeCodeCamp editorial team do our best to ensure articles are as accurate as they can be.我們的freeCodeCamp編輯團隊竭盡所能&#xff0c;以確保文章盡可能準確。 Still, we occasionally miss factual inaccuracies, non-functioning code exa…

如何對接oracle 建立pdb

Oracle數據庫的結構是一個數據庫實例下有許多用戶&#xff0c;每一個用戶有自己的表空間&#xff0c;即每一個用戶相當于MySQL中的一個數據庫。不久前下了oracle 12c的數據庫&#xff0c;安裝之后建user時才知道oracle12c 有一個很大的變動就是引入了pdb可插入數據庫&#xff0…

二、數據庫設計與操作

一、 數據庫設計仿QQ數據庫一共包括5張數據表&#xff0c;每張數據表結構如下&#xff1a;1、 tb_User&#xff08;用戶信息表&#xff09;這張表主要用來存儲用戶的好友關系與信息字段名數據類型是否Null值默認值綁定描述IDint否用戶賬號PwdVarchar(50)否用戶密碼Frie…

hdu 過山車_從機械工程師到軟件開發人員–我的編碼過山車

hdu 過山車There arent many people out there who grew up dreaming of writing code. I definitely didnt. I wanted to design cars. But somehow I ended up building software.很少有人夢見編寫代碼。 我絕對沒有。 我想設計汽車。 但是我最終以某種方式開發了軟件。 I u…

mysql 兩列互換

mysql 如果想互換兩列的值&#xff0c;直接寫 update 表 set col1col2&#xff0c;col2col1 這樣的后果就是兩列都是 col2 的值 注意這和sql server 是不同的&#xff0c; 如果想實現上述功能&#xff0c;添加一個自增列作為標識&#xff08;必須的&#xff09;&#xff0c; u…

劍指 Offer 36. 二叉搜索樹與雙向鏈表

輸入一棵二叉搜索樹&#xff0c;將該二叉搜索樹轉換成一個排序的循環雙向鏈表。要求不能創建任何新的節點&#xff0c;只能調整樹中節點指針的指向。 為了讓您更好地理解問題&#xff0c;以下面的二叉搜索樹為例&#xff1a; 我們希望將這個二叉搜索樹轉化為雙向循環鏈表。鏈表…