一般來(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的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
如果想多次使用這個(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)建屉来。