一文看完 Runloop

Runloop 是和線程緊密相關的一個基礎組件,是不少線程有關功能的幕後功臣。 本文將從如下幾個方面來總結runloop:html

  • 什麼是runloop
  • runloop的做用
  • runloop和線程的關係
  • runloop詳細介紹及源碼分析
  • runloop原理分析
  • runloop應用

什麼是runloop

runloop 蘋果官方文檔地址數組

  • Runloop 仍是比較顧名思義的一個東西,說白了就是一種循環,只不過它這種循環比較高級。通常的do..while 循環會致使 CPU 進入忙等待狀態,而 Runloop 則是一種「閒」等待。

runlooprun方法源碼以下所示,是一個do..while循環服務器

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 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠。
  • runloop其實是一個對象,這個對象提供了一個入口函數。

runloop的做用

  • 保持程序的持續運行,循環避免線程銷燬
  • 處理APP的各類事件(觸摸、定時器、performSelector)
  • 節省cpu資源、提供程序的性能(該作事就作事,該休息就休息)

runloop在系統裏的使用

在iOS系統裏,下面的這些都有使用runloop,經過斷點查看堆棧能夠看到調用的方法名:網絡

  • block應用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK多線程

  • 調用timer: CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTIONapp

  • 響應source0: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION框架

  • 響應source1: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTIONsocket

  • GCD主隊列: CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUEasync

  • observer源: CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函數

斷點查看runloop信息

timerblock裏添加斷點,而後左邊箭頭指示的按鈕不選中(默認是選中的),能夠看到runloop的調用信息 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 源碼以下:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
    if (func) {
        func(timer, info);
    }
    getpid(); // thwart tail-call optimization
}
複製代碼

關於上面總結的其餘幾種調用的runloop方法名,均可以用上面的這種調試方式查看一下。

runloop和線程的關係

  1. runloop和線程是一一對應的
  2. runloop在首次被線程獲取時建立,在線程結束時被銷燬
  3. 主線程默認啓動runloop,子線程手動啓動(程序啓動時,啓動主線程runloop,[[NSRunLoop currentRunLoop] run])

圖中展示了 Runloop 在線程中的做用:從 input sourcetimer source 接受事件,而後在線程中處理事件。

獲取runloop

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}
複製代碼

源碼裏調用了_CFRunLoopGet0(),這裏是傳一個主線程pthread_main_thread_np()進去,以下定義了它是主線程

#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR
CF_EXPORT pthread_t _CF_pthread_main_thread_np(void);
#define pthread_main_thread_np() _CF_pthread_main_thread_np()
複製代碼

還有一個獲取當前線程runloop的方法:一樣是調用了_CFRunLoopGet0,只不過傳進去的是當前線程pthread_self()

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
複製代碼

接下來看獲取線程runloop的函數_CFRunLoopGet0(包括主線程和子線程)的源碼

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    //根據線程獲取runloop
	t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //若是存儲RunLoop的字典不存在
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
	CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
	 //建立主線程的RunLoop
	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);
        __CFSpinLock(&loopsLock);
    }
    //字典裏找runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
        __CFSpinUnlock(&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);
        }
    }
複製代碼
  • 若是當前存儲的字典容器不存在,首先就建立了一個容器CFMutableDictionaryRef可變字典

  • 第二步使用主線程建立了一個主線程runloopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

  • 第三步CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);把主線程和它的runloop用key-value形式保存在這個CFMutableDictionaryRef字典容器裏

  • 以上說明,第一次進來的時候,不論是getMainRunloop仍是get子線程的runloop,主線程的runloop老是會被建立

  • 再看到CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));,能夠用線程把保存在字典裏的runloop取出來

  • 若是字典裏沒有找到runloop,就根據當前的子線程建立一個新的runloop對象並保存到字典裏

  • 最後一步if (pthread_equal(t, pthread_self())) {...}判斷當前的線程是否是傳遞進來的線程,若是是則建立一個回調,若是線程銷燬,就銷燬當前的runloop

  • 這裏驗證了上面的結論1和2: runloop和線程是一一對應的(字典保存)。 runloop在首次被線程獲取時建立(而且: 無論獲取的是主線程runloop仍是子線程runloop,老是會建立主線程的runloop),在線程結束時被銷燬(經過回調銷燬)

runloop代碼驗證

AppDelegate打斷點,能夠看到主線程是有調用__CFRunloopRun方法的,因此證實了上面的結論三: 主線程是默認開啓runloop ![]user-gold-cdn.xitu.io/2019/10/16/…) 測試runloop代碼以下

- (vod)viewDidLoad {
    super viewDidLoad];
DLThread *thread = [[DLThread alloc]initWithBlock:^{
       NSLog(@"%@",[NSThread currentThread]);
       [NSTimer scheduledTimerWithTimeInterval:1repeats:YES block:^(NSTimer * _Nonnul) {
            NSLog(@"timer");
        }];
        
    }];
    thread.name = @"Test";
    [thread start];
複製代碼

DLThread.m裏只寫了以下代碼

-(void)dealloc{
    NSLog(@"線程銷燬了");
}
複製代碼

運行上面的代碼,發現timer並無打印,說明子線程裏開啓timer沒成功,而後添加了代碼運行當前線程的runloop,以下所示:

DLThread *thread = [[DLThread alloc] initWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer");
        }];
        [[NSRunLoop currentRunLoop] run];
    }];
    thread.name = @"Test";
    [thread start];
複製代碼

發現timer一直在打印了,這裏證實了兩個結論: timer的運行是和runloop有關的,子線程的runloop是須要手動開啓的

那麼如何中止timer呢?新增了一個標記值isStopping用來退出線程

DLThread *thread = [[DLThread alloc] initWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer");
            if(self.isStopping){
                [NSThread exit];
            }
        }];
        [[NSRunLoop currentRunLoop] run];
    }];
    thread.name = @"Test";
    [thread start];
複製代碼

運行發現,在線程銷燬後,timer也中止了,這裏側面證實了上面的結論二: runloop是在線程結束時銷燬的

runloop源碼分析

點擊下載runloop源碼:密碼 3kww

在runloop源碼裏須要探索的:

  • CFRunLoop
  • CFRunLoopMode
  • CFRunLoopSource
  • CFRunLoopObserver
  • CFRunLoopTimer

CFRunLoop

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 內核向該端口發送消息能夠喚醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop 
    pthread_t _pthread;  //RunLoop對應的線程
    uint32_t _winthread; //
    CFMutableSetRef _commonModes; //存儲的是字符串,記錄全部標記爲common的mode
    CFMutableSetRef _commonModeItems;//存儲全部commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode; //當前運行的mode
    CFMutableSetRef _modes;   //存儲的是CFRunLoopModeRef
    struct _block_item *_blocks_head; //doblocks的時候用到
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};
複製代碼

能夠看到,其實runloop就是一個結構體對象,裏面包含了一個線程,一個當前正在運行的mode, N個mode,N個commonMode。

  • runloop和線程一一對應
  • runloop包含多個mode, mode包含多個 mode item(sources,timers,observers)
  • runloop一次只能運行在一個model下:
    • 切換mode:中止loop -> 設置mode -> 重啓runloop
    • runloop經過切換mode來篩選要處理的事件,讓其互不影響
    • iOS運行流暢的關鍵

CFRunLoopMode

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name; //mode的名稱
    Boolean _stopped;  //mode是否被終止
    char _padding[3];
    CFMutableSetRef _sources0; //sources0
    CFMutableSetRef _sources1;  //sources1
    CFMutableArrayRef _observers; //通知
    CFMutableArrayRef _timers;  //定時器
    CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet;    //保存全部須要監聽的port,好比_wakeUpPort,_timerPort都保存在這個數組中
    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 */
};
複製代碼

一個CFRunLoopMode對象有一個name,N個source0、N個source一、timer、observer和port,可見事件都是由Mode在管理,而RunLoop管理Mode

它們之間的關係以下圖:

mode是容許定製的,不過至少要包含一個mode item(source/timer/observer)。 同一個mode item能夠被多個mode持有

蘋果公開的三種 RunLoop Mode:

  • NSDefaultRunLoopMode(kCFRunloopDefaultMode):默認狀態,app一般在這個mode下運行
  • UITrackingRunLoopMode:界面跟蹤mode(例如滑動scrollview時不被其餘mode影響)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):是前兩個mode的集合,能夠把自定義mode用CFRunLoopAddCommonMode函數加入到集合中

還有兩種mode,只需作了解便可:

  • GSEventReceiveRunLoopMode:接收系統內部mode,一般用不到
  • UIInitializationRunLoopMode:私有,只在app啓動時使用,使用完就不在集合中了

CFRunLoopSource

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; ////用於標記Signaled狀態,source0只有在被標記爲Signaled狀態,纔會被處理
    pthread_mutex_t _lock;
    CFIndex _order;			/* immutable */
    CFMutableBagRef _runLoops;
    union {
	CFRunLoopSourceContext version0;	/* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;	/* immutable, except invalidation */
    } _context;
};
複製代碼

CFRunloopSourceRef是runloop的數據源抽象類對象(protocol),由源碼能夠看到共用體(union:在相同的內存位置存儲不一樣的數據類型),可見Source分爲兩類:

Source0
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;
複製代碼

source0: 處理App內部事件、APP本身負責管理(觸發)例如:UIEvent CFSocket。 打斷點基本都會看到它。

  • source0是非基於Port的。只包含了一個回調(函數指針),它並不能主動觸發事件。

  • CFRunLoopSourceSignal(source)將這個事件標記爲待處理

  • CFRunLoopWakeUp來喚醒runloop,讓他處理事件

自定義source實現步驟:

  1. 建立一個底層source0 源 CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
  2. 把咱們的建立的source0添加到runloop CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode)
  3. 執行信號,標記待處理CFRunLoopSourceSignal
  4. 喚醒runloop去處理CFRunLoopWakeUp
  5. 取消移除源CFRunLoopRemoveSource
  6. 釋放runloopCFRelease(rlp)
Source1
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
複製代碼

source1:

  • 由runloop和 Mach port管理,Mach port驅動,包含一個 mach_port和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。

  • 它可以主動喚醒RunLoop(由操做系統內核進行管理,例如: CFMachPort,CFMessagePort)

  • 還容許實現本身的Source,但通常不會這麼作

CFRunLoopObserver

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};
複製代碼

它是一個觀察者,可以監聽Runloop的狀態改變,能夠向外部報告runloop狀態的更改,框架中不少機制都由它觸發(如CAAnimation)

CFRunloop.h文件裏能夠看到observer監聽的狀態以下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

正好和下圖runloop流程裏的observer所對應:

CFRunLoopTimer

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;		/* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;			/* TSR units */
    CFIndex _order;			/* immutable */
    CFRunLoopTimerCallBack _callout;	/* immutable */
    CFRunLoopTimerContext _context;	/* immutable, except invalidation */
};
複製代碼
  • CFRunLoopTimer是定時器,能夠在設定的時間點拋出回調
  • CFRunLoopTimerNSTimer是toll-free bridged的,能夠相互轉換
  • CFRunLoopTimer的封裝有三種: NSTimer,performSelector和CADisplayLink
+ (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;
複製代碼

簡單總結了這三種timer,以下圖:

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

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼

CFRunLoopRunCFRunLoopRunInMode都調用了CFRunLoopRunSpecific函數

CFRunLoopRunSpecific

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    
    /// 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// 內部函數,進入loop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}
複製代碼

上面的源碼是簡化代碼後的源碼,實際源碼複雜一些,根據源碼可得出以下結論:

  • 在進入run loop以前通知observer,狀態爲kCFRunLoopEntry
  • 在退出run loop以後通知observer,狀態爲kCFRunLoopExit
  • 進入runloop的時候調用了__CFRunLoopRun函數

__CFRunLoopRun(核心重點)

/// 核心函數
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    int32_t retVal = 0;
    do {
        
        /// 通知 Observers: 即將處理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        /// 通知 Observers: 即將處理Source事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
        
        /// 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        /// 處理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        
        /// 處理sources0返回爲YES
        if (sourceHandledThisLoop) {
            /// 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        
        /// 判斷有無故口消息(Source1)
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            /// 處理消息
            goto handle_msg;
        }
        
        /// 通知 Observers: 即將進入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        
        /// 等待被喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        
        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);
        
        /// 通知 Observers: 被喚醒,結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:
        if (被Timer喚醒) {
            /// 處理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD喚醒) {
            /// 處理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else if (被Source1喚醒) {
            /// 被Source1喚醒,處理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
        }
        
        /// 處理block
        __CFRunLoopDoBlocks(rl, rlm);
        
        
        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;
}
複製代碼

以上是runloop核心函數的簡寫源碼(比較清晰易懂)點擊下載runloop源碼:密碼 3kww 還有一個監聽喚醒端口消息的函數__CFRunLoopServiceMachPort比較重要,系統內核將這個線程掛起,停留在mach_msg_trap狀態,等待接受 mach_port(用於喚醒的端口) 的消息。線程將進入休眠, 直到被其餘線程或另外一個進程的某個線程向該端口發送mach_msg消息喚醒

__CFRunLoopServiceMachPort

/** * 接收指定內核端口的消息 * * @param port 接收消息的端口 * @param buffer 消息緩衝區 * @param buffer_size 消息緩衝區大小 * @param livePort 暫且理解爲活動的端口,接收消息成功時候值爲msg->msgh_local_port,超時時爲MACH_PORT_NULL * @param timeout 超時時間,單位是ms,若是超時,則RunLoop進入休眠狀態 * * @return 接收消息成功時返回true 其餘狀況返回false */
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) {
    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(); }
        //經過mach_msg發送或者接收的消息都是指針,
        //若是直接發送或者接收消息體,會頻繁進行內存複製,損耗性能
        //因此XNU使用了單一內核的方式來解決該問題,全部內核組件都共享同一個地址空間,所以傳遞消息時候只須要傳遞消息的指針
        ret = mach_msg(msg, MACH_RCV_MSG|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);
        CFRUNLOOP_WAKEUP(ret);
        //接收/發送消息成功,給livePort賦值爲msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
         //MACH_RCV_TIMEOUT
        //超出timeout時間沒有收到消息,返回MACH_RCV_TIMED_OUT
        //此時釋放緩衝區,把livePort賦值爲MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
         //MACH_RCV_LARGE
        //若是接收緩衝區過小,則將過大的消息放在隊列中,而且出錯返回MACH_RCV_TOO_LARGE,
        //這種狀況下,只返回消息頭,調用者能夠分配更多的內存
        if (MACH_RCV_TOO_LARGE != ret) break;
          //此處給buffer分配更大內存
        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應用

事件響應

  • 當一個硬件事件(觸摸/鎖屏/搖晃/加速)發生後,首先有IOKit.framework生成一個IOHIDEvent事件並由SpringBoard接受,以後由mach port轉發給須要的App進程。

  • 蘋果註冊了一個 Source1 來接受系統事件,經過回調函數觸發Source0(因此Event其實是基於Source0)的,調用_UIApplicationHandleEventQueue() 進行應用內部的分發。 _UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。

手勢識別

  • 當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer標記爲待處理。

  • 蘋果註冊了一個 Observer 監測 BeforeWaiting(Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的GestureRecognizer,並執行GestureRecognizer的回調。

  • 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。

界面刷新

  • 當UI發生改變時(Frame變化,UIView/CALayer的結構變化)時,或手動調用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法後,這個UIView/CALayer就被標記爲待處理。

  • 蘋果註冊了一個用來監聽BeforeWaitingExit的Observer,在他的回調函數裏會遍歷全部待處理的UIView/CALayer來執行實際的繪製和調整,並更新UI界面。

AutoreleasePool

  • 主線程Runloop註冊了兩個Observers,其回調都是_wrapRunloopWithAutoreleasePoolHandler

  • Observers1 監聽Entry事件: 優先級最高,確保在全部的回調前建立釋放池,回調內調用 _objc_autoreleasePoolPush()建立自動釋放池

  • Observers2監聽BeforeWaitingExit事件: 優先級最低,保證在全部回調後釋放釋放池。BeforeWaiting事件:調用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊池並建立新池,Exit事件: 調用_objc_autoreleasePoolPop(),釋放自動釋放池

tableView延遲加載圖片,保證流暢

ImageView加載圖片的方法用PerformSelector設置當前線程的RunLoop的運行模式kCFRunLoopDefaultMode,這樣滑動時候就不會執行加載圖片的方法 [self.imgView performSelector:@selector(setImage:) withObject:cellImg afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

Timer不被ScrollView的滑動影響

  • +timerWihtTimerInterval... 建立timer
  • [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]把timer加到當前runloop,使用佔位模式
  • runloop run/runUntilData手動開啓子線程runloop
  • 使用GCD建立定時器,GCD建立的定時器不會受RunLoop的影響
// 得到隊列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 建立一個定時器(dispatch_source_t本質仍是個OC對象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 設置定時器的各類屬性(幾時開始任務,每隔多長時間執行一次)
    // GCD的時間參數,通常是納秒(1秒 == 10的9次方納秒)
    // 比當前時間晚1秒開始執行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
    //每隔一秒執行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    
    // 設置回調
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });
    
    // 啓動定時器
    dispatch_resume(self.timer);
複製代碼

GCD

  • dispatch_async(dispatch_get_main_queue)使用到了RunLoop

  • libDispatch向主線程的Runloop發送消息將其喚醒,並從消息中取得block,並在回調__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()裏執行這個block

NSURLConnection

  • 使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 後,這個 Delegate就會不停收到事件回調。

  • start 這個函數的內部會會獲取 CurrentRunLoop,而後在其中的 DefaultMode 添加了4個 Source0 (即須要手動觸發的Source)。 CFMultiplexerSource 是負責各類 Delegate 回調的,CFHTTPCookieStorage 是處理各類 Cookie 的。

  • 當開始網絡傳輸時,咱們能夠看到 NSURLConnection 建立了兩個新線程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 鏈接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,並經過以前添加的 Source0 通知到上層的 Delegate。

AFNetworking

  • 使用runloop開啓常駐線程
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];
複製代碼
  • 給 runloop 添加[NSMachPort port](source1)使runloop不退出,實際並無給這個port發消息

AsyncDisplayKit

仿照 QuartzCore/UIKit 框架的模式,實現了一套相似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer,監聽了 kCFRunLoopBeforeWaitingkCFRunLoopExit 事件,在收到回調時,遍歷全部以前放入隊列的待處理的任務,而後一一執行。

卡頓檢測

  • dispatch_semaphore_t 是一個信號量機制,信號量到達、或者 超時會繼續向下進行,不然等待,若是超時則返回的結果一定不爲0,信號量到達結果爲0。GCD信號量-dispatch_semaphore_t

  • 經過監聽mainRunloop的狀態和信號量阻塞線程的特色來檢測卡頓,經過kCFRunLoopBeforeSourcekCFRunLoopBeforeWaiting的間隔時長超過自定義閥值則記錄堆棧信息。

  • 推薦文章: RunLoop實戰:實時卡頓監控

FPS檢測

  • 建立CADisplayLink對象的時候會指定一個selector,把建立的CADisplayLink對象加入runloop,因此就實現了以屏幕刷新的頻率調用某個方法。

  • 在調用的方法中計算執行的次數,用次數除以時間,就算出了FPS。

  • 注:iOS正常刷新率爲每秒60次。

@implementation ViewController {
    UILabel *_fpsLbe;
    
    CADisplayLink *_link;
    NSTimeInterval _lastTime;
    float _fps;
}

- (void)startMonitoring {
    if (_link) {
        [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        [_link invalidate];
        _link = nil;
    }
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    self.count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    _fps = _count / delta;
    NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
    self.count = 0;
    _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}
複製代碼

防崩潰處理

  • NSSetUncaughtExceptionHandler(&HandleException);監聽異常信號SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGVSIGFPE

  • 回調方法內建立一個Runloop,將主線程的全部Runmode都拿過來跑,做爲應用程序主Runloop的替代。

CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);

while (captor.needKeepAlive) {
    for (NSString *mode in (__bridge NSArray *)allModesRef) {
        if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
            continue;
        }
        CFStringRef modeRef  = (__bridge CFStringRef)mode;
        CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
    }
}
複製代碼
  • 能夠記錄堆棧信息,上傳服務器或者彈出友好提示頁面等一系列操做。

常駐線程

能夠把本身建立的線程添加到Runloop中,作一些頻繁處理的任務,例如:檢測網絡狀態,定時上傳一些信息等。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"----------run----%@", [NSThread currentThread]);
    @autoreleasepool{
    /*若是不加這句,會發現runloop建立出來就掛了,由於runloop若是沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。 下面的方法給runloop添加一個NSport,就是添加一個事件源,也能夠添加一個定時器,或者observer,讓runloop不會掛掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    // 方法1 ,2,3實現的效果相同,讓runloop無限期運行下去
    [[NSRunLoop currentRunLoop] run];
   }

    
    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    NSLog(@"---------");
}

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
複製代碼

以上均爲我的對runloop的資料收集及部分理解,若有錯誤請指正,歡迎討論。

相關文章
相關標籤/搜索