【iOS 底層原理】Runloop

一. RunLoop簡介

運行循環(huán)禁舷,在程序運行過程中循環(huán)做一些事情权逗,如果沒有Runloop程序執(zhí)行完畢就會立即退出举塔,如果有Runloop程序會一直運行,并且時時刻刻在等待用戶的輸入操作奉狈。RunLoop可以在需要的時候自己跑起來運行,在沒有操作的時候就停下來休息涩惑。充分節(jié)省CPU資源仁期,提高程序性能图焰。

應用場景

  • 定時器(Timer)------- timer
  • PerformSelector ---------- source0
  • GCD Async Main Queue
    • 只有 dispatch_async(dispatch_get_main_queue) 情況下是通過 runloop 去調(diào)度的
  • 事件響應收捣、手勢識別皮假、界面刷新
  • 網(wǎng)絡請求
  • AutoreleasePool

二. RunLoop基本作用

  • 保持程序持續(xù)運行藻雌,程序一啟動就會開一個主線程销睁,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷毀咆槽,也就保證了程序的持續(xù)運行
  • 處理App中的各種事件(比如:觸摸事件来吩,定時器事件俐银,Selector事件等)
  • 節(jié)省CPU資源岔绸,提高程序性能理逊,程序運行起來時,當什么操作都沒有做的時候盒揉,RunLoop就告訴CPU晋被,現(xiàn)在沒有事情做,我要去休息刚盈,這時CPU就會將其資源釋放出來去做其他的事情羡洛,當有事情做的時候RunLoop就會立馬起來去做事情

我們先通過API內(nèi)一張圖片來簡單看一下RunLoop內(nèi)部運行原理

image.png

通過圖片可以看出,RunLoop在跑圈過程中藕漱,當接收到Input sources 或者 Timer sources時就會交給對應的處理方去處理欲侮。當沒有事件消息傳入的時候崭闲,RunLoop就休息了

三. 主線程的 RunLoop

UIApplicationMain函數(shù)內(nèi)啟動了Runloop,程序不會馬上退出锈麸,而是保持運行狀態(tài)镀脂。因此每一個應用必須要有一個runloop,
我們知道主線程一開起來忘伞,就會跑一個和主線程對應的RunLoop薄翅,那么RunLoop一定是在程序的入口main函數(shù)中開啟。

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

在UIApplicationMain函數(shù)中氓奈,開啟了一個和主線程相關的RunLoop翘魄,導致UIApplicationMain不會返回,一直在運行中舀奶,也就保證了程序的持續(xù)運行暑竟。

Runloop 源碼:

// 用DefaultMode啟動
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的值實現(xiàn)的。因此育勺,我們可以把RunLoop看成一個死循環(huán)但荤。

四. RunLoop對象

  • Fundation框架 (基于CFRunLoopRef的封裝) NSRunLoop對象
  • CoreFoundation CFRunLoopRef對象

獲取 Runloop 對象

Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象

Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

CFRunLoopRef 開源代碼:https://opensource.apple.com/tarballs/CF/

五. RunLoop和線程間的關系

  • 每條線程都有唯一的一個與之對應的RunLoop對象
  • RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了涧至,子線程的RunLoop需要主動創(chuàng)建
  • RunLoop在第一次獲取時創(chuàng)建腹躁,在線程結束時銷毀

相關源碼

// 拿到當前Runloop 調(diào)用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法內(nèi)部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 根據(jù)傳入的主線程獲取主線程對應的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 保存主線程 將主線程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 從字典里面拿,將線程作為key從字典里獲取一個loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop為空南蓬,則創(chuàng)建一個新的loop纺非,所以runloop會在第一次獲取的時候創(chuàng)建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 創(chuàng)建好之后,以線程為key runloop為value赘方,一對一存儲在字典中烧颖,下次獲取的時候,則直接返回字典內(nèi)的runloop
    if (!loop) { 
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

從上面的代碼可以看出窄陡,線程和 RunLoop 之間是一一對應的炕淮,其關系是保存在一個 Dictionary 里。所以我們創(chuàng)建子線程RunLoop時跳夭,只需在子線程中獲取當前線程的RunLoop對象即可[NSRunLoop currentRunLoop];如果不獲取鳖悠,那子線程就不會創(chuàng)建與之相關聯(lián)的RunLoop,并且只能在一個線程的內(nèi)部獲取其 RunLoop
[NSRunLoop currentRunLoop];方法調(diào)用時优妙,會先看一下字典里有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop憎账,如果沒有則會創(chuàng)建一個套硼,并將與之對應的子線程存入字典中。當線程結束時胞皱,RunLoop會被銷毀邪意。

從源碼看出子線程只有第一次獲取 runloop 時才會創(chuàng)建

image.png

六. RunLoop結構體

image.png

__CFRunLoop結構體 源碼

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

主要成員變量

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

CFRunLoopModeRef 其實是指向__CFRunLoopMode結構體的指針九妈,__CFRunLoopMode結構體源碼如下

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

主要成員變量

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

通過上面分析我們知道,CFRunLoopModeRef代表RunLoop的運行模式雾鬼,一個RunLoop包含若干個Mode萌朱,每個Mode又包含若干個Source0/Source1/Timer/Observer,而RunLoop啟動時只能選擇其中一個Mode作為currentMode策菜。

Source1/Source0/Timers/Observer分別代表什么

  1. Source1 : 基于Port的線程間通信
  2. Source0 : 觸摸事件晶疼,PerformSelectors
  3. Timers : 定時器,NSTimer
  4. Observer : 監(jiān)聽器又憨,用于監(jiān)聽RunLoop的狀態(tài)
觸摸事件屬于 source1 還是 source0
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"點擊了屏幕");
}

打斷點之后打印堆棧信息翠霍,當xcode工具區(qū)打印的堆棧信息不全時,可以在控制臺通過“bt”指令打印完整的堆棧信息蠢莺,由堆棧信息中可以發(fā)現(xiàn)寒匙,觸摸事件確實是會觸發(fā)Source0事件。


image.png
performSelector 屬于 source1 還是 source0
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});

可以發(fā)現(xiàn)PerformSelectors同樣是觸發(fā)Source0事件


image.png

當我們觸發(fā)了事件(觸摸/鎖屏/搖晃等)后躏将,由IOKit.framework生成一個 IOHIDEvent事件锄弱,而IOKit是蘋果的硬件驅動框架,由它進行底層接口的抽象封裝與系統(tǒng)進行交互傳遞硬件感應的事件祸憋,并專門處理用戶交互設備会宪,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的夺衍,它會將事件封裝成IOHIDEvents對象狈谊,接著用mach port轉發(fā)給需要的App進程,隨后 Source1就會接收IOHIDEvent沟沙,之后再回調(diào)__IOHIDEventSystemClientQueueCallback()河劝,__IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)Source0,Source0 再觸發(fā) _UIApplicationHandleEventQueue()矛紫。所以觸摸事件看到是在 Source0 內(nèi)的赎瞎。

總結:觸摸事件先通過 mach port 發(fā)送,封裝為 source1颊咬,之后又轉換為 source0

Timers 驗證
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
    NSLog(@"NSTimer ---- timer調(diào)用了");
}];
image.png
Observer
image.png

七. 詳解RunLoop相關類及作用

相關類

  • CFRunLoopRef - 獲得當前RunLoop和主RunLoop
  • CFRunLoopModeRef - RunLoop 運行模式务甥,只能選擇一種,在不同模式中做不同的操作
  • CFRunLoopSourceRef - 事件源喳篇,輸入源
  • CFRunLoopTimerRef - 定時器時間
  • CFRunLoopObserverRef - 觀察者

1. CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的運行模式
一個 RunLoop 包含若干個 Mode敞临,每個Mode又包含若干個Source、Timer麸澜、Observer
每次RunLoop啟動時挺尿,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
如果需要切換Mode,只能退出當前一次 Loop编矾,再重新指定一個Mode進入熟史,這樣做主要是為了分隔開不同組的Source、Timer窄俏、Observer蹂匹,讓其互不影響。如果Mode里沒有任何Source0/Source1/Timer/Observer凹蜈,RunLoop會立馬退出

image.png

注意:一種Mode中可以有多個Source(事件源限寞,輸入源,基于端口事件源例鍵盤觸摸等) Observer(觀察者踪区,觀察當前RunLoop運行狀態(tài)) 和Timer(定時器事件源)昆烁。但是必須至少有一個Source或者Timer,因為如果Mode為空缎岗,RunLoop運行到空模式不會進行空轉静尼,就會立刻退出。

系統(tǒng)默認注冊的5個Mode:

RunLoop 有五種運行模式传泊,其中常見的有1.2兩種

1. kCFRunLoopDefaultMode:App的默認Mode鼠渺,通常主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動眷细,保證界面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode拦盹,啟動完成后就不再使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode溪椎,通常用不到
5. kCFRunLoopCommonModes: 這是一個占位用的Mode普舆,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode 

2. CFRunLoopSourceRef事件源(輸入源)

source 分為兩種

  • Source0:非基于Port的 用于用戶主動觸發(fā)的事件(點擊button 或點擊屏幕)
  • Source1:基于Port的 通過內(nèi)核和其他線程相互發(fā)送消息(與內(nèi)核相關)

3. 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()默認分配
     第二個參數(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),   //   即將進入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添加監(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);
}

八. RunLoop處理邏輯

image.png
image.png

源碼解析

// 共外部調(diào)用的公開的CFRunLoopRun方法谎碍,其內(nèi)部會調(diào)用CFRunLoopRunSpecific
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 經(jīng)過精簡的 CFRunLoopRunSpecific 函數(shù)代碼,其內(nèi)部會調(diào)用__CFRunLoopRun函數(shù)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    // 通知Observers : 進入Loop
    // __CFRunLoopDoObservers內(nèi)部會調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函數(shù)
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 核心的Loop邏輯
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知Observers : 退出Loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

// 精簡后的 __CFRunLoopRun函數(shù)洞焙,保留了主要代碼
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知Observers:即將處理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
        
        // 通知Observers:即將處理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        // 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 處理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        // 如果有Sources1蟆淀,就跳轉到handle_msg標記處
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
        
        // 通知Observers:即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        
        // 進入休眠太援,等待其他消息喚醒
        __CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
        do {
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        
        // 醒來
        __CFPortSetRemove(dispatchPort, waitSet);
        __CFRunLoopUnsetSleeping(rl);
        
        // 通知Observers:已經(jīng)喚醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg: // 看看是誰喚醒了RunLoop,進行相應的處理
        if (被Timer喚醒的) {
            // 處理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        else if (被GCD喚醒的) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Sources1喚醒的
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }
        
        // 執(zhí)行Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 根據(jù)之前的執(zhí)行結果扳碍,來決定怎么做,為retVal賦相應的值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
    } while (0 == retVal);
    
    return retVal;
}


image.png

runloop 休眠實現(xiàn)原理

image.png

runloop 休眠時仙蛉,不占用 CPU笋敞,當有消息時,CPU 從內(nèi)核態(tài)發(fā)送消息荠瘪,喚醒線程夯巷。

九. RunLoop退出

主線程銷毀RunLoop退出
Mode中有一些Timer 、Source哀墓、 Observer趁餐,這些保證Mode不為空時保證RunLoop沒有空轉并且是在運行的,當Mode中為空的時候篮绰,RunLoop會立刻退出
我們在啟動RunLoop的時候可以設置什么時候停止

[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>

十. RunLoop API

1. runloop 啟動

NSRunLoop 有三種啟動方式:

  • Unconditionally(run)
  • With a set time limit(runUntilDate)
  • In a particular mode(runMode:beforeDate:)

runUntilDate:

  • 這個方法,會循環(huán)調(diào)用 runMode:beforeDate: 直到達到參數(shù) NSDate 所指定的時間吠各,也就是超時時間臀突。

run

  • 這個方法,可以看做是 [runloop runUntilDate:[NSDate distantFuture]];
  • 相當于 while 死循環(huán)贾漏,一直調(diào)用 runMode:beforeDate:

runMode:beforeDate:

  • 這個方法是啟動一次 RunLoop候学,與前面兩個 API 大不相同,一次 loop 過后就退出 runloop

demo

- (void)setup{
    NSLog(@"%@",[[NSRunLoop currentRunLoop] currentMode]);
    //創(chuàng)建并啟動一個 thread
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    [self.thread setName:@"Test Thread"];
    [self.thread start];

    //向 RunLoop 發(fā)送消息的簡便方法纵散,系統(tǒng)會將消息傳遞到指定的 SEL 里面
    [self performSelector:@selector(receiveMsg) onThread:self.thread withObject:nil waitUntilDone:NO];
        
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //直接去的子線程 RunLoop 的一個 port梳码,并向其發(fā)送消息,這個是比較底層的 NSPort 方式進行線程通信
        [self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];
    });
}

- (void)threadTest{
    NSLog(@"%@",@"child thread start");
    //threadTest 這個方法是在 Test Thread 這個線程里面運行的伍掀。
    NSLog(@"%@",[NSThread currentThread]);
    //獲取這個線程的 RunLoop 并讓他運行起來
    NSRunLoop* runloop = [NSRunLoop currentRunLoop];
    self.port = [[NSMachPort alloc]init];
    self.port.delegate = self;
    [runloop addPort:self.port forMode:NSRunLoopCommonModes];
    //約等于runUntilDate:[NSDate distantFuture]
    [runloop run];
}

- (void)receiveMsg{
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"%@",@"receive msg");
}

#pragma NSMachPortDelegate
- (void)handleMachMessage:(void *)msg{
    NSLog(@"handle message thread:%@",[NSThread currentThread]);
}

2.runloop 退出

  • 使用 run 啟動 RunLoop掰茶,直到程序結束再退出。主線程的 RunLoop 一定是這種方式啟動的硕盹。
  • 使用 runUntileDate 啟動 RunLoop符匾,date 時間到退出。這里的 date 會當做參數(shù)傳到下面這個方法中瘩例,當 date 時間到啊胶,RunLoop 也會因為超時而結束一次循環(huán)。
  • 使用 runMode:beforeDate: 這種方式啟動一次消息循環(huán)垛贤,并且自己編寫代碼來控制一次消息循環(huán)結束后是否啟動下一次消息循環(huán)焰坪。也就是類似這樣的代碼:
BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

3.線程保活封裝

使用 run 方法開啟 runloop 會使 runloop 一直存活聘惦,直到應用的生命周期結束某饰,這樣的線程無法銷毀。如果希望不使用時銷毀線程,需要調(diào)用 runMode:beforeDate 這個 API 結合 while 去實現(xiàn)黔漂〗刖。或者使用 C 語言的 API

方式一:

@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.stopped = NO;
        
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(MJPermenantThreadTask)task
{
    if (!self.innerThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

@end

方式二:
基于 CFRunLoop 的 API 不需要使用 while 循環(huán)實現(xiàn)永久 loop

@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            NSLog(@"begin----");
            
            // 創(chuàng)建上下文(要初始化一下結構體)
            CFRunLoopSourceContext context = {0};
            
            // 創(chuàng)建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            // 往Runloop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            // 銷毀source
            CFRelease(source);
            
            // 啟動,第三個參數(shù) returnAfterSourceHandled 為 false 表示永久 loop炬守,為 true 表示單次 loop
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
            
            NSLog(@"end----");
        }];
        
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(MJPermenantThreadTask)task
{
    if (!self.innerThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

十一. RunLoop應用

1. 常駐線程

子線程執(zhí)行完操作之后就會立即釋放牧嫉,即使我們使用強引用引用子線程使子線程不被釋放,也不能給子線程再次添加操作减途,或者再次開啟酣藻。我們可以通過在線程中添加 runloop 的方式讓線程保活

demo

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
   // 創(chuàng)建子線程并開啟
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
    self.thread = thread;
    [thread start];
}
-(void)show
{
    // 注意:打印方法一定要在RunLoop創(chuàng)建開始運行之前鳍置,如果在RunLoop跑起來之后打印辽剧,RunLoop先運行起來,已經(jīng)在跑圈了就出不來了税产,進入死循環(huán)也就無法執(zhí)行后面的操作了怕轿。
    // 但是此時點擊Button還是有操作的,因為Button是在RunLoop跑起來之后加入到子線程的砖第,當Button加入到子線程RunLoop就會跑起來
    NSLog(@"%s",__func__);
    // 1.創(chuàng)建子線程相關的RunLoop撤卢,在子線程中創(chuàng)建即可,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出梧兼,因此在創(chuàng)建的時候直接加入
    // 添加Source [NSMachPort port] 添加一個端口
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    // 添加一個Timer
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];    
    //創(chuàng)建監(jiān)聽者
    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添加監(jiān)聽者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 2.子線程需要開啟RunLoop
    [[NSRunLoop currentRunLoop]run];
    CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
    NSLog(@"%@",[NSThread currentThread]);
}
@end

注意:創(chuàng)建子線程相關的RunLoop放吩,在子線程中創(chuàng)建即可,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出羽杰,因此在創(chuàng)建的時候直接加入渡紫,如果沒有加入Timer或者Source,或者只加入一個監(jiān)聽者考赛,運行程序會崩潰

2. 自動釋放池

RunLoop內(nèi)部有一個自動釋放池惕澎,當RunLoop開啟時,就會自動創(chuàng)建一個自動釋放池颜骤,當RunLoop在休息之前會釋放掉自動釋放池的東西唧喉,然后重新創(chuàng)建一個新的空的自動釋放池,當RunLoop被喚醒重新開始跑圈時忍抽,Timer,Source等新的事件就會放到新的自動釋放池中八孝,當RunLoop退出的時候也會被釋放。

注意:只有主線程的RunLoop會默認啟動鸠项。也就意味著會自動創(chuàng)建自動釋放池干跛,子線程需要在線程調(diào)度方法中手動添加自動釋放池。

@autorelease{  
      // 執(zhí)行代碼 
}

3.解決NSTimer在滑動時停止工作的問題

4.監(jiān)控應用卡頓

5.性能優(yōu)化

十二.面試題

1.runloop 和 GCD 的關系

1.RunLoop 的超時時間
RunLoop 中大量使用到了GCD祟绊,啟動 RunLoop 的超時時間就是使用 GCD 中的 dispatch_source_t來實現(xiàn)的

2.runloop 去執(zhí)行 GCD MainQueue 上的異步任務
dispatch_async(dispatch_get_main_queue(), block)產(chǎn)生的任務需要通過 runloop 去調(diào)度

所以 runloop 和 GCD 是相互調(diào)用依賴的關系

2.runloop 與 autorelease pool 的關系

第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)楼入,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池哥捕。其 order 是-2147483647,優(yōu)先級最高嘉熊,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前遥赚。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入睡眠) 和 Exit(即將退出Loop),
BeforeWaiting(準備進入睡眠)時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池阐肤;
Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池鸽捻。這個 Observer 的 order 是 2147483647,優(yōu)先級最低泽腮,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衣赶,一起剝皮案震驚了整個濱河市诊赊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌府瞄,老刑警劉巖碧磅,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異遵馆,居然都是意外死亡鲸郊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門货邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秆撮,“玉大人,你說我怎么就攤上這事换况≈氨妫” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵戈二,是天一觀的道長舒裤。 經(jīng)常有香客問我,道長觉吭,這世上最難降的妖魔是什么腾供? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮鲜滩,結果婚禮上伴鳖,老公的妹妹穿的比我還像新娘。我一直安慰自己绒北,他們只是感情好黎侈,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著闷游,像睡著了一般峻汉。 火紅的嫁衣襯著肌膚如雪贴汪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天休吠,我揣著相機與錄音扳埂,去河邊找鬼。 笑死瘤礁,一個胖子當著我的面吹牛阳懂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柜思,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼岩调,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赡盘?” 一聲冷哼從身側響起号枕,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陨享,沒想到半個月后葱淳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡抛姑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年赞厕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定硝。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡皿桑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔬啡,到底是詐尸還是另有隱情唁毒,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布星爪,位于F島的核電站浆西,受9級特大地震影響,放射性物質發(fā)生泄漏顽腾。R本人自食惡果不足惜近零,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抄肖。 院中可真熱鬧久信,春花似錦、人聲如沸漓摩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽管毙。三九已至腿椎,卻和暖如春桌硫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啃炸。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工铆隘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人南用。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓膀钠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親裹虫。 傳聞我的和親對象是個殘疾皇子肿嘲,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359