iOS-RunLoop淺析

RunLoop是iOS事件響應與任務處理最核心的機制,它貫穿iOS整個系統,自動釋放池,延遲處理,觸摸事件,屏幕刷新都是通過RunLoop實現的.Foundation中的NSRunLoop和Core Foundation中CFRunLoop 是RunLoop的主要實現.

基礎實現

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

Apple不允許直接創建 RunLoop,提供了兩個自動獲取的函數: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獲取當前線程,pthread_main_thread_np獲取主線程,通過線程獲取當前的Runloop.線程和Runloop被保存在全局字典__CFRunLoops中,如果字典中存在則會取出,如果不存在則會創建.

// 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運行:

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是線程中的一個循環,在循環中不斷檢測通過Input sources(輸入源)和Timer sources(定時源)兩種來源等待接受事件,然后對接受到的事件通知線程進行處理,并在沒有事件的時候進行睡眠.

RunLoop與線程之間的關系密不可分:
1.線程與RunLoop是一一對應的,一個線程對應一個RunLoop對象,根RunLoop可以嵌套子RunLoop.
2.主線程的RunLoop在應用啟動的時候會自動創建,非主線程的RunLoop需要在該線程自己啟動.
3.RunLoop對象在第一次獲取RunLoop時創建,銷毀則是在線程結束的時候.
4.不能自己創建RunLoop.
5.RunLoop并不是線程安全的,只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop,同時也需要避免在其他線程上調用當前線程的RunLoop.

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

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

Timer 是基于時間的觸發器,CFRunLoopTimerRef 和 NSTimer 可以通過 Toll-free bridging 技術混用,Toll-free bridging 是一種允許某些 ObjC 類與其對應的 CoreFoundation 類之間可以互換使用的機制,當將 Timer 加入到 RunLoop 時,RunLoop 會注冊對應的時間點,當時間點到時,RunLoop 會被喚醒以執行 Timer 回調.

__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

基礎知識

RunLoop運行狀態通過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
};
運行時.png
  1. kCFRunLoopEntry -- 進入runloop循環
  2. kCFRunLoopBeforeTimers -- 處理定時調用前回調
  3. kCFRunLoopBeforeSources -- 處理input sources的事件
  4. kCFRunLoopBeforeWaiting -- runloop睡眠前調用
  5. kCFRunLoopAfterWaiting -- runloop喚醒后調用
  6. kCFRunLoopExit -- 退出runloop

CoreFoundation中關于RunLoop的5個重要的類:

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

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

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

ibireme.png

Runloop 模式

系統默認定義了多種運行模式(CFRunLoopModeRef),常見的有五種:

  1. kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個 Mode 下運行的.

  2. UITrackingRunLoopMode:界面跟蹤 Mode,用于ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode 影響.

  3. UIInitializationRunLoopMode:在剛啟動 App 時第進入的第一個Mode,啟動完成之后就不再使用.

  4. GSEventReceiveRunLoopMode:接收系統事件的內部 Mode,通常用不到.

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

RunLoop與自動釋放池:
自動釋放池寄生于Runloop,程序啟動后,主線程注冊了兩個Observer監聽runloop的進出和睡眠.一個最高優先級OB監測Entry狀態,一個最低優先級OB監聽BeforeWaiting狀態和Exit狀態.

RunLoop 實戰

1.維護線程的生命周期,讓線程不自動退出,isFinished為Yes時退出.

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

2.創建常駐線程,執行一些會一直存在的任務,該線程的生命周期跟App相同:

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

創建常駐線程最經典的例子是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.在一定時間內監聽某種事件,或執行某種任務的線程
如下代碼,在30分鐘內,每隔30s執行onTimerFired.這種場景一般會出現在,如我需要在應用啟動之后,在一定時間內持續更新某項數據,如果用來監控屏幕的卡頓也可以.

@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滾動加載圖片
當tableView的cell上有需要從網絡獲取的圖片的時候,滾動tableView,異步線程回去加載圖片,加載完成后主線程會設置cell的圖片,但是會造成卡頓。可以設置圖片的任務在CFRunloopDefaultMode下進行,當滾動tableView的時候,Runloop切換到UITrackingRunLoopMode,不去設置圖片,而是而是當停止的時候,再去設置圖片.

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

5.NSTimer失效
如果頁面有計時器同時有滑動視圖的時候,需要注意NSTimer的模式,視圖滑動的過程會切換至UITrackingMode模式下,造成Timer短暫失效,將Timer的模式設置為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 原理和核心機制
CoreFoundation
深入理解RunLoop
滑動卡頓優化
官方文檔
CF源碼
http://blog.raozhizhen.com/post/2016-08-18

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

推薦閱讀更多精彩內容

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