RunLoop學(xué)習(xí)筆記

一般來(lái)講荐类,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完任務(wù)后線程就會(huì)退出久窟。如果我們需要線程隨時(shí)處理任務(wù)而不退出癞谒,通常的代碼邏輯是這樣的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱(chēng)為Event Loop桐猬。Event Loop在iOS里的實(shí)現(xiàn)就是RunLoop矩动。實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息勺三;如何讓線程在沒(méi)有消息處理時(shí)處于休眠以避免資源占用隧膘、在有消息到來(lái)時(shí)被喚醒來(lái)處理消息囤采。
所以述呐,RunLoop實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息蕉毯,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面的Event Loop的邏輯乓搬。
iOS系統(tǒng)中提供了兩個(gè)這樣的對(duì)象:NSRunLoop和CFRunLoopRef。

  • CFRunLoopRef是在CoreFoundation框架內(nèi)的代虾,它提供了純C函數(shù)的API进肯,所有這些API都是線程安全的;
  • NSRunLoop是基于CFRunLoopRef的封裝棉磨,提供了面向?qū)ο蟮腁PI江掩,但這些對(duì)象不是線程安全的。
    CFRunLoopRef是開(kāi)源的乘瓤,在這里可以下載到整個(gè)CoreFoundation环形。

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

iOS里有兩個(gè)線程對(duì)象:NSThread和pthread_t。CFRunloop是基于pthread來(lái)管理的衙傀。

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

/// 全局的Dictionary统抬,key 是 pthread_t火本, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問(wèn) loopsDic 時(shí)的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進(jìn)入時(shí)聪建,初始化全局Dic钙畔,并先為主線程創(chuàng)建一個(gè) RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取金麸。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時(shí)擎析,創(chuàng)建一個(gè)
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊(cè)一個(gè)回調(diào),當(dāng)線程銷(xiāo)毀時(shí)钱骂,順便也銷(xiāo)毀其對(duì)應(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());
}

從上面的代碼可以看出挪鹏,線程和RunLoop之間是一一對(duì)應(yīng)的见秽,線程作為key、CFRunLoopRef作為value保存到了全局的字典里讨盒。線程剛創(chuàng)建時(shí)沒(méi)有對(duì)應(yīng)的RunLoop解取,RunLoop的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop的銷(xiāo)毀是發(fā)生在線程結(jié)束時(shí)返顺。你只能在一個(gè)線程的內(nèi)部獲取RunLoop(主線程除外)禀苦。
我們的應(yīng)用程序不需要自己創(chuàng)建RunLoop,而是在合適的時(shí)間來(lái)啟動(dòng)RunLoop蔓肯。可以通過(guò) [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]來(lái)獲取RunLoop振乏。

RunLoop對(duì)外的接口

在CoreFoundation里面關(guān)于RunLoop有5各類(lèi):
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中CFRunLoopModeRef類(lèi)并沒(méi)有對(duì)外暴露蔗包,只是通過(guò)CFRunLoopRef的接口進(jìn)行了封裝。它們的關(guān)系如下:

RunLoop_0.png

RunLoop的Mode

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

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"慧邮,@"kCFRunLoopCommonModes"
    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
    ...
};

滑動(dòng)ScrollView時(shí)NSTimer失效的問(wèn)題

主線程的RunLoop里有兩個(gè)預(yù)制的mode:
KCFRunLoopDefaultMode和UITrackingRunLoopMode调限,這兩個(gè)Mode都已經(jīng)被標(biāo)記為“Common”屬性。DefaultMode是App平時(shí)所處的狀態(tài)误澳,TrackingRunLoopMode是追蹤scrollView滑動(dòng)是的狀態(tài)耻矮。NSTimer初始化后默認(rèn)是KCFRunLoopDefaultMode的狀態(tài)。
要想讓timer在兩個(gè)狀態(tài)中都能正常使用忆谓,一種方法是將timer分別加入兩個(gè)mode中裆装,還有一種,就是將timer加入到頂層的RunLoop的“commonModeItems”中倡缠∩诿猓“commonModeItems”被更新到所有具有“Common”屬性的Mode里去。

開(kāi)啟一個(gè)NSTimer實(shí)際上就是在當(dāng)前的RunLoop中注冊(cè)了一個(gè)新的事件源昙沦,當(dāng)scrollView滑動(dòng)的過(guò)程中铁瞒,當(dāng)前的RunLoop會(huì)處于UITrackingRunLoopMode的模式下,在這個(gè)模式下桅滋,是不會(huì)處理NSDefaultRunLoopMode的消息慧耍。簡(jiǎn)單地說(shuō)就是NSTimer不會(huì)開(kāi)啟新的進(jìn)程,只是在RunLoop里注冊(cè)了一下丐谋,RunLoop每次loop時(shí)都會(huì)檢測(cè)這個(gè)timer芍碧,看是否可以觸發(fā)。當(dāng)RunLoop處于A Mode中号俐,而timer注冊(cè)在B Mode時(shí)就無(wú)法檢測(cè)到這個(gè)timer泌豆,所以需要把timer也注冊(cè)到A Mode,這樣就可以檢測(cè)到吏饿。
解決方法:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

如果定時(shí)器所在的runloop沒(méi)有運(yùn)行踪危,或者runloop所處的mode和定時(shí)器的不一致,都會(huì)導(dǎo)致定時(shí)器失效猪落。

NSRunLoopMode

typedef NSString *NSRunLoopMode;
NSRunLoopCommonMode:被添加到RunLoop里的對(duì)象使用這個(gè)mode會(huì)被那些已經(jīng)描述作為“common”Modes的所有的RunLoop所監(jiān)測(cè)到贞远。
NSDefaultRunLoopMode:這個(gè)model處理輸入源除了NSConnection對(duì)象。
NSEventTrackingRunLoopMode
NSModalPanelRunLoopMode
UITrackingRunLoopMode

NSThread

NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[th start];

start方法會(huì)異步地生成一個(gè)新的線程并且在線程中調(diào)用NSThread對(duì)象的main方法笨忌。
start方法只能使用一次蓝仲,自此調(diào)用會(huì)引起crash


屏幕快照 2017-05-04 下午4.52.36.png

如果想多次使用這個(gè)線程怎么處理呢:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
    [_thread start];
    [self useThread];
    [self useThread];
}
- (void)startThread{
    NSLog(@"start");
}
- (void)useThread{
    [self performSelector:@selector(log) onThread:_thread withObject:nil waitUntilDone:NO];
}jiu
- (void)log{
    NSLog(@"log");
}

運(yùn)行之后發(fā)現(xiàn),log方法并沒(méi)有調(diào)用,原因是線程在執(zhí)行完startThread方法后袱结,便退出了亮隙。為了讓線程能夠再次使用,可以讓線程對(duì)應(yīng)的runLoop運(yùn)行起來(lái)垢夹。我們把上面的startThread方法修改一下:

- (void)startThread{
    NSRunLoop * runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];
}

然后運(yùn)行可以看到打印的log溢吻。
即使RunLoop開(kāi)始運(yùn)行,如果RunLoop中的modes為空果元,或者要執(zhí)行的mode里沒(méi)有item煤裙,那么RunLoop會(huì)直接在當(dāng)前l(fā)oop中返回,并進(jìn)入睡眠狀態(tài)噪漾。
如果把上面的[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];給去掉硼砰,也是沒(méi)有l(wèi)og的。
當(dāng)在另外一個(gè)線程執(zhí)行selector的時(shí)候欣硼,這個(gè)線程必須有一個(gè)運(yùn)行的runloop题翰。

如果在主線程里使用 initWithRequest:delegate:startImmediately:創(chuàng)建了一個(gè)NSURLConnetion,當(dāng)調(diào)用star方法時(shí)诈胜,這個(gè)NSURLConnetion會(huì)被安排到當(dāng)前的runloop里豹障,并且是默認(rèn)的mode。如果此時(shí)滑動(dòng)uiscrollview焦匈,由于主線程的runloop的mode被切換到UITrackingRunLoopMode,導(dǎo)致NSURLConnetion的代理方法無(wú)法回調(diào)血公。
所以需要用到scheduleInRunLoop: scheduleInRunLoop

 NSURL * url = [NSURL URLWithString:@"https://www.baidu.com"];
 NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
 [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
 [connection start];

RunLoop的管理不是全自動(dòng)的,你仍然需要開(kāi)啟運(yùn)行runLoop并且在適當(dāng)?shù)臅r(shí)候處里到來(lái)的事件缓熟。應(yīng)用的每一個(gè)線程都有對(duì)應(yīng)的RunLoop對(duì)象累魔,只有除了主線程之外的線程需要啟動(dòng)運(yùn)行它們的RunLoop,app的框架會(huì)自動(dòng)的配置和運(yùn)行主線程的RunLoop够滑。

RunLoop從兩個(gè)不同類(lèi)型的源里接收事件垦写。Input sources 異步地傳遞事件,這些事件通常來(lái)源于另外一個(gè)線程或應(yīng)用彰触。Timer Source同步地傳遞事件梯投,通常發(fā)生在預(yù)訂的時(shí)間或重復(fù)間隔。

Input Source異步地把事件提交到相應(yīng)的handles并且調(diào)用了runUntilDate:(與線程相關(guān)聯(lián)的RunLoop對(duì)象調(diào)用這個(gè)方法)來(lái)退出况毅。Timer Source提供事件到handles但不會(huì)引起RunLoop退出分蓖。
除了處理Input Source,RunLoop也會(huì)生成關(guān)于RunLoop運(yùn)行行為的通知尔许。注冊(cè)成為RunLoop的觀察者可以收到那些通知并且使用它們?cè)诰€程上做一些額外的處理么鹤。
RunLoop的Mode是一個(gè)集合,它包括了輸入源母债,timers午磁,observers(會(huì)受到通知)。每次你運(yùn)行了你的RunLoop毡们,你需要為RunLoop指定一個(gè)mode迅皇。
你可以自定義一個(gè)mode,給mode的name設(shè)置一個(gè)自定義的字符串衙熔。你必須要給自定義的mode添加source登颓,timers和observes。

mode Name Description
Default NSDefaultRunLoopMode 大部分情況下红氯,你需要使用這個(gè)mode來(lái)開(kāi)啟你的RunLoop并且配置你的輸入源
Common modes NSRunLoopCommonModes 這是一組可配置的常用模式框咙,將輸入源于此模式相關(guān)聯(lián)還將其與組中的每種模式向關(guān)聯(lián)。默認(rèn)情況下痢甘,此設(shè)置包括了默認(rèn)模式和事件追蹤模式喇嘱。

Input Source

Input Source異步地傳遞事件到線程里。Input Source通常有兩類(lèi):一種是基于Port的input source塞栅,用于檢測(cè)應(yīng)用程序的Mach ports者铜;一種是自定義的input source,用于監(jiān)測(cè)自定義的事件源放椰。這兩個(gè)源唯一的區(qū)別是如何發(fā)出信號(hào)的作烟,基于Port的源由內(nèi)核自動(dòng)發(fā)出信號(hào),自定義源必須從另一個(gè)線程手動(dòng)發(fā)出信號(hào)砾医。
當(dāng)你創(chuàng)建一個(gè)intput source拿撩,你可以把它分配到runloop的多個(gè)mode中。如果一個(gè)intput source不在runloop當(dāng)下的監(jiān)測(cè)到的mode中如蚜,他所生成的任何事件都會(huì)被掛起知道runloop切換到正確的mode中压恒。

什么時(shí)候使用runloop

應(yīng)用的主線程是默認(rèn)開(kāi)啟了runloop。當(dāng)我們使用自定義的線程時(shí)错邦,如果想要向和線程進(jìn)行更多的交互涎显,想要線程處于活躍狀態(tài),就要考慮使用runloop兴猩。
如果使用到了以下操作期吓,需要使用runloop:

  • 使用了port或自定義的inputsource來(lái)與其他線程通信;
  • 在線程中使用了定時(shí)器倾芝;
  • 在線程中使用了performSelector:
  • 保持線程執(zhí)行周期性的任務(wù)讨勤。

RunLoop對(duì)象

在run一個(gè)runloop之前,必須要至少添加一個(gè)inputsource晨另,否則run后runloop會(huì)立即退出潭千。
除了添加inputsource,也可以添加observes來(lái)監(jiān)測(cè)runloop的運(yùn)行狀態(tài)借尿。你可以使用CFRunLoopObserveRef來(lái)創(chuàng)建對(duì)象并使用CFRunLoopAddObserve函數(shù)來(lái)添加刨晴。RunLoop Observe必須使用Core Foundation框架來(lái)創(chuàng)建屉来。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狈癞,隨后出現(xiàn)的幾起案子茄靠,更是在濱河造成了極大的恐慌,老刑警劉巖蝶桶,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慨绳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡真竖,警方通過(guò)查閱死者的電腦和手機(jī)脐雪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)恢共,“玉大人战秋,你說(shuō)我怎么就攤上這事√志拢” “怎么了获询?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拐袜。 經(jīng)常有香客問(wèn)我吉嚣,道長(zhǎng),這世上最難降的妖魔是什么蹬铺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任尝哆,我火速辦了婚禮,結(jié)果婚禮上甜攀,老公的妹妹穿的比我還像新娘秋泄。我一直安慰自己,他們只是感情好规阀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布恒序。 她就那樣靜靜地躺著,像睡著了一般谁撼。 火紅的嫁衣襯著肌膚如雪歧胁。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,929評(píng)論 1 290
  • 那天厉碟,我揣著相機(jī)與錄音喊巍,去河邊找鬼。 笑死箍鼓,一個(gè)胖子當(dāng)著我的面吹牛崭参,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播款咖,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼何暮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奄喂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起海洼,我...
    開(kāi)封第一講書(shū)人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤跨新,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后贰军,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體玻蝌,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蟹肘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年词疼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帘腹。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贰盗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阳欲,到底是詐尸還是另有隱情舵盈,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布球化,位于F島的核電站秽晚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏筒愚。R本人自食惡果不足惜赴蝇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巢掺。 院中可真熱鬧句伶,春花似錦、人聲如沸陆淀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)轧苫。三九已至楚堤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間含懊,已是汗流浹背钾军。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绢要,地道東北人吏恭。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像重罪,于是被迫代替她去往敵國(guó)和親樱哼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哀九,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

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