iOS:RunLoop詳解

RunLoop 本質(zhì):

RunLoop 本質(zhì)上是一個運行循環(huán)撵溃,其作用是保持線程的生命,防止線程被銷毀掂咒,日常開發(fā)中無處不在才沧,為了感受到 RunLoop 的存在,舉個簡單的例子绍刮,我們都知道温圆,APP程序的入口在main.m里面:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

接下來我們試著感受一下 RunLoop,點擊查看 UIApplicationMain孩革,其返回值是一個 int 值:

// If nil is specified for principalClassName, the value for NSPrincipalClass from the Info.plist is used. If there is no
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

我們在 main 函數(shù)里面用一個 int 變量去接收它岁歉,然后在返回值前后添加輸出語句:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"1 %@", [NSThread currentThread]);
        
        int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        
        NSLog(@"2 %@", [NSThread currentThread]);
        
        return a;
    }
}

運行程序,看一下輸出:


image.png

我們看到膝蜈,只執(zhí)行了第一個輸出锅移,并且當(dāng)前線程是主線程,第二個輸出語句并沒有執(zhí)行饱搏,沒有執(zhí)行的原因非剃,有兩種可能,第一種:UIApplicationMain 內(nèi)部阻塞推沸;第二種:UIApplicationMain 函數(shù)內(nèi)部開啟了一個死循環(huán)努潘;但是一旦程序發(fā)生阻塞,APP 就無法使用了坤学,那么說得通的可能性就是第二種疯坤,這個函數(shù)內(nèi)部開啟了死循環(huán),就是 RunLoop 循環(huán)深浮,它的作用压怠,就是保持主線程的生命,并且?guī)臀覀儽O(jiān)聽一些事件飞苇,比如觸摸菌瘫、時鐘、網(wǎng)絡(luò)等布卡;

Runloop 的兩種形式:

OC: Foundation ——> NSRunLoop

// NSRunLoop獲取RunLoop
[NSRunloop currentRunloop];  // 獲得當(dāng)前線程的RunLoop的方法
[NSRunloop  mainRunloop];   // 獲得主線程的RunLoop的方法   

C: CoreFoudation ——> CFRunLoopRef

// CFRunLoopRef獲取RunLoop
CFRunloopGetCurrent();  // 獲得當(dāng)前線程的RunLoop的方法  
CFRunloopGetMain();   // 獲得主線程的RunLoop的方法  

NSRunLoop 是對 CFRunLoopRef 的進(jìn)一步封裝雨让,并且 CFRunLoopRef 是線程安全的,而這一點 NSRunLoop 并不能保證忿等。

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

每一條線程都有一個與之對應(yīng)的 RunLoop 對象栖忠;
RunLoop 保存在全局的字典內(nèi),是以線程為 key,RunLoop 為 value庵寞;
RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(懶加載狸相, 不獲取是不會創(chuàng)建的),RunLoop 的銷毀是發(fā)生在線程結(jié)束時捐川,只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)脓鹃。

RunLoop 對外的接口:

在 CoreFoundation 里面關(guān)于 RunLoop 有5個類:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;    // CFRunLoopRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;   // CFRunLoopSourceRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;  // CFRunLoopObserverRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;   // CFRunLoopTimerRef

CFRunLoopModeRef

其中 CFRunLoopModeRef 類并沒有對外暴露,只是通過 CFRunLoopRef 的接口進(jìn)行了封裝:


image.png

一個 RunLoop 包含若干個 Mode古沥,每個 Mode 又包含若干個 Source / Timer / Observer瘸右,每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode岩齿,這個 Mode 被稱作 CurrentMode尊浓,如果需要切換 Mode,只能退出 Loop纯衍,再重新指定一個 Mode 進(jìn)入栋齿。這樣做主要是為了分隔開不同組的 Source / Timer / Observer,讓其互不影響襟诸。

CFRunLoopSourceRef 是事件產(chǎn)生的地方瓦堵。Source有兩個版本:Source0 和 Source1。
? Source0 只包含了一個回調(diào)(函數(shù)指針)歌亲,它并不能主動觸發(fā)事件菇用。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source)陷揪,將這個 Source 標(biāo)記為待處理惋鸥,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件悍缠。
? Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針)卦绣,被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動喚醒 RunLoop 的線程飞蚓,其原理在下面會講到滤港。

CFRunLoopTimerRef 是基于時間的觸發(fā)器,它和 NSTimer 是 toll-free bridged 的趴拧,可以混用溅漾。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時著榴,RunLoop會注冊對應(yīng)的時間點呈宇,當(dāng)時間點到時腺晾,RunLoop 會被喚醒以執(zhí)行那個回調(diào)拓提。

CFRunLoopObserverRef 是觀察者斥扛,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)享完,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化,RunLoop 的狀態(tài)如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),  // 即將進(jìn)入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 即將處理Timer
    kCFRunLoopBeforeSources = (1UL << 2),   // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  // 剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),  // 即將退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

上面的 Source / Timer / Observer 被統(tǒng)稱為 mode item,一個 item 可以被同時加入多個 Mode瞎饲。但一個 item 被重復(fù)加入同一個 Mode 時是不會有效果的口叙。如果一個 Mode 中一個 item 都沒有炼绘,則 RunLoop 會直接退出,不進(jìn)入循環(huán)妄田。

RunLoop 的 Mode:

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

常見的2種 Mode:

kCFRunloopDefaultMode  //程序的默認(rèn)Mode
UITrackingRunloopMode  //界面追蹤Mode

不同 Mode 對應(yīng)的事件類型都不相同俺亮,例如我們平時使用的 Timer:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    // timerWithTimeInterval:target:selector:userInfo:repeats: 創(chuàng)建的 Timer 需要手動添加到RunLoop中才能運行
    // 兩個 Mode 同時添加,在兩個模式下都會運行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];  // 默認(rèn)模式下運行疟呐,但當(dāng)觸摸事件發(fā)生時脚曾,就會停止
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];  // 有觸摸事件時才會運行,但是時間結(jié)束后就會停止
    
    // 添加到 NSRunLoopCommonModes 中启具,在兩個模式下都會運行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}


- (void)timerMethod {
    NSLog(@"%@", [NSThread currentThread]);
}

通過這種方式創(chuàng)建的 Timer 本身是不會運行的本讥,只有將 Timer 添加到 RunLoop 中才會運行,我們先將 Mode 設(shè)置為 NSDefaultRunLoopMode鲁冯,Timer 會運行拷沸,并且打印輸出, 這個時候我們往界面中添加交互控件薯演,例如添加一個ScrollView撞芍,當(dāng)我們滑動這個控件時, Timer 就會停止跨扮,響應(yīng)完觸摸事件又會恢復(fù)運行序无,這是因為 RunLoop 會優(yōu)先響應(yīng) UITrackingRunloopMode 的事件,相反衡创,如果我們將 Mode 設(shè)置為 UITrackingRunloopMode帝嗡,那正常情況下 Timer 不會運行,當(dāng)有 UI 事件的時候璃氢,才會運行丈探;這和我們的初衷相背離,我們希望在任何情況下拔莱,Timer 都能運行碗降,這個時候,我們就要將 Timer 同時添加到 NSDefaultRunLoopMod 和 UITrackingRunloopMode 兩個模式下塘秦,NSRunLoopCommonModes 就是為我們解決這個問題讼渊。

注:
kCFRunloopCommonModes 默認(rèn)包括了 kCFRunloopDefaultMode 和 UITrackingRunloopMode;
kCFRunloopCommonModes 并不是一個真正的模式,只是一個標(biāo)記尊剔,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 的模式是真正意義的模式爪幻,timer 在設(shè)置了 kCFRunloopCommonModes 標(biāo)記的模式下都能運行,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 這兩個模式都是 kCFRunloopCommonModes 標(biāo)記的。

RunLoop 運行邏輯:

image.png

// 用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 即將進(jìn)入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內(nèi)部函數(shù)奶甘,進(jìn)入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 的線程即將進(jìn)入休眠(sleep)钉赁。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息蹄殃。線程將進(jìn)入休眠, 直到被下面某一個事件喚醒。
            /// ? 一個基于 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) {
                /// 進(jìn)入loop時參數(shù)說處理完事件就返回束莫。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入?yún)?shù)標(biāo)記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調(diào)用者強(qiáng)制停止了
                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)。當(dāng)你調(diào)用 CFRunLoopRun() 時怀各,線程就會一直停留在這個循環(huán)里倔韭,直到超時或被手動停止,該函數(shù)才會返回瓢对。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寿酌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子硕蛹,更是在濱河造成了極大的恐慌醇疼,老刑警劉巖硕并,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秧荆,居然都是意外死亡倔毙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門乙濒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陕赃,“玉大人,你說我怎么就攤上這事琉兜】” “怎么了毙玻?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵豌蟋,是天一觀的道長。 經(jīng)常有香客問我桑滩,道長梧疲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任运准,我火速辦了婚禮幌氮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胁澳。我一直安慰自己该互,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布韭畸。 她就那樣靜靜地躺著宇智,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胰丁。 梳的紋絲不亂的頭發(fā)上随橘,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機(jī)與錄音锦庸,去河邊找鬼机蔗。 笑死,一個胖子當(dāng)著我的面吹牛甘萧,可吹牛的內(nèi)容都是我干的萝嘁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扬卷,長吁一口氣:“原來是場噩夢啊……” “哼牙言!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起邀泉,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嬉挡,失蹤者是張志新(化名)和其女友劉穎钝鸽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庞钢,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡拔恰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了基括。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颜懊。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖风皿,靈堂內(nèi)的尸體忽然破棺而出河爹,到底是詐尸還是另有隱情,我是刑警寧澤桐款,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布咸这,位于F島的核電站,受9級特大地震影響魔眨,放射性物質(zhì)發(fā)生泄漏媳维。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一遏暴、第九天 我趴在偏房一處隱蔽的房頂上張望侄刽。 院中可真熱鬧,春花似錦朋凉、人聲如沸州丹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墓毒。三九已至,卻和暖如春盖灸,著一層夾襖步出監(jiān)牢的瞬間蚁鳖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工赁炎, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留醉箕,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓徙垫,卻偏偏與公主長得像讥裤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姻报,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

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

  • 參考資料:ibireme :http://blog.ibireme.com/2015/05/18/runloop/...
    代碼移動工程師閱讀 246評論 0 0
  • 一己英、簡介 CFRunLoopRef源碼 RunLoop是一個對象,這個對象在循環(huán)中用來處理程序運行過程中出現(xiàn)的各種...
    sheldon_龍閱讀 172評論 0 0
  • ios RunLoop詳解 一吴旋、概述 一般來說,一個線程只能執(zhí)行一個任務(wù)损肛,執(zhí)行完就會退出厢破。如果我們需要一種機(jī)制,讓...
    721e472431a4閱讀 797評論 0 1
  • 一.RunLoop介紹 1.概念 RunLoop是一個運行循環(huán),正是因為RunLoop治拿,IOS才可以保持程序的持續(xù)...
    一片姜汁閱讀 383評論 0 0
  • 一摩泪、概述 一般來說,一個線程只能執(zhí)行一個任務(wù)劫谅,執(zhí)行完就會退出见坑,如果我們需要一種機(jī)制,讓線程能隨時處理時間但并不退出...
    一直在路上66閱讀 287評論 0 0