「轉(zhuǎn)載」iOS:RunLoop

一、RunLoop簡介

RunLoop是通過內(nèi)部維護(hù)的事件循環(huán)來對事件/消息進(jìn)行管理的一個對象。

事件循環(huán):
沒有消息處理時串结,休眠以避免資源占用。用戶態(tài)切換到內(nèi)核態(tài)舅列,等待消息肌割。
有消息需要處理時,立刻被喚醒帐要。內(nèi)核態(tài)切換到用戶態(tài)把敞,處理消息。

維護(hù)事件循環(huán)可以用來不斷處理消息和事件榨惠,并對他們進(jìn)行管理奋早。

runloop通過調(diào)用mach_msg()函數(shù)來轉(zhuǎn)移當(dāng)前線程的控制權(quán)給內(nèi)核態(tài)/用戶態(tài)。

基本作用

1赠橙、 保持程序的持續(xù)運行

如果沒有RunLoop耽装,main()函數(shù)一執(zhí)行完,程序就會立刻退出期揪。
而 iOS 程序能保持持續(xù)運行的原因就是在main()函數(shù)中調(diào)用了UIApplicationMain函數(shù)掉奄,這個函數(shù)內(nèi)部會啟動主線程的RunLoop;

2凤薛、處理 App 中的的各種事件(比如觸摸事件姓建、定時器事件等);

3缤苫、節(jié)省 CPU 資源速兔,提高程序性能:該做事時做事,該休息時休息活玲。

RunLoop對象

iOS 中有 2 套 API 來訪問和使用RunLoop:

Foundation:NSRunLoop(是CFRunLoopRef的封裝涣狗,提供了面向?qū)ο蟮?API)
Core Foundation:CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表著RunLoop對象
NSRunLoop不開源,而CFRunLoopRef是開源的:Core Foundation 源碼
獲取RunLoop對象的方式:

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

// Core Foundation
CFRunLoopGetMain();     // 獲取主線程的 RunLoop 對象
CFRunLoopGetCurrent();  // 獲取當(dāng)前線程的 RunLoop 對象

應(yīng)用范疇

定時器(Timer)翼虫、PerformSelector

GCD:dispatch_async(dispatch_get_main_queue(), ^{ });

事件響應(yīng)屑柔、手勢識別、界面刷新

網(wǎng)絡(luò)請求

AutoreleasePool

二珍剑、RunLoop數(shù)據(jù)結(jié)構(gòu)

CFRunLoopRef

RunLoop對象的底層就是一個CFRunLoopRef結(jié)構(gòu)體掸宛,它里面存儲著:

_pthread:RunLoop與線程是一一對應(yīng)關(guān)系

_commonModes:存儲著 NSString 對象的集合(Mode 的名稱)

_commonModeItems:存儲著被標(biāo)記為通用模式的Source0/Source1/Timer/Observer

_currentMode:RunLoop當(dāng)前的運行模式

_modes:存儲著RunLoop所有的 Mode(CFRunLoopModeRef)模式

// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;

// CFRunLoop.c
struct __CFRunLoop {
    .......             
    pthread_t _pthread; //與線程一一對應(yīng)
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ......
};

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的運行模式;

一個RunLoop包含若干個 Mode招拙,每個 Mode 又包含若干個Source0/Source1/Timer/Observer唧瘾;

RunLoop啟動時只能選擇其中一個 Mode措译,作為 currentMode;

如果需要切換 Mode饰序,只能退出當(dāng)前 Loop领虹,再重新選擇一個 Mode 進(jìn)入,切換模式不會導(dǎo)致程序退出求豫;

不同 Mode 中的Source0/Source1/Timer/Observer能分隔開來塌衰,互不影響;

如果 Mode 里沒有任何Source0/Source1/Timer/Observer蝠嘉,RunLoop會立馬退出最疆。

// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
    CFStringRef _name;             // mode 類型,如:NSDefaultRunLoopMode
    CFMutableSetRef _sources0;     // CFRunLoopSourceRef
    CFMutableSetRef _sources1;     // CFRunLoopSourceRef
    CFMutableArrayRef _observers;  // CFRunLoopObserverRef
    CFMutableArrayRef _timers;     // CFRunLoopTimerRef
    ...
};

RunLoop 的常見模式

NSDefaultRunLoopMode / KCFRunLoopDefaultMode:默認(rèn)模式

UITrackingRunLoopMode:界面追蹤模式蚤告,用于 ScrollView 追蹤觸摸滑動努酸,保證界面滑動時不受其他 Mode 影響

NSRunLoopCommonModes / KCFRunLoopCommonModes:通用模式(默認(rèn)包含 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode)

該模式不是實際存在的一種模式,它只是一個特殊的標(biāo)記杜恰,是同步Source0/Source1/Timer/Observer到多個 Mode 中的技術(shù)方案获诈。

被標(biāo)記為通用模式的Source0/Source1/Timer/Observer都會存放到 _commonModeItems 集合中,會同步這些Source0/Source1/Timer/Observer到多個 Mode 中心褐。

注意:

NSDefaultRunLoopMode和NSRunLoopCommonModes屬于Foundation框架舔涎;

KCFRunLoopDefaultMode和KCFRunLoopCommonModes屬于Core Foundation框架;

前者是對后者的封裝檬寂,作用相同终抽。

CFRunLoopModeRef 這樣設(shè)計有什么好處?Runloop為什么會有多個 Mode桶至?

Mode 做到了屏蔽的效果昼伴,當(dāng)RunLoop運行在 Mode1 下面的時候,是處理不了 Mode2 的事件的镣屹,有多個 Mode 使得其在處理不同類型任務(wù)時更加靈活圃郊、高效和可控。

比如NSDefaultRunLoopMode默認(rèn)模式和UITrackingRunLoopMode滾動模式女蜈,滾動屏幕的時候就會切換到滾動模式持舆,就不用去處理默認(rèn)模式下的事件了,保證了 UITableView 等的滾動順暢伪窖。

CFRunLoopSourceRef

在RunLoop中有兩個很重要的概念逸寓,一個是上面提到的模式,還有一個就是事件源覆山。事件源分為輸入源(Input Sources)和定時器源(Timer Sources)兩種竹伸;

輸入源(Input Sources)又分為Source0和Source1兩種,以下__CFRunLoopSource中的共用體union中的version0和version1就分別對應(yīng)Source0和Source1簇宽。

// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;                       /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
};

Source0 和 Source1的區(qū)別

Source0:需要手動喚醒線程:添加Source0到RunLoop并不會主動喚醒線程勋篓,需要手動喚醒)例如: 觸摸事件處理吧享、performSelector:onThread:

Source1:具備喚醒線程的能力,例如基于 Port 的線程間通信和系統(tǒng)事件捕捉譬嚣。

系統(tǒng)事件捕捉是由Source1來處理钢颂,然后再交給Source0處理

CFRunLoopTimerRef

CFRunloopTimer和NSTimer是 toll-free bridged 的,可以相互轉(zhuǎn)換拜银;

performSelector:withObject:afterDelay:方法會創(chuàng)建timer并添加到RunLoop中殊鞭。

// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;           // 添加該 timer 的 RunLoop
    CFMutableSetRef _rlModes;        // 所有包含該 timer 的 modeName
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable 理想時間間隔 */    
    CFTimeInterval _tolerance;       /* mutable 時間偏差 */  
    uint64_t _fireTSR;               /* TSR units */
    CFIndex _order;                  /* immutable */
    CFRunLoopTimerCallBack _callout; /* immutable 回調(diào)入口 */
    CFRunLoopTimerContext _context;  /* immutable, except invalidation */
};

CFRunLoopObserverRef

CFRunLoopObserverRef用來監(jiān)聽RunLoop的 6 種活動狀態(tài)。

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          // 即將進(jìn)入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),   // 即將處理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),  // 即將處理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   // 剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),           // 即將退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  // 表示以上所有狀態(tài)
};

UI 刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)

// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;              // 添加該 observer 的 RunLoop
    CFIndex _rlCount;
    CFOptionFlags _activities;          /* immutable 監(jiān)聽的活動狀態(tài) */
    CFIndex _order;                     /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable 回調(diào)入口 */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

CFRunLoopObserverRef中的_activities用來保存RunLoop的活動狀態(tài)盐股。

當(dāng)RunLoop的狀態(tài)發(fā)生改變時钱豁,通過回調(diào)_callout通知所有監(jiān)聽這個狀態(tài)的Observer。

三疯汁、事件循環(huán)機(jī)制

主線程的 RunLoop 的啟動過程

首先我們來看一下主線程的RunLoop的啟動過程。
上面我們說過卵酪,iOS 程序能保持持續(xù)運行的原因就是在main()函數(shù)中調(diào)用了UIApplicationMain函數(shù)幌蚊,這個函數(shù)內(nèi)部會啟動主線程的RunLoop。
打斷點溃卡,通過 LLDB 指令bt查看函數(shù)調(diào)用棧如下

可以看到溢豆,在UIApplicationMain函數(shù)中調(diào)用了 Core Foundation 框架下的CFRunLoopRunSpecific函數(shù)。

CFRunLoopRunSpecific 函數(shù)實現(xiàn):

RunLoop 的入口瘸羡。
查看源碼中該函數(shù)的實現(xiàn)漩仙,如下:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    ......
    其他邏輯校驗等
    ......
    // 通知 Observers:即將進(jìn)入 RunLoop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // RunLoop 具體要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 通知 Observers:即將退出 RunLoop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

從函數(shù)調(diào)用棧,以及CFRunLoopRunSpecific函數(shù)的實現(xiàn)中可以得知犹赖,RunLoop事件循環(huán)的實現(xiàn)機(jī)制體現(xiàn)在__CFRunLoopRun函數(shù)中队他。

__CFRunLoopRun 函數(shù)實現(xiàn):

事件循環(huán)的實現(xiàn)機(jī)制。

由于該函數(shù)實現(xiàn)較復(fù)雜峻村,以下為刪掉細(xì)節(jié)的精簡版本麸折,想探究具體的可以查看Core Foundation 源碼

/**
 *  __CFRunLoopRun
 *
 *  @param rl              運行的 RunLoop 對象
 *  @param rlm             運行的 mode
 *  @param seconds         loop 超時時間
 *  @param stopAfterHandle true: RunLoop 處理完事件就退出  false:一直運行直到超時或者被手動終止
 *  @param previousMode    上一次運行的 mode
 *
 *  @return 返回 4 種狀態(tài)
 */
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);
        }
        // 判斷有無 Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有 Source1,就跳轉(zhuǎn)到 handle_msg
            goto handle_msg;
        }
        // 通知 Observers:即將進(jìn)入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        // ??休眠粘昨,等待消息來喚醒線程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        __CFRunLoopUnsetSleeping(rl);
        // 通知 Observers:剛從休眠中喚醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

handle_msg:
        if (被 Timer 喚醒) {
            // 處理 Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (被 GCD 喚醒) {
            // 處理 GCD 
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {  // 被 Source1 喚醒   
            // 處理 Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;  
        }
        
        ......

        // 處理 Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        /* 設(shè)置返回值 */
        // 進(jìn)入 loop 時參數(shù)為處理完事件就返回
        if (sourceHandledThisLoop && stopAfterHandle) {  
            retVal = kCFRunLoopRunHandledSource;
        // 超出傳入?yún)?shù)標(biāo)記的超時時間
        } else if (timeout_context->termTSR < mach_absolute_time()) {  
            retVal = kCFRunLoopRunTimedOut;
        // 被外部調(diào)用者強(qiáng)制停止
        } else if (__CFRunLoopIsStopped(rl)) {  
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        // 自動停止
        } else if (rlm->_stopped) {  
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        // mode 中沒有任何的 Source0/Source1/Timer/Observer
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {  
            retVal = kCFRunLoopRunFinished;
        }
    
    } while (0 == retVal);
    
    ......

    return retVal;
}

從該函數(shù)實現(xiàn)中可以得知RunLoop主要就做以下幾件事情:

__CFRunLoopDoObservers:通知Observers接下來要做什么
__CFRunLoopDoBlocks:處理Blocks
__CFRunLoopDoSources0:處理Sources0
__CFRunLoopDoSources1:處理Sources1
__CFRunLoopDoTimers:處理Timers
處理 GCD 相關(guān):dispatch_async(dispatch_get_main_queue(), ^{ });
__CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping:休眠等待/結(jié)束休眠
__CFRunLoopServiceMachPort -> mach-msg():轉(zhuǎn)移當(dāng)前線程的控制權(quán)

__CFRunLoopServiceMachPort 函數(shù)實現(xiàn):

RunLoop 休眠的實現(xiàn)原理垢啼。

在__CFRunLoopRun函數(shù)中,會調(diào)用__CFRunLoopServiceMachPort函數(shù)张肾,該函數(shù)中調(diào)用了mach_msg()函數(shù)來轉(zhuǎn)移當(dāng)前線程的控制權(quán)給內(nèi)核態(tài)/用戶態(tài)芭析。

沒有消息需要處理時,休眠線程以避免資源占用吞瞪。調(diào)用mach_msg()從用戶態(tài)切換到內(nèi)核態(tài)馁启,等待消息;
有消息需要處理時尸饺,立刻喚醒線程进统,調(diào)用mach_msg()回到用戶態(tài)處理消息助币。

這就是RunLoop休眠的實現(xiàn)原理,也是RunLoop與簡單的do...while循環(huán)區(qū)別:

RunLoop:休眠的時候螟碎,當(dāng)前線程不會做任何事眉菱,CPU 不會再分配資源;
簡單的do...while循環(huán):當(dāng)前線程并沒有休息掉分,一直占用 CPU 資源俭缓。

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;
        msg->msgh_local_port = port;
        msg->msgh_remote_port = MACH_PORT_NULL;
        msg->msgh_size = buffer_size;
        msg->msgh_id = 0;
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }

        // ??????
        ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);

        // Take care of all voucher-related work right after mach_msg.
        // If we don't release the previous voucher we're going to leak it.
        voucher_mach_msg_revert(*voucherState);
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        *voucherState = voucher_mach_msg_adopt(msg);
        
        if (voucherCopy) {
            if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
                // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
                // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
                *voucherCopy = voucher_copy();
            } else {
                *voucherCopy = NULL;
            }
        }

        CFRUNLOOP_WAKEUP(ret);
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        if (MACH_RCV_TOO_LARGE != ret) break;
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

四、RunLoop與線程

RunLoop官方文檔以及RunLoop的數(shù)據(jù)結(jié)構(gòu)中可知酥郭,Runloop和線程的關(guān)系:

RunLoop對象和線程是一一對應(yīng)關(guān)系华坦;
RunLoop保存在一個全局的Dictionary里,線程作為key不从,RunLoop作為value惜姐;
如果沒有RunLoop,線程執(zhí)行完任務(wù)就會退出椿息;如果沒有RunLoop歹袁,主線程執(zhí)行完main()函數(shù)就會退出,程序就不能處于運行狀態(tài)寝优;
RunLoop創(chuàng)建時機(jī):線程剛創(chuàng)建時并沒有RunLoop對象条舔,RunLoop會在第一次獲取它時創(chuàng)建;
RunLoop銷毀時機(jī):RunLoop會在線程結(jié)束時銷毀乏矾;
主線程的RunLoop自動獲让峡埂(創(chuàng)建),子線程默認(rèn)沒有開啟RunLoop钻心;
主線程的RunLoop對象是在UIApplicationMain中通過[NSRunLoop currentRunLoop]獲取凄硼,一旦發(fā)現(xiàn)它不存在,就會創(chuàng)建RunLoop對象扔役。

未啟動 RunLoop 的子線程

創(chuàng)建一個NSThread的子類HTThread并重寫了dealloc方法來觀察線程的狀態(tài)帆喇。

執(zhí)行以下代碼,發(fā)現(xiàn)子線程執(zhí)行完一次test任務(wù)就退出銷毀了亿胸,沒有再執(zhí)行test任務(wù)坯钦,原因就是沒有啟動該線程的RunLoop。

@implementation HFThread
- (void)dealloc {
    NSLog(@"%s ",__func__);

}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    
    HFThread *thread = [[HFThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
    [thread start];
    [self performSelector:@selector(threadAction) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)threadAction {
    NSLog(@"test on %@", [NSThread currentThread]);
}

打印輸出:

test on <HFThread: 0x600001a517c0>{number = 7, name = (null)}
[HFThread dealloc]

開啟子線程的 RunLoop 的過程

獲取 RunLoop 對象

上面提到過可以使用了獲取runloop對象的方法:

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

// Core Foundation
CFRunLoopGetMain();     // 獲取主線程的 RunLoop 對象
CFRunLoopGetCurrent();  // 獲取當(dāng)前線程的 RunLoop 對象

再來看一下CFRunLoopGetCurrent()函數(shù)是怎么獲取RunLoop對象的:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());  // 調(diào)用 _CFRunLoopGet0 函數(shù)并傳入當(dāng)前線程
}

_CFRunLoopGet0內(nèi)部實現(xiàn):

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
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);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // ?當(dāng)前線程作為 Key侈玄,從 __CFRunLoops 字典中獲取 RunLoop 
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {  // ?如果字典中不存在
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);  // 創(chuàng)建當(dāng)前線程的 RunLoop
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        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

可以通過以下方式來啟動子線程的RunLoop:

// Foundation
[[NSRunLoop currentRunLoop] run];
[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// Core Foundation
CFRunLoopRun();
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);  
// 第3個參數(shù):設(shè)置為 true婉刀,代表執(zhí)行完 Source/Port 后就會退出當(dāng)前 loop
- (void)threadAction {
    NSLog(@"test on %@", [NSThread currentThread]);
    [[NSRunLoop currentRunLoop] run];
}

打印輸出

test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}
test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}

CFRunLoopRun()/CFRunLoopRunInMode()函數(shù)是怎么啟動RunLoop的:

void CFRunLoopRun(void) {
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {   
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunSpecific在上面描述事件循環(huán)機(jī)制的時候詳細(xì)講解過。

實現(xiàn)一個常駐線程

好處:

可以長期執(zhí)行任務(wù)序仙,適用于需要持續(xù)執(zhí)行的后臺務(wù)突颊,例如網(wǎng)絡(luò)請求,定時任務(wù)等。

減少資源消耗律秃,常駐線程不需要頻繁的創(chuàng)建銷毀線程爬橡,可以減少資源消耗和系統(tǒng)開銷。

提高響應(yīng)速度棒动,常駐線程中的光任務(wù)可以減少線程切換的開銷糙申,提高響應(yīng)速度,特別適合需要頻繁執(zhí)行任務(wù)的場景船惨。

設(shè)計條件:該任務(wù)需是串行的柜裸,而非并發(fā);

設(shè)計步驟:

1粱锐、獲取/創(chuàng)建當(dāng)前線程的RunLoop疙挺;
2、向該RunLoop中添加一個Source/Port等來維持RunLoop的事件循環(huán)(如果 Mode 里沒有任何Source0/Source1/Timer/Observer怜浅,RunLoop會立馬退出)铐然;
3、啟動該RunLoop海雪。

#import "HFNextViewViewController.h"
#import "HFThread.h"

@interface HFNextViewViewController ()
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@property (nonatomic, strong) HFThread *thread;
@end

@implementation HFNextViewViewController
- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    if (!self.thread) return;
    // 在子線程調(diào)用(waitUntilDone設(shè)置為YES锦爵,代表子線程的代碼執(zhí)行完畢后,當(dāng)前方法才會繼續(xù)往下執(zhí)行)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    [self hf_addButton];
    __weak typeof(self) weakSelf = self;
        
    self.stopped = NO;
    self.thread = [[HFThread alloc] initWithBlock:^{
        NSLog(@"begin-----%@", [NSThread currentThread]);
        
        // ① 獲取/創(chuàng)建當(dāng)前線程的 RunLoop
        // ② 向該 RunLoop 中添加一個 Source/Port 等來維持 RunLoop 的事件循環(huán)
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while (weakSelf && !weakSelf.isStoped) {
            // ③ 啟動該 RunLoop
            /*
              [[NSRunLoop currentRunLoop] run]
              如果調(diào)用 RunLoop 的 run 方法奥裸,則會開啟一個永不銷毀的線程
              因為 run 方法會通過反復(fù)調(diào)用 runMode:beforeDate: 方法,以運行在 NSDefaultRunLoopMode 模式下
              換句話說沪袭,該方法有效地開啟了一個無限的循環(huán)湾宙,處理來自 RunLoop 的輸入源 Sources 和 Timers 的數(shù)據(jù)
            */
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"end-----%@", [NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (!self.thread) return;
    [self performSelector:@selector(threadAction) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)threadAction {
    NSLog(@"%s- - - - %@",__func__, [NSThread currentThread]);
}

// 停止子線程的 RunLoop
- (void)stopThread
{
    // 設(shè)置標(biāo)記為 YES
    self.stopped = YES;
    // 停止 RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
    // 清空線程
    self.thread = nil;
}


- (void)hf_addButton {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(50, 140, 200, 200);
    button.backgroundColor = [UIColor redColor];
    [self.view addSubview:button];
    [button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
    

    
}
- (void)buttonAction:(UIButton *)button
{
    NSLog(@"back= %@",button);
    [self dismissViewControllerAnimated:YES completion:nil];
    
}
@end
#import "HFThread.h"

@implementation HFThread
- (void)dealloc {
    NSLog(@"%s ",__func__);

}
@end

多次點擊HFNextViewViewController的view,然后退出此頁面冈绊,輸出日志

begin-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController dealloc]
-[HFNextViewViewController stopThread]-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
end-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFThread dealloc]

五侠鳄、RunLoop與NSTimer

NSTimer是由RunLoop來管理的,NSTimer其實就是CFRunLoopTimerRef死宣,他們之間是 toll-free bridged 的伟恶,可以相互轉(zhuǎn)換;
如果在子線程上使用NSTimer毅该,就必須開啟子線程的RunLoop博秫,否則定時器無法生效。

解決 tableview 滑動時 NSTimer 失效的問題

問題:

由上面的分析我們知道眶掌,RunLoop同一時間只能運行在一種模式下挡育,當(dāng)我們滑動tableview/scrollview的時候RunLoop會切換到UITrackingRunLoopMode界面追蹤模式下。如果我們的NSTimer是添加到RunLoop的KCFRunLoopDefaultMode/NSDefaultRunLoopMode默認(rèn)模式下的話朴爬,此時是會失效的即寒。

解決方案:

可以將NSTimer添加到RunLoop的KCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,來保證無論在默認(rèn)模式還是界面追蹤模式下NSTimer都可以執(zhí)行。

NSTimer的創(chuàng)建方式

 // 方式一母赵、這種方式創(chuàng)建的NSTimer是自動添加到RunLoop模式下的
 [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
      NSLog(@"123");
  }];
  
  
  // 方式二逸爵、通過timerxxx開頭方法創(chuàng)建的NSTimer是不會自動添加到RunLoop中的,
  // 所以一定要記得手動添加凹嘲,否則NSTimer不生效师倔。
  NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
      NSLog(@"123");
  }];
  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

CFRunLoopAddTimer 函數(shù)實現(xiàn)

CFRunLoopAddTimer()函數(shù)中會判斷傳入的modeName模式名稱是不是kCFRunLoopCommonModes通用模式。

是的話就會將timer添加到RunLoop的 _commonModeItems 集合中施绎,并同步該timer到 _commonModes 里的所有模式中溯革,

這樣無論在默認(rèn)模式還是界面追蹤模式下NSTimer都可以執(zhí)行。

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
    __CFRunLoopLock(rl);
    if (modeName == kCFRunLoopCommonModes) {       // 判斷 modeName 是不是 kCFRunLoopCommonModes
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {    // 懶加載谷醉,判斷 _commonModeItems 是否為空致稀,是的話創(chuàng)建
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        CFSetAddValue(rl->_commonModeItems, rlt);  // 將 timer 添加到 _commonModeItems 中
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};  // 將 timer 和 RunLoop 封裝到 context 中
            /* add new item to all common-modes */
            // 遍歷 commonModes,將 timer 添加到 commonModes 的所有模式下
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    ......
    }
}


static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
    CFStringRef modeName = (CFStringRef)value;
    CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
    CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
    if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
    CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
    } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
    CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
    } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
    CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
    }
}

NSTimer 和 CADisplayLink 存在的問題

問題:

不準(zhǔn)時:NSTime和CADisplayLink底層都是基于RunLoop的CFRunLoopTimerRef的實現(xiàn)的俱尼,也就是說它們都依賴于RunLoop抖单。如果RunLoop的任務(wù)過于繁重,會導(dǎo)致它們不準(zhǔn)時遇八。

比如NSTimer每1.0秒就會執(zhí)行一次任務(wù)矛绘,Runloop每進(jìn)行一次循環(huán),就會看一下NSTimer的時間是否達(dá)到1.0秒刃永,是的話就執(zhí)行任務(wù)货矮。但是由于Runloop每一次循環(huán)的任務(wù)不一樣,所花費的時間就不固定斯够。假設(shè)第一次循環(huán)所花時間為 0.2s囚玫,第二次 0.3s,第三次 0.3s读规,則再過 0.2s 就會執(zhí)行NSTimer的任務(wù)抓督,這時候可能Runloop的任務(wù)過于繁重,第四次花了0.5s束亏,那加起來時間就是 1.3s铃在,導(dǎo)致NSTimer不準(zhǔn)時。

解決方案:

使用 GCD 的定時器碍遍。GCD 的定時器是直接跟系統(tǒng)內(nèi)核掛鉤的定铜,而且它不依賴于RunLoop,所以它非常的準(zhǔn)時雀久。

轉(zhuǎn)載參考:
深入淺出RoonLoop

相關(guān)鏈接:
Core Foundation 源碼
RunLoop官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宿稀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赖捌,更是在濱河造成了極大的恐慌祝沸,老刑警劉巖矮烹,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異罩锐,居然都是意外死亡奉狈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門涩惑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仁期,“玉大人,你說我怎么就攤上這事竭恬□说埃” “怎么了?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵痊硕,是天一觀的道長赊级。 經(jīng)常有香客問我,道長岔绸,這世上最難降的妖魔是什么理逊? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮盒揉,結(jié)果婚禮上晋被,老公的妹妹穿的比我還像新娘。我一直安慰自己刚盈,他們只是感情好羡洛,可當(dāng)我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著藕漱,像睡著了一般翘县。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谴分,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機(jī)與錄音镀脂,去河邊找鬼牺蹄。 笑死,一個胖子當(dāng)著我的面吹牛薄翅,可吹牛的內(nèi)容都是我干的沙兰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼翘魄,長吁一口氣:“原來是場噩夢啊……” “哼鼎天!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起暑竟,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤斋射,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罗岖,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡涧至,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了桑包。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片南蓬。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哑了,靈堂內(nèi)的尸體忽然破棺而出赘方,到底是詐尸還是另有隱情,我是刑警寧澤弱左,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布窄陡,位于F島的核電站,受9級特大地震影響科贬,放射性物質(zhì)發(fā)生泄漏泳梆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一榜掌、第九天 我趴在偏房一處隱蔽的房頂上張望优妙。 院中可真熱鬧,春花似錦憎账、人聲如沸套硼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邪意。三九已至,卻和暖如春反砌,著一層夾襖步出監(jiān)牢的瞬間雾鬼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工宴树, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留策菜,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓酒贬,卻偏偏與公主長得像又憨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锭吨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,492評論 2 348

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