iOS底層探究-Runloop

Runloop

1. 概述

一般來說凳枝,一個線程只能執(zhí)行一個任務(wù)抄沮,執(zhí)行完就會退出,如果我們需要一種機(jī)制岖瑰,讓線程能隨時處理時間但并不退出叛买,那么 RunLoop 就是這樣的一個機(jī)制。Runloop是事件接收和分發(fā)機(jī)制的一個實現(xiàn)蹋订。

RunLoop實際上是一個對象率挣,這個對象在循環(huán)中用來處理程序運行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件露戒、定時器事件椒功、Selector事件)捶箱,從而保持程序的持續(xù)運行;而且在沒有事件處理的時候动漾,會進(jìn)入睡眠模式丁屎,從而節(jié)省CPU資源,提高程序性能旱眯。

簡單的說run loop是事件驅(qū)動的一個大循環(huán)晨川,如下代碼所示:

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

2. Runloop 基本作用

2.1 保持程序持續(xù)運行

程序一啟動就會開一個主線程删豺,主線程一開起來就會跑一個主線程對應(yīng)的Runloop, Runloop保證主線程不會被銷毀共虑,也就保證了程序的持續(xù)運行。不光iOS吼鳞,在其他的編程平臺看蚜,Android, Windows等都有一個類似Runloop的機(jī)制保證程序的持續(xù)運行。

2.2 處理App中的各類事件

系統(tǒng)級別

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會被dispatch到main Runloop中執(zhí)行)赔桌, NSPort, NSURLConnection, AFNetworking(這個第三方網(wǎng)絡(luò)請求框架使用在開啟新線程中添加自己到Runloop監(jiān)聽事件)

2.3 節(jié)省CPU資源供炎,提高程序性能

程序運行起來時,當(dāng)什么操作都沒有做的時候疾党,Runloop告訴CPU, 現(xiàn)在沒有事情做音诫,我要去休息, 這時CPU就會將資源釋放出來去做其他的事情雪位,當(dāng)有事情做的時候Runloop就會立馬起來去做事情竭钝。

3. Runloop 的開啟

程序入口

iOS 程序的入口是 main 函數(shù)

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

程序主線程一開起來,就會跑一個和主線程對應(yīng)的Runloop, 那么Runloop一定是在程序的入口main函數(shù)中開啟雹洗。

在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);
}

我們發(fā)現(xiàn)RunLoop確實是do while通過判斷result的值實現(xiàn)的螃成。因此旦签,我們可以把RunLoop看成一個死循環(huán)。如果沒有RunLoop寸宏,UIApplicationMain函數(shù)執(zhí)行完畢之后將直接返回宁炫,也就沒有程序持續(xù)運行一說了。

執(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)氮凝,直接處理這個 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 queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進(jìn)入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內(nèi)核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();
        
        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

5. Runloop 對象

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

獲得Runloop 對象

//Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
 
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

值的注意的是子線程中的runloop不是默認(rèn)開啟的常摧,需要手動開啟搅吁,當(dāng)調(diào)用 [NSRunLoop currentRunLoop] 時,若已存在當(dāng)前線程的runloop返回落午,若不存在創(chuàng)建一個新的runloop對象再返回谎懦。

6. Runloop 和 線程

6.1 Runloop 和 線程 之間的關(guān)系

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

6.2 主線程想關(guān)聯(lián)的Runloop創(chuàng)建

CFRunloopRef 源碼

// 創(chuàng)建字典
 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);

// 創(chuàng)建主線程 根據(jù)傳入的主線程創(chuàng)建主線程對應(yīng)的RunLoop
 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

6.3 創(chuàng)建與子線程想關(guān)聯(lián)的Runloop

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

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據(jù)modeName找到對應(yīng)mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里沒有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)走哺,直接處理這個 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)入休眠, 直到被下面某一個事件喚醒择示。
            /// ? 一個基于 port 的Source 的事件。
            /// ? 一個 Timer 到時間了
            /// ? RunLoop 自身的超時時間到了
            /// ? 被其他什么調(diào)用者手動喚醒
            __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 到時間了敢朱,觸發(fā)這個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 如果一個 Source1 (基于port) 發(fā)出事件了孝常,處理這個事件
            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ù)說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標(biāo)記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強(qiáng)制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時蚓哩,mode里沒空构灸,loop也沒被停止,那繼續(xù)loop岸梨。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出喜颁。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看出稠氮,線程和 RunLoop 之間是一一對應(yīng)的,其關(guān)系是保存在一個全局的 Dictionary 里半开。線程剛創(chuàng)建時并沒有 RunLoop隔披,如果你不主動獲取,那它一直都不會有寂拆。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時奢米,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)纠永。

[NSRunLoop currentRunLoop];方法調(diào)用時鬓长,會先看一下字典里有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop尝江,如果沒有則會創(chuàng)建一個涉波,并將與之對應(yīng)的子線程存入字典中。

7. Runloop 相關(guān)類

Core Foundation中關(guān)于RunLoop的5個類:

CFRunLoopRef  //獲得當(dāng)前RunLoop和主RunLoop
CFRunLoopModeRef  //運行模式炭序,只能選擇一種啤覆,在不同模式中做不同的操作
CFRunLoopSourceRef  //事件源,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者

7.1 CFRunLoopModeRef

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

系統(tǒng)默認(rèn)注冊了 5 個Mode, 其中常見的有第 1彼妻,2 種:

1. kCFRunLoopDefaultMode:App的默認(rèn)Mode嫌佑,通常主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動侨歉,保證界面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode屋摇,啟動完成后就不再使用
4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個占位用的Mode幽邓,作為標(biāo)記kCFRunLoopDefaultMode和UITrackingRunLoopMode用炮温,并不是一種真正的Mode

上面的Source/Timer/Observer 被統(tǒng)稱為 model item, 一個item 可以被同時加入多個 Mode. 但一個item被重復(fù)加入同一個mode時是不會有效果的牵舵。如果一個mode中一個item都沒有柒啤,則Runloop會直接退出,不進(jìn)入循環(huán)畸颅。

Mode 間切換
我們平時在開發(fā)中一定遇到過担巩,當(dāng)我們使用NSTimer每一段時間執(zhí)行一些事情時滑動UIScrollView,NSTimer就會暫停没炒,當(dāng)我們停止滑動以后涛癌,NSTimer又會重新恢復(fù)的情況,我們通過一段代碼來看一下:

-(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中,并且選擇默認(rèn)運行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 當(dāng)textFiled滑動的時候拳话,timer失效先匪,停止滑動時,timer恢復(fù)
    // 原因:當(dāng)textFiled滑動的時候弃衍,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式呀非,因此timer失效,當(dāng)停止滑動镜盯,RunLoop又會切換回NSDefaultRunLoopMode模式姜钳,因此timer又會重新啟動了

    // 2. 當(dāng)我們將timer添加到UITrackingRunLoopMode模式中,此時只有我們在滑動textField時timer才會運行
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // 3. 那個如何讓timer在兩個模式下都可以運行呢形耗?
    // 3.1 在兩個模式下都添加timer 是可以的,但是timer添加了兩次辙浑,并不是同一個timer
    // 3.2 使用站位的運行模式 NSRunLoopCommonModes標(biāo)記激涤,凡是被打上NSRunLoopCommonModes標(biāo)記的都可以運行,下面兩種模式被打上標(biāo)簽
    //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,因此在主線程的定時器就不在管用了功蜓,調(diào)用的方法也就不再執(zhí)行了园爷,當(dāng)我們停止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode式撼,所有NSTimer就又管用了童社。

使用GCD也可以創(chuàng)建計時器,而且更為精確:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //創(chuàng)建隊列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //1.創(chuàng)建一個GCD定時器
    /*
     第一個參數(shù):表明創(chuàng)建的是一個定時器
     第四個參數(shù):隊列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 需要對timer進(jìn)行強(qiáng)引用著隆,保證其不會被釋放掉扰楼,才會按時調(diào)用block塊
    // 局部變量,讓指針強(qiáng)引用
    self.timer = timer;
    //2.設(shè)置定時器的開始時間,間隔時間,精準(zhǔn)度
    /*
     第1個參數(shù):要給哪個定時器設(shè)置
     第2個參數(shù):開始時間
     第3個參數(shù):間隔時間
     第4個參數(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è)置定時器要執(zhí)行的事情
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"---%@--",[NSThread currentThread]);
    });
    // 啟動
    dispatch_resume(timer);
}

7.2 CFRunLoopSourceRef

Source分為兩種:

Source0:非基于Port的 用于用戶主動觸發(fā)的事件(點擊button 或點擊屏幕)
Source1:基于Port的 通過內(nèi)核和其他線程相互發(fā)送消息(與內(nèi)核相關(guān))
注意:Source1在處理的時候會分發(fā)一些操作給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是觀察者美浦,能夠監(jiān)聽RunLoop的狀態(tài)改變弦赖。
我們直接來看代碼,給RunLoop添加監(jiān)聽者抵代,監(jiān)聽其運行狀態(tài):

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     //創(chuàng)建監(jiān)聽者
     /*
     第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認(rèn)分配
     第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
     第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
     第四個參數(shù) CFIndex order:優(yōu)先級腾节,一般填0即可
     第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
     */
     /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即將進(jìn)入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠
     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進(jìn)入");
                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添加監(jiān)聽者
    /*
     第一個參數(shù) CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop
     第二個參數(shù) CFRunLoopObserverRef observer 監(jiān)聽者
     第三個參數(shù) CFStringRef mode 要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的內(nèi)存管理(Core Foundation)
     凡是帶有Create、Copy、Retain等字眼的函數(shù)案腺,創(chuàng)建出來的對象庆冕,都需要在最后做一次release
     GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經(jīng)納入到了ARC中劈榨,所以我們不需要管了
     */
    CFRelease(observer);
}

運行結(jié)果:


結(jié)果
結(jié)果

8. Runloop 退出

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

9. 一些有關(guān)Runloop的問題

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

NSTimer不管用是因為Mode的切換,因為如果我們在主線程使用定時器棒妨,此時RunLoop的Mode為kCFRunLoopDefaultMode踪古,即定時器屬于kCFRunLoopDefaultMode,那么此時我們滑動ScrollView時券腔,RunLoop的Mode會切換到UITrackingRunLoopMode伏穆,因此在主線程的定時器就不在管用了,調(diào)用的方法也就不再執(zhí)行了纷纫,當(dāng)我們停止滑動時枕扫,RunLoop的Mode切換回kCFRunLoopDefaultMode,所有NSTimer就又管用了辱魁。若想定時器繼續(xù)執(zhí)行烟瞧,需要將NSTimer 注冊為 kCFRunLoopCommonModes 。

9.2 延遲執(zhí)行performSelecter相關(guān)方法是怎樣被執(zhí)行的商叹?在子線程中也是一樣的嗎燕刻?

當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當(dāng)前線程的 RunLoop 中剖笙。所以如果當(dāng)前線程沒有 RunLoop卵洗,則這個方法會失效。
當(dāng)調(diào)用 performSelector:onThread: 時弥咪,實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去过蹂,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效聚至。

9.3 事件響應(yīng)和手勢識別底層處理是一致的嗎酷勺,為什么?

事件響應(yīng):
蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件扳躬,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()脆诉。
當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后甚亭,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等)击胜,觸摸亏狰,加速,接近傳感器等幾種 Event偶摔,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程暇唾。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)辰斋。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā)策州,其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊宫仗、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的够挂。

手勢識別:
當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷藕夫。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理下硕。
蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()汁胆,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)霜幼。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時嫩码,這個回調(diào)都會進(jìn)行相應(yīng)處理。

9.4 界面刷新時罪既,是在什么時候會真正執(zhí)行刷新铸题,為什么會刷新不及時?

當(dāng)在操作 UI 時琢感,比如改變了 Frame丢间、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后驹针,這個 UIView/CALayer 就被標(biāo)記為待處理烘挫,并被提交到一個全局的容器去。

蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件柬甥,回調(diào)去執(zhí)行一個很長的函數(shù):_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()饮六。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面苛蒲。所以說界面刷新并不一定是在setNeedsLayout相關(guān)的代碼執(zhí)行后立刻進(jìn)行的卤橄。

9.5 項目程序運行中,總是伴隨著多次自動釋放池的創(chuàng)建和銷毀臂外,這些是在什么時候發(fā)生的呢窟扑?

系統(tǒng)就是通過@autoreleasepool {}這種方式來為我們創(chuàng)建自動釋放池的喇颁,一個線程對應(yīng)一個runloop,系統(tǒng)會為每一個runloop隱式的創(chuàng)建一個自動釋放池嚎货,所有的autoreleasePool構(gòu)成一個棧式結(jié)構(gòu)橘霎,在每個runloop結(jié)束時,當(dāng)前棧頂?shù)腶utoreleasePool會被銷毀厂抖,而且會對其中的每一個對象做一次release(嚴(yán)格來說茎毁,是你對這個對象做了幾次autorelease就會做幾次release,不一定是一次)忱辅,特別指出七蜘,使用容器的block版本的枚舉器的時候,系統(tǒng)會自動添加一個autoreleasePool

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

9.6 當(dāng)我們在子線程上需要執(zhí)行代理方法或者回調(diào)時墙懂,怎么確保當(dāng)前線程沒有被銷毀橡卤?

首先引入一個概念:Event_loop,一般一個線程執(zhí)行完任務(wù)后就會退出损搬,當(dāng)需要保證該線程不退出碧库,可以通過類似以下方式:

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

開啟一個循環(huán),保證線程不退出巧勤,這就是Event_loop模型嵌灰。這是在很多操作系統(tǒng)中都使用的模型,例如OS/iOS中的RunLoop颅悉。這種模型最大的作用就是管理事件/消息沽瞭,在有新消息到來時立刻喚醒處理,沒有待處理消息時線程休眠剩瓶,避免資源浪費驹溃。

10 Runloop 使用

10.1 AFNetworking

使用NSOperation+NSURLConnection并發(fā)模型都會面臨NSURLConnection下載完成前線程退出導(dǎo)致NSOperation對象接收不到回調(diào)的問題。AFNetWorking解決這個問題的方法是按照官方的guid NSURLConnection 上寫的NSURLConnection的delegate方法需要在connection發(fā)起的線程runloop中調(diào)用延曙,于是AFNetWorking直接借鑒了Apple自己的一個Demo的實現(xiàn)方法單獨起一個global thread豌鹤,內(nèi)置一個runloop,所有的connection都由這個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:self
               selector:@selector(networkRequestThreadEntryPoint:)
               object:nil];
          [_networkRequestThread start];
     });

     return _networkRequestThread;
}

類似的可以用這個方法創(chuàng)建一個常駐服務(wù)的線程拐辽。

10.2 TableView中實現(xiàn)平滑滾動延遲加載圖片

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

UIImage *downloadedImage = ...;
[self.imageView performSelector:@selector(setImage:)
     withObject:downloadedImage
     afterDelay:0
     inModes:@[NSDefaultRunLoopMode]];

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

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;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赊舶,隨后出現(xiàn)的幾起案子睁搭,更是在濱河造成了極大的恐慌赶诊,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件园骆,死亡現(xiàn)場離奇詭異舔痪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锌唾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門锄码,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晌涕,你說我怎么就攤上這事滋捶。” “怎么了余黎?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵重窟,是天一觀的道長。 經(jīng)常有香客問我惧财,道長巡扇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任垮衷,我火速辦了婚禮厅翔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搀突。我一直安慰自己知给,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布描姚。 她就那樣靜靜地躺著,像睡著了一般戈次。 火紅的嫁衣襯著肌膚如雪轩勘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天怯邪,我揣著相機(jī)與錄音绊寻,去河邊找鬼。 笑死悬秉,一個胖子當(dāng)著我的面吹牛澄步,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播和泌,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼村缸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了武氓?” 一聲冷哼從身側(cè)響起梯皿,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤仇箱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后东羹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剂桥,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年属提,在試婚紗的時候發(fā)現(xiàn)自己被綠了权逗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡冤议,死狀恐怖斟薇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情求类,我是刑警寧澤奔垦,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站尸疆,受9級特大地震影響椿猎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寿弱,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一犯眠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧症革,春花似錦筐咧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至艇挨,卻和暖如春残炮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缩滨。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工势就, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脉漏。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓苞冯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親侧巨。 傳聞我的和親對象是個殘疾皇子舅锄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 3,983評論 4 30
  • 說明iOS中的RunLoop使用場景1.保持線程的存活,而不是線性的執(zhí)行完任務(wù)就退出了<1>不開啟RunLoop的...
    野生塔塔醬閱讀 6,786評論 15 109
  • Runloop 是和線程緊密相關(guān)的一個基礎(chǔ)組件,是很多線程有關(guān)功能的幕后功臣坪稽。盡管在平常使用中幾乎不太會直接用到,...
    jackyshan閱讀 9,843評論 10 75
  • 概述 RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系峰档,同時也是很多常見技術(shù)的幕后功臣禁添。盡管在平時多...
    陽明先生_x閱讀 1,092評論 0 17
  • 包豪斯撮胧,一間天生反骨的藝術(shù)學(xué)校。 親愛的旅客們你們好老翘,開往(“霍格沃茲”劃掉)“包豪斯”的時光列車馬上就要出發(fā)了芹啥,...
    儒鏡閱讀 1,151評論 0 3