iOS-RunLoop淺析

RunLoop是iOS事件響應(yīng)與任務(wù)處理最核心的機(jī)制,它貫穿iOS整個(gè)系統(tǒng),自動(dòng)釋放池,延遲處理,觸摸事件,屏幕刷新都是通過RunLoop實(shí)現(xiàn)的.Foundation中的NSRunLoop和Core Foundation中CFRunLoop 是RunLoop的主要實(shí)現(xiàn).

基礎(chǔ)實(shí)現(xiàn)

RunLoop通過do-while循環(huán)保持整個(gè)App的持續(xù)運(yùn)行,同時(shí)能在運(yùn)行和睡眠狀態(tài)之間切換,節(jié)省CPU資源.Android中的Looper跟iOS中的RunLoop類似,接收異步消息,控制應(yīng)用程序的生命周期.一般情況一個(gè)線程只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后就會退出.我們可以通過Runloop保證線程能隨時(shí)處理事件,并不退出.

Apple不允許直接創(chuàng)建 RunLoop,提供了兩個(gè)自動(dòng)獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent().

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

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

pthread_self獲取當(dāng)前線程,pthread_main_thread_np獲取主線程,通過線程獲取當(dāng)前的Runloop.線程和Runloop被保存在全局字典__CFRunLoops中,如果字典中存在則會取出,如果不存在則會創(chuàng)建.

// 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);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __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運(yùn)行:

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

運(yùn)行機(jī)制

RunLoop是線程中的一個(gè)循環(huán),在循環(huán)中不斷檢測通過Input sources(輸入源)和Timer sources(定時(shí)源)兩種來源等待接受事件,然后對接受到的事件通知線程進(jìn)行處理,并在沒有事件的時(shí)候進(jìn)行睡眠.

RunLoop與線程之間的關(guān)系密不可分:
1.線程與RunLoop是一一對應(yīng)的,一個(gè)線程對應(yīng)一個(gè)RunLoop對象,根RunLoop可以嵌套子RunLoop.
2.主線程的RunLoop在應(yīng)用啟動(dòng)的時(shí)候會自動(dòng)創(chuàng)建,非主線程的RunLoop需要在該線程自己啟動(dòng).
3.RunLoop對象在第一次獲取RunLoop時(shí)創(chuàng)建,銷毀則是在線程結(jié)束的時(shí)候.
4.不能自己創(chuàng)建RunLoop.
5.RunLoop并不是線程安全的,只能在當(dāng)前線程中操作當(dāng)前線程的RunLoop,而不能去操作其他線程的RunLoop,同時(shí)也需要避免在其他線程上調(diào)用當(dāng)前線程的RunLoop.

一個(gè) RunLoop 對象包含若干個(gè) Mode 對象,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer,RunLoop 一次運(yùn)行只能在一個(gè) Mode 之下,如果需要切換 Mode,需要退出 Loop 才能重新指定一個(gè) Mode.這樣做主要是為了分隔開不同組Source/Timer/Observer,讓其互不影響.

一個(gè) Source 對象是一個(gè)事件,Source 有兩個(gè)版本:Source0 和 Source1,Source0 只包含一個(gè)函數(shù)指針,并不能主動(dòng)觸發(fā),需要將 Source0 標(biāo)記為待處理,在 RunLoop 運(yùn)轉(zhuǎn)的時(shí)候,才會處理這個(gè)事件(如果 RunLoop 處于休眠狀態(tài),則不會被喚醒去處理),而 Source1 包含了一個(gè) mach_port 和一個(gè)函數(shù)指針,mach_port 是 iOS 系統(tǒng)提供的基于端口的輸入源,可用于線程或進(jìn)程間通訊。而 RunLoop 支持的輸入源類型中就包括基于端口的輸入源,可以做到對 mach_port 端口源事件的監(jiān)聽。所以監(jiān)聽到 source1 端口的消息時(shí),RunLoop 就會自己醒來去執(zhí)行 Source1 事件(也能稱為被消息喚醒)。也就是 Source0 是直接添加給 RunLoop 處理的事件,而 Source1 是基于端口的,進(jìn)程或線程之間傳遞消息觸發(fā)的事件.

Timer 是基于時(shí)間的觸發(fā)器,CFRunLoopTimerRef 和 NSTimer 可以通過 Toll-free bridging 技術(shù)混用,Toll-free bridging 是一種允許某些 ObjC 類與其對應(yīng)的 CoreFoundation 類之間可以互換使用的機(jī)制,當(dāng)將 Timer 加入到 RunLoop 時(shí),RunLoop 會注冊對應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop 會被喚醒以執(zhí)行 Timer 回調(diào).

__CFRunLoopMode定義如下:

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

基礎(chǔ)知識

RunLoop運(yùn)行狀態(tài)通過CFRunLoopActivity可以查看:

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
};
運(yùn)行時(shí).png
  1. kCFRunLoopEntry -- 進(jìn)入runloop循環(huán)
  2. kCFRunLoopBeforeTimers -- 處理定時(shí)調(diào)用前回調(diào)
  3. kCFRunLoopBeforeSources -- 處理input sources的事件
  4. kCFRunLoopBeforeWaiting -- runloop睡眠前調(diào)用
  5. kCFRunLoopAfterWaiting -- runloop喚醒后調(diào)用
  6. kCFRunLoopExit -- 退出runloop

CoreFoundation中關(guān)于RunLoop的5個(gè)重要的類:

  1. CFRunLoopRef:運(yùn)行循環(huán)對象,也就是它自身.
  2. CFRunLoopModeRef:指定runloop的運(yùn)行模式.給事件源分組,避免互相影響.一個(gè)runLoop可以有很多個(gè)Mode,1個(gè)Mode可以有很多個(gè)Source,Observer,Timer,但是在同一時(shí)刻只能同時(shí)執(zhí)行一種Mode.
  3. CFRunLoopSourceRef:輸入源.
  4. CFRunLoopTimerRef:定時(shí)源,定時(shí)器,必須加入到runloop.
  5. CFRunLoopObserverRef(觀察者,觀察是否有事件).

關(guān)于五個(gè)主要的類可以描述為一個(gè)RunLoop對象中包含若干個(gè)運(yùn)行模式(CFRunLoopModeRef),而每一個(gè)運(yùn)行模式下又包含若干個(gè)輸入源(CFRunLoopSourceRef)、定時(shí)源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef).

單次Runloop可以處理Source1(觸摸/鎖屏/搖晃),Source0事件(需要手動(dòng)觸發(fā)),Timer事件和觀察者事件.

ibireme.png

Runloop 模式

系統(tǒng)默認(rèn)定義了多種運(yùn)行模式(CFRunLoopModeRef),常見的有五種:

  1. kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的.

  2. UITrackingRunLoopMode:界面跟蹤 Mode,用于ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他Mode 影響.

  3. UIInitializationRunLoopMode:在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè)Mode,啟動(dòng)完成之后就不再使用.

  4. GSEventReceiveRunLoopMode:接收系統(tǒng)事件的內(nèi)部 Mode,通常用不到.

  5. kCFRunLoopCommonModes:占位用的 Mode,不是一種真正的 Mode.

RunLoop與自動(dòng)釋放池:
自動(dòng)釋放池寄生于Runloop,程序啟動(dòng)后,主線程注冊了兩個(gè)Observer監(jiān)聽runloop的進(jìn)出和睡眠.一個(gè)最高優(yōu)先級OB監(jiān)測Entry狀態(tài),一個(gè)最低優(yōu)先級OB監(jiān)聽BeforeWaiting狀態(tài)和Exit狀態(tài).

RunLoop 實(shí)戰(zhàn)

1.維護(hù)線程的生命周期,讓線程不自動(dòng)退出,isFinished為Yes時(shí)退出.

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

2.創(chuàng)建常駐線程,執(zhí)行一些會一直存在的任務(wù),該線程的生命周期跟App相同:

@autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
}

創(chuàng)建常駐線程最經(jīng)典的例子是AFNetWorking 2.x版本中代碼:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

3.在一定時(shí)間內(nèi)監(jiān)聽某種事件,或執(zhí)行某種任務(wù)的線程
如下代碼,在30分鐘內(nèi),每隔30s執(zhí)行onTimerFired.這種場景一般會出現(xiàn)在,如我需要在應(yīng)用啟動(dòng)之后,在一定時(shí)間內(nèi)持續(xù)更新某項(xiàng)數(shù)據(jù),如果用來監(jiān)控屏幕的卡頓也可以.

@autoreleasepool {
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                    target:self
                                                  selector:@selector(onTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}

4.UITableView滾動(dòng)加載圖片
當(dāng)tableView的cell上有需要從網(wǎng)絡(luò)獲取的圖片的時(shí)候,滾動(dòng)tableView,異步線程回去加載圖片,加載完成后主線程會設(shè)置cell的圖片,但是會造成卡頓??梢栽O(shè)置圖片的任務(wù)在CFRunloopDefaultMode下進(jìn)行,當(dāng)滾動(dòng)tableView的時(shí)候,Runloop切換到UITrackingRunLoopMode,不去設(shè)置圖片,而是而是當(dāng)停止的時(shí)候,再去設(shè)置圖片.

[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];

5.NSTimer失效
如果頁面有計(jì)時(shí)器同時(shí)有滑動(dòng)視圖的時(shí)候,需要注意NSTimer的模式,視圖滑動(dòng)的過程會切換至UITrackingMode模式下,造成Timer短暫失效,將Timer的模式設(shè)置為CommonMode即可.

self.upTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(upTimeUpdate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.upTimer forMode:NSRunLoopCommonModes];
    [self.upTimer fire];
    
    self.bottomTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(bottomTimeUpdate) userInfo:nil repeats:YES];

參考資料:
CF框架源碼
RunLoop 原理和核心機(jī)制
CoreFoundation
深入理解RunLoop
滑動(dòng)卡頓優(yōu)化
官方文檔
CF源碼
http://blog.raozhizhen.com/post/2016-08-18

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 最近離職了,可以盡情熬夜寫點(diǎn)總結(jié),不用擔(dān)心第二天上班爽并蛋疼著,這篇的主角 RunLoop 一座大山,涵蓋的...
    zerocc2014閱讀 12,549評論 13 67
  • 一、什么是runloop 字面意思是“消息循環(huán)、運(yùn)行循環(huán)”。它不是線程,但它和線程息息相關(guān)。一般來講,一個(gè)線程一次...
    WeiHing閱讀 8,309評論 11 111
  • 注:本篇博客只在 ibireme 的 深入理解RunLoop 基礎(chǔ)上做了點(diǎn)方便自己復(fù)習(xí)該知識點(diǎn)的修改,能力有限,如...
    AidenRao閱讀 3,015評論 6 26
  • RunLoop的定義與概念RunLoop的主要作用main函數(shù)中的RunLoopRunLoop與線程的關(guān)系RunL...
    __silhouette閱讀 1,074評論 0 6
  • 任濁世繁華 風(fēng)景如畫 在夕陽西下 你們就是我最深的牽掛 兩周的別離 雖說不上三秋 也是漫長如戈壁黃沙 此刻再美的風(fēng)...
    劉寒霜閱讀 195評論 8 9

友情鏈接更多精彩內(nèi)容