iOS底層原理探究-Runloop

極客學偉發表於2018-05-11

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 queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待核心mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。。。
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 處理非同步方法喚醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 處理Source1
        else
            __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.

RunloopModels

6.2 主執行緒想關聯的Runloop建立

CFRunloopRef 原始碼

// 建立字典
 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);

// 建立主執行緒 根據傳入的主執行緒建立主執行緒對應的RunLoop
 CFRunLoopRef 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找到對應mode
    CFRunLoopModeRef 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),   //   即將進入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
     kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
     kCFRunLoopAllActivities = 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等字眼的函式,建立出來的物件,都需要在最後做一次release
     GCD本來在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:self
               selector:@selector(networkRequestThreadEntryPoint:)
               object:nil];
          [_networkRequestThread start];
     });

     return _networkRequestThread;
}
複製程式碼

類似的可以用這個方法建立一個常駐服務的執行緒。

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

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

UIImage *downloadedImage = ...;
[self.imageView performSelector:@selector(setImage:)
     withObject:downloadedImage
     afterDelay:0
     inModes:@[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;
}
複製程式碼

相關文章