iOS內存管理10 -- RunLoop運行循環

RunLoop的基本概念

  • RunLoop運行循環,其本質是一個 do-while循環,與普通的while循環是有區別的,普通的while循環會導致CPU進入忙等待狀態,即一直消耗CPU,而RunLoop則不會,RunLoop是一種閑等待,無事件處理時會進入休眠狀態,不會消耗CPU資源

RunLoop的基本作用

  • 保證應用程序能持續穩定的運行而不退出,例如我們的在app啟動之后,默認會開啟一個主運行循環RunLoop,其能讓app持續穩定運行,不會中途退出,除非用戶手動殺掉app進程;
  • 處理App中的各種事件(觸摸、定時器、performSelector),,也就是說RunLoop是一個事件處理循環;
  • 節省CPU資源,提升應用程序的性能,即當沒有事件處理時會進入休眠狀態,不會消耗CPU資源;

RunLoop的源碼分析

  • OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoopCFRunLoopRef
    CFRunLoopRef是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的,NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的;
  • CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/ 下載到整個 CoreFoundation 的源碼來查看
  • RunLoop不能手動創建,只能獲取,被動的創建,下面提供了獲取主線程RunLoop與獲取當前正在運行RunLoop函數的源碼:
//獲取主線程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;
}
//獲取當前正在運行的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
  • 最終都會進入CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)函數,參數是當前線程;
//全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //pthread_t為空 默認設置成main線程
    if (pthread_equal(t, kNilPthreadT)) {
       t = pthread_main_thread_np();
    }
    //保證線程安全
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        //第一次進入時,初始化全局Dic,并先為主線程創建一個RunLoop。
        __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //創建主運行循環
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //將線程和RunLoop 以鍵值對的形式 存儲在全局字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    //獲取其他線程的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        //RunLoop不存在,創建一個新的RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //線程與RunLoop 鍵值對存在在全局字典中
            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是以鍵值對的形式存儲在一個全局字典(__CFRunLoops)中的,所以線程與RunLoop之間是一一對應的關系;
  • 全局字典(__CFRunLoops)為空,即第一次初始化時,會創建主線程循環,并將主線程和主RunLoop 以鍵值對的形式 存儲在__CFRunLoops中;
  • 接下來,進入獲取其他線程的RunLoop,若RunLoop不存在,會創建RunLoop,執行__CFRunLoopCreate(t)函數;
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
    if (NULL == loop) {
       return NULL;
    }
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);
    //RunLoop的屬性設置
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}
  • 創建RunLoop對象,并進行相關屬性的設置;
RunLoop的底層結構
  • RunLoop的結構體定義如下:
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;
};
  • _currentMode:表示RunLoop當前的模式,是CFRunLoopModeRef結構體類型;
  • _modes:表示RunLoop的模式集合,說明RunLoop可以有多種不同的模式;
  • _commonModeItems:是<Source/Observer/Timer>組的Items集合;
RunLoop的模式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; //事件源0
    CFMutableSetRef _sources1; //事件源1
    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 */
};
  • 可以看到__CFRunLoopMode中包含以下主要部分:
  • 事件源:_sources0_sources1
  • 觀察者源:_observers
  • 定時器源:_timers
  • RunLoop擁有多種不同的Mode,每個Mode中包含Source/Timer/Observer; Source/Timer/Observer又稱為一組Item,子線程的RunLoop中必須要有Item的存在,也就是說必須要有源,有任務事件處理,否則RunLoop會無事可做,就會自動退出
RunLoop_Mode.png
  • RunLoopMode的類型有如下:
    • RunLoopMode在蘋果文檔中提及的有五個,而在iOS中公開暴露出來的只有 NSDefaultRunLoopModeNSRunLoopCommonModes,NSRunLoopCommonModes 實際上是一個 Mode 的集合,默認包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode
    • NSDefaultRunLoopMode:App的默認 Mode,通常主線程是在這個 Mode 下運行的;
    • NSConnectionReplyMode
    • NSModalPanelRunLoopMode
    • NSEventTrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響;
    • NSRunLoopCommonModes:偽模式,靈活性更好;
RunLoop的事件源_sources0與_sources1 -- CFRunLoopSourceRef
  • _sources0_sources1屬于CFRunLoopSourceRef類型,其底層結構體如下所示:
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;
};
  • CFRunLoopSourceRef 是事件產生的地方,可以看到Source源有兩種類型:Source0Source1
  • Source0 屬于CFRunLoopSourceContext類型:其只包含了一個回調(函數指針),它并不能主動觸發事件,使用時你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件,主要包括觸摸事件PerformSelectors事件
  • CFRunLoopSourceContext結構體如下:
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;
  • 代碼驗證如下:
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"點擊");
}
@end
  • 點擊屏幕,控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_45.png
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
    
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
}

- (void)test{
    NSLog(@"test");
}

@end
  • 控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_47.png
  • Source1 屬于CFRunLoopSourceContext1 類型:其包含了一個 mach_port 和一個回調(函數指針),被用于基于Port的線程間通信
  • CFRunLoopSourceContext1結構體如下:
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;
RunLoop的定時器源timers -- CFRunLoopTimerRef
  • RunLoop的定時器timers的類型為CFRunLoopTimerRef,其底層結構體如下所示:
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 */
};
  • 其是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用,其包含一個時間長度一個回調(函數指針),當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當到指定時間點時,RunLoop會被喚醒以執行那個回調(函數指針);

  • 代碼測試:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"NSTimer ---- timer調用了");
    }];
}
@end
  • 控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_48.png
RunLoop的觀察者源Observers -- CFRunLoopObserverRef
  • RunLoop的觀察者Observers屬于CFRunLoopObserverRef類型,其底層結構體如下:
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 */
};
  • 每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接收到這個變化,可以觀測的時間點狀態有以下幾個:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),       //即將進入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),//即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),//即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),         //即將退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
  • 測試代碼:
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
     //創建監聽者
     /*
     第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
     第二個參數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態
     第三個參數 Boolean repeats:YES:持續監聽 NO:不持續
     第四個參數 CFIndex order:優先級,一般填0即可
     第五個參數 :回調 兩個參數observer:監聽者 activity:監聽的事件
     */
    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添加監聽者
    /*
     第一個參數 CFRunLoopRef rl:要監聽哪個RunLoop,這里監聽的是主線程的RunLoop
     第二個參數 CFRunLoopObserverRef observer 監聽者
     第三個參數 CFStringRef mode 要監聽RunLoop在哪種運行模式下的狀態
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的內存管理(Core Foundation)
     凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最后做一次release
     GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經納入到了ARC中,所以我們不需要管了
     */
    CFRelease(observer);
}
@end
  • 點擊屏幕,控制臺打印結果如下:
Snip20210528_51.png
  • 可以看到Observer確實能監聽到RunLoop的運行狀態,包括喚醒,休息,以及處理各種事件;
Items源所觸發的事件回調類型
  • 上面已經驗證了Source0,定時器Timer與observer源的事件回調;
Snip20210528_49.png
  • 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的運行
  • 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);
}
  • 內部調用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);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    ////通知 Observers: RunLoop 即將進入loop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    ////進入loop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    ////通知 Observers: RunLoop 即將退出loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}
  • __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry):通知Observers,RunLoop即將進入循環,此處有Observer會創建AutoreleasePool: _objc_autoreleasePoolPush()
  • __CFRunLoopRun:核心函數,進入RunLoop運行循環;
  • __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit):通知Observers,RunLoop即將退出循環,此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
  • 進入__CFRunLoopRun源碼,由于這部分代碼較多,于是這里用偽代碼代替。其主要邏輯是根據不同的事件源進行不同的處理,當RunLoop休眠時,可以通過相應的事件喚醒RunLoop;
//核心函數
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){
    
    //通過GCD開啟一個定時器,然后開始跑圈
    dispatch_source_t timeout_timer = NULL;
    ...
    dispatch_resume(timeout_timer);
    
    int32_t retVal = 0;
    
    //處理事務,即處理items
    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 (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            // 處理消息
            goto handle_msg;
        }
        
        // 通知 Observers: 即將進入休眠 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        
        // 等待被喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
        
        // 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 就是這樣一個函數,其內部是一個 do-while 循環,當調用 CFRunLoopRun()函數時,線程就會一直停留在這個循環里,直到超時或被手動停止,該函數才會返回;
image.png
RunLoop與AutoreleasePool之間的關系
  • App啟動后,蘋果在主線程 RunLoop 里注冊了兩個Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()
  • 第一個Observer源 監聽RunLoop的進入狀態,其回調會調用 _objc_autoreleasePoolPush() 創建自動釋放池,此回調用執行的優先級是最高的,保證自動釋放池的創建 在其他所有回調執行之前;
  • 第二個Observer源監聽RunLoop進入休眠狀態RunLoop的退出狀態,RunLoop進入休眠狀態時 回調執行 釋放舊的自動釋放池并創建新的自動釋放池,RunLoop的退出狀態時 回調執行釋放自動釋放池,此時回調執行的優先級是最低的,保證自動釋放池的釋放,在其他所有回調執行之后;
  • 在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的,這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了;
  • 調試,在工程中加入符號斷點_wrapRunLoopWithAutoreleasePoolHandler,然后運行工程,我運行的Xcode版本為11.3.1;
Snip20210528_55.png
Snip20210528_56.png
面試題一:ScrollView發生滾動的時候,NSTimer為什么會停止工作?
  • 當ScrollView發生滾動的時候,主線程的RunLoop會切換到UITrackingRunLoopModel模式下,而NSTimer默認是添加到UIDefaultRunLoopModel下的,所以NSTimer不會執行回調,當ScrollView停止滾動是主線程的RunLoop又會切換到UIDefaultRunLoopModel模式下,此時NSTimer會恢復工作,執行自己的回調方法,為了保證NSTimer在UITrackingRunLoopModel模式下也能工作,可將主線程RunLoop的model模式設置成組合模式NSRunLoopCommonModes;
面試題二:RunLoop的實際應用
  • 應用一:線程保活,一般情況下,當線程執行完任務之后就會自動退出銷毀,如下所示:
#import "ViewController.h"
#import "YYThread.h"

@interface ViewController ()

@property(nonatomic,strong)YYThread *thread;

@end

@implementation ViewController

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

- (void)callBack {
    NSLog(@"%s %@ -- 開始",__func__,[NSThread currentThread]);
    NSLog(@"%s -- 結束",__func__);
}
@end
  • 自定義線程YYThread在執行完callBack方法之后,就會退出銷毀,下面使用RunLoop保活YYThread線程,并能執行其他任務,實現如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[YYThread alloc]initWithTarget:self selector:@selector(callBack) object:nil];
    [self.thread start];
}

- (void)callBack {
    NSLog(@"%s %@ -- 開始",__func__,[NSThread currentThread]);
    //實現線程保活
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"%s -- 結束",__func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //點擊讓子線程 繼續執行其他任務
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)test {
    NSLog(@"%s %@ -- 開始",__func__,[NSThread currentThread]);
}
  • 點擊屏幕,讓YYThread線程繼續執行test函數,說明RunLoop確實能讓YYThread線程存活,繼續執行其他任務;

  • 應用二:監聽App的卡頓,原理是子線程向主線程發送消息,若主線程未在指定的時間內作出應答,說明主線程在執行耗時任務,存在App的卡頓,代碼實現見iOS性能優化01 -- 卡頓優化

  • 應用三:優化tableView的滾動,利用RunLoop的Model特性,可將更新UI的操作放到
    UIDefaultRunLoopModel模式中,這樣tableView在滾動時不會因為更新UI而影響到平滑的滾動效果,網絡圖片加載完成,更新UI到ImageView時,使用UIDefaultRunLoopModel模式,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIImage *networkImage = nil;
    [self.icon performSelector:@selector(setImage:) withObject:networkImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容