1. 程式人生 > IOS開發 >一文看完 Runloop

一文看完 Runloop

Runloop 是和執行緒緊密相關的一個基礎元件,是很多執行緒有關功能的幕後功臣。 本文將從以下幾個方面來總結runloop:

  • 什麼是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_FUNCTION****

  • 響應source0: ****CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION****

  • 響應source1: ****CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION****

  • GCD主佇列: ****CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE****

  • 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,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,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 [圖片上傳失敗...(image-ad314f-1571322546197)]) 測試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個source1、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,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(),&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(),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,Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);

    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl,false);

    /// 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(rl,currentMode,kCFRunLoopEntry);

    /// 內部函式,進入loop
    result = __CFRunLoopRun(rl,returnAfterSourceHandled,previousMode);

    /// 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl,kCFRunLoopExit);

    return result;
}
複製程式碼

上面的原始碼是簡化程式碼後的原始碼,實際原始碼複雜一些,根據原始碼可得出如下結論:

  • 在進入run loop之前通知observer,狀態為kCFRunLoopEntry
  • 在退出run loop之後通知observer,狀態為kCFRunLoopExit
  • 進入runloop的時候呼叫了__CFRunLoopRun函式

__CFRunLoopRun(核心重點)

/// 核心函式
static int32_t __CFRunLoopRun(CFRunLoopRef rl,CFRunLoopModeRef rlm,Boolean stopAfterHandle,CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        /// 通知 Observers: 即將處理timer事件
        __CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeTimers);

        /// 通知 Observers: 即將處理Source事件
        __CFRunLoopDoObservers(rl,kCFRunLoopBeforeSources)

        /// 處理Blocks
        __CFRunLoopDoBlocks(rl,rlm);

        /// 處理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl,stopAfterHandle);

        /// 處理sources0返回為YES
        if (sourceHandledThisLoop) {
            /// 處理Blocks
            __CFRunLoopDoBlocks(rl,rlm);
        }

        /// 判斷有無埠訊息(Source1)
        if (__CFRunLoopServiceMachPort(dispatchPort,&msg,sizeof(msg_buffer),&livePort,&voucherState,NULL)) {
            /// 處理訊息
            goto handle_msg;
        }

        /// 通知 Observers: 即將進入休眠
        __CFRunLoopDoObservers(rl,kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);

        /// 等待被喚醒
        __CFRunLoopServiceMachPort(waitSet,poll ? 0 : TIMEOUT_INFINITY,&voucherCopy);

        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);

        /// 通知 Observers: 被喚醒,結束休眠
        __CFRunLoopDoObservers(rl,kCFRunLoopAfterWaiting);

    handle_msg:
        if (被Timer喚醒) {
            /// 處理Timers
            __CFRunLoopDoTimers(rl,mach_absolute_time());
        } else if (被GCD喚醒) {
            /// 處理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else if (被Source1喚醒) {
            /// 被Source1喚醒,處理Source1
            __CFRunLoopDoSource1(rl,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,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),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,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的資料收集及部分理解,如有錯誤請指正,歡迎討論。

歡迎加入iOS開發交流學習群(密碼123),我們一起共同學習,共同成長

收錄:原文地址