RunLoop的一些學習與總結(jié)

最近在學習一些OC底層的東西, 下面是學習了RunLoop的一些總結(jié)和感受^^

首先,RunLoop的作用

從字面上看,RunLoop是運行循環(huán)的意思.實際上,線程就是依靠RunLoop來實現(xiàn)任務(wù)來了被喚醒,完成任務(wù)之后休眠的這一特性.

RunLoop的基本作用:

  • 保持程序的持續(xù)運行
  • 處理App中的各種事件(比如觸摸事件, 定時器事件, selector事件)
  • 節(jié)約系統(tǒng)資源,提高程序性能(有事件要處理時被喚醒,沒事件時休眠)

RunLoop實際上就是一個對象棱诱,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行任務(wù)來了被喚醒,完成任務(wù)之后休眠的邏輯涝动。線程執(zhí)行了這個函數(shù)后迈勋,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入quit的消息)醋粟,函數(shù)返回靡菇。

OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象:NSRunLoopCFRunLoopRef米愿。
CFRunLoopRef是在CoreFoundation框架內(nèi)的厦凤,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的育苟。
NSRunLoop是基于CFRunLoopRef的封裝较鼓,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的违柏。

RunLoop與線程的關(guān)系

蘋果不允許直接創(chuàng)建RunLoop博烂,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()CFRunLoopGetCurrent()。這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:

/// 全局的Dictionary漱竖,key 是 pthread_t禽篱, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時馍惹,初始化全局Dic躺率,并先為主線程創(chuàng)建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取万矾。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時悼吱,創(chuàng)建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊一個回調(diào),當線程銷毀時良狈,順便也銷毀其對應(yīng)的 RunLoop后添。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

從上面的代碼中,我們可以得到以下的一些結(jié)論:

  • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象,并且以{pthread_t : CFRunLoopRef}的形式保存在一個全局的Dictionary中
  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要手動進行創(chuàng)建
  • RunLoop在第一次獲取時通過懶加載創(chuàng)建,在線程結(jié)束時銷毀
  • 你只能在一個線程內(nèi)部獲取屬于它自己的那個RunLoop(主線程除外)
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

RunLoop相關(guān)類

Core Foundation中關(guān)于RunLoop的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

他們的關(guān)系關(guān)系如圖:

RunLoop_0.png

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer们颜。每次調(diào)用 RunLoop 的主函數(shù)時吕朵,只能指定其中一個 Mode猎醇,這個Mode被稱作 CurrentMode。如果需要切換 Mode努溃,只能退出 Loop硫嘶,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer梧税,讓其互不影響沦疾。

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(輸入源)

以前的分法

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

現(xiàn)在的分法

  • Source0:只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件第队。使用時哮塞,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理凳谦,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop忆畅,讓其處理這個事件。
  • Source1:包含了一個 mach_port和一個回調(diào)(函數(shù)指針)尸执,被用于通過內(nèi)核和其他線程相互發(fā)送消息

可以以下代碼添加新的CFRunLoopSourceRef:

[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

CFRunLoopTimerRef

CFRunLoopTimerRef是基于時間的觸發(fā)器家凯,它和 NSTimertoll-free bridged 的,可以混用如失。其包含一個時間長度和一個回調(diào)(函數(shù)指針)绊诲。當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點褪贵,當時間點到時掂之,RunLoop會被喚醒以執(zhí)行那個回調(diào)。

  • NSTimer會受到RunLoop的Mode的影響可能會不準確
  • GCD的定時器不受RunLoop的Mode的影響

下面是兩種往RunLoop中添加定時器的方法:

- (void)testRunloopTimerWithNSTimer
{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"我是NSTimer");
    }];
    //默認模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //滑動模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    //能夠在默認模式或者滑動模式中回調(diào)這個NSTimer
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)testRunloopTimerWithGCDTimer
{
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"我是GCD Timer");
    });
    dispatch_resume(timer);
    //需要要有一個指針指向Timer,不然這個函數(shù)結(jié)束Timer就被釋放掉了
    self.timer = timer;
}

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop狀態(tài)變化. 能夠觀測的時間點有以下幾個:

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

Runloop的Mode

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item脆丁,一個item可以被同時加入多個mode世舰。但一個 item被重復加入同一個mode 時是不會有效果的。如果一個 mode 中一個 item都沒有偎快,則 RunLoop會直接退出冯乘,不進入循環(huán)。

CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu)大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

幾種常使用的Mode:

  • NSDefaultRunLoopMode:這是App平時所處的狀態(tài)
  • UITrackingRunLoopMode:這是追蹤 ScrollView滑動時的狀態(tài)
  • NSRunLoopCommonModes:能夠?qū)?code>mode itemr添加到__CFRunLoop里面的_commonModeItems中,然后RunLoop會將會自動將這個mode item同步到具有 "Common"標記的所有Mode(NSDefaultRunLoopMode和UITrackingRunLoopMode)里晒夹。

RunLoop的內(nèi)部邏輯

其內(nèi)部代碼整理如下:

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據(jù)modeName找到對應(yīng)mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里沒有source/timer/observer, 直接返回姊氓。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop丐怯。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內(nèi)部函數(shù),進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)翔横。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)读跷。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)禾唁,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息效览。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)无切。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒丐枉。
            /// ? 一個基于 port 的Source 的事件哆键。
            /// ? 一個 Timer 到時間了
            /// ? RunLoop 自身的超時時間到了
            /// ? 被其他什么調(diào)用者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息瘦锹,處理消息籍嘹。
            handle_msg:
 
            /// 9.1 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)弯院。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block辱士,執(zhí)行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了听绳,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 執(zhí)行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時參數(shù)說處理完事件就返回颂碘。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時,mode里沒空椅挣,loop也沒被停止凭涂,那繼續(xù)loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出贴妻。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到切油,實際上RunLoop 就是這樣一個函數(shù),其內(nèi)部是一個 do-while 循環(huán)名惩。當你調(diào)用CFRunLoopRun()時澎胡,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止娩鹉,該函數(shù)才會返回攻谁。

應(yīng)用

  • 使用線程的定時器

  • Cocoa中使用任何performSelector…的方法

  • 自動釋放池

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer弯予,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()戚宦。

第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池锈嫩。其 order 是-2147483647受楼,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前呼寸。

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池艳汽;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop()來釋放自動釋放池。這個 Observer 的 order 是 2147483647对雪,優(yōu)先級最低河狐,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)馋艺、Timer回調(diào)內(nèi)的栅干。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏捐祠,開發(fā)者也不必顯示創(chuàng)建 Pool了碱鳞。

  • 創(chuàng)建一個常駐線程

開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件),可以在這個線程里面開啟一個定時器或者進行一些長期監(jiān)控

通過方法[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];在RunLoop中添加至少一個Source就能夠讓線程常駐

  • 可以添加Observer監(jiān)聽RunLoop的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)

參考資料

ibireme深入理解RunLoop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雏赦,隨后出現(xiàn)的幾起案子劫笙,更是在濱河造成了極大的恐慌,老刑警劉巖星岗,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件填大,死亡現(xiàn)場離奇詭異,居然都是意外死亡俏橘,警方通過查閱死者的電腦和手機允华,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寥掐,“玉大人靴寂,你說我怎么就攤上這事≌僭牛” “怎么了百炬?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長污它。 經(jīng)常有香客問我剖踊,道長,這世上最難降的妖魔是什么衫贬? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任德澈,我火速辦了婚禮,結(jié)果婚禮上固惯,老公的妹妹穿的比我還像新娘梆造。我一直安慰自己,他們只是感情好葬毫,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布镇辉。 她就那樣靜靜地躺著,像睡著了一般供常。 火紅的嫁衣襯著肌膚如雪摊聋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天栈暇,我揣著相機與錄音,去河邊找鬼箍镜。 笑死源祈,一個胖子當著我的面吹牛煎源,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播香缺,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼手销,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了图张?” 一聲冷哼從身側(cè)響起锋拖,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祸轮,沒想到半個月后兽埃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡适袜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年柄错,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苦酱。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡售貌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疫萤,到底是詐尸還是另有隱情颂跨,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布扯饶,位于F島的核電站恒削,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帝际。R本人自食惡果不足惜蔓同,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹲诀。 院中可真熱鬧斑粱,春花似錦、人聲如沸脯爪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痕慢。三九已至尚揣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掖举,已是汗流浹背快骗。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人方篮。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓名秀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親藕溅。 傳聞我的和親對象是個殘疾皇子匕得,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容