Runloop 詳解
參考鏈接:
概念
runloop :是管理和處理事件和消息的對象
帚湘。
-
并提供了一個(gè)入口函數(shù)來執(zhí)行 event loop 的邏輯。
線程執(zhí)行該函數(shù)后大诸,就會(huì)一直處于該函數(shù)內(nèi)部的
接受消息->等待->處理
的循環(huán)中捅厂,知道循環(huán)結(jié)束(傳入 quit 消息),函數(shù)返回恒傻。 -
iOS 中,提供了2個(gè) runloop 對象:
NSRunLoop:基于
CFRunLoopRef
的封裝建邓,提供了面向?qū)ο蟮?API盈厘,但是這些 api 不是線程安全的官边。CFRunLoopRef:是在 CoreFoundation 框架內(nèi)的,提供了純 c 函數(shù)的 API注簿,這些 api 都是線程安全的契吉。
RunLoop 和線程
- 線程:pthread_t 和 NSThread 是一一對應(yīng)的。
- CFRunLoop 是基于 pthread 來管理的诡渴。
apple 不允許直接創(chuàng)建 RunLoop捐晶,只提供了2個(gè)自動(dòng)獲取的函數(shù):CFRunLoopGetMain()、CFRunLoopGetCurrent().
內(nèi)部實(shí)現(xiàn):
//全局的 Dictionary惑灵,key 是 pthread_t,value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
//訪問 loosDic 時(shí)的鎖
static CFSpinLock_t loopsLock;
//獲取一個(gè) pthread 對應(yīng)的 RunLoop
CFRunLoopRef _CFRunLoopGet(pthread_t thread){
OSSpinLockLock(&loopsLock);
if(!loopsDic)
{
//第一次進(jìn)入,初始化全局 Dic眼耀,并先為主線程創(chuàng)建一個(gè) runloop
loosDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic,pthread_main_thread_np(),mainLoop);
}
//直接從 dic 里獲取
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic,thread);
if(!loop)
{
//取不到時(shí),new 一個(gè)
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic,thread,loop);
//注冊一個(gè)回調(diào)哮伟,當(dāng)線程銷毀時(shí)干花,順便也銷毀對應(yīng)的 runloop
_CFSetTSD(...,thread,loop,__CFFinalizeRunLoop);
}
OSSpinLockUnLock(&oopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain(){
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent(){
return _CFRunLoopGet(pthread_self());
}
結(jié)論:從上面的代碼可以看出,線程和 RunLoop 之間是一一對應(yīng)的池凄,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒有 RunLoop谅辣,如果你不主動(dòng)獲取修赞,那它一直都不會(huì)有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí)勾邦,RunLoop 的銷毀是發(fā)生在線程結(jié)束時(shí)。你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程除外)割择。
RunLoop 對外的接口
-
在 CF 中關(guān)于 RunLoop 的有5個(gè)類:
CFRunLoopRef:
CFRunLoopModeRef
-
CFRunLoopSourceRef:事件產(chǎn)生的地方
Source有兩個(gè)版本:Source0 和 Source1眷篇。
-
Source0 只包含了一個(gè)回調(diào)(函數(shù)指針)荔泳,它并不能主動(dòng)觸發(fā)事件。
處理如 UIEvent玛歌,CFSoket 等事件昧港。
使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source)支子,將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop值朋,讓其處理這個(gè)事件叹侄。
-
Source1 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針)昨登,被用于通過內(nèi)核和其他線程相互發(fā)送消息趾代。這種 Source 能主動(dòng)喚醒 RunLoop 的線程.
Mach port驅(qū)動(dòng)丰辣,CFMachport,CFMessagePort
-
-
CFRunLoopTimerRef:基于時(shí)間的觸發(fā)器.<mark>NSTimer是對RunLoopTimer的封裝.
它和 NSTimer 是toll-free bridged 的笙什,可以混用尿褪。其包含一個(gè)時(shí)間長度和一個(gè)回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊對應(yīng)的時(shí)間點(diǎn)顿仇,當(dāng)時(shí)間點(diǎn)到時(shí)淘正,RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào).
-
CFRunLoopObserverRef:觀察者
Cocoa框架中很多機(jī)制比如CAAnimation等都是由RunLoopObserver觸發(fā)的 .
每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針)臼闻,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí)鸿吆,觀察者就能通過回調(diào)接受到這個(gè)變化述呐〕痛荆可以觀測的時(shí)間點(diǎn)有以下幾個(gè):
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){ KCFRunLoopEntry,//即將進(jìn)入 loop KCFRunLoopBeforeTimers,//即將處理 timer KCFRunLoopBeforeSources,//即將處理 source KCFRunLoopBeforeWaiting代虾,//即將進(jìn)入休眠 KCFRunLoopAfterWaiting,//剛從休眠中喚醒 KCFRunLoopExit//即將退出loop }
-
一個(gè) RunLoop 包含若干個(gè) Mode激蹲,每個(gè) Mode 又包含若干個(gè)
Source/Timer/Observer
(統(tǒng)稱 mode item棉磨。同時(shí)一個(gè) mode item 也可被加入不同的 mode 中)学辱。每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode策泣,這個(gè)Mode被稱作 CurrentMode衙傀。
如果需要切換 Mode,只能退出 Loop萨咕,再重新指定一個(gè) Mode 進(jìn)入。這樣做主要是為了分隔開不同組的
Source/Timer/Observer
任洞,讓其互不影響蓄喇。如果一個(gè) mode 中一個(gè) item 都沒有交掏,則 RunLoop 會(huì)直接退出妆偏,不進(jìn)入循環(huán)盅弛。
RunLoop 的 mode
NSDefaultRunLoopMode:默認(rèn)钱骂,空閑狀態(tài)
UITrackingRunLoopMode:ScrollView滑動(dòng)時(shí)
UIInitializationRunLoopMode:啟動(dòng)時(shí)
-
NSRunLoopCommonModes:Mode集合.
<mark>Timer計(jì)時(shí)會(huì)被scrollView的滑動(dòng)影響的問題可以通過將timer添加到NSRunLoopCommonModes來解決.
涉及到 RunLoop 的技術(shù):
- 系統(tǒng)級(jí):GCD挪鹏、mach kernel、block讨盒、pthread
- 應(yīng)用層:NSTimer解取、UIEvent、Autorelease禀苦、NSObject(NSDelayPerform、NSThreadPerformAddition)遂鹊、CADisplayLink振乏、CATranstion秉扑、CAAinimation慧邮、NSPort、NSURLConnection误澳、AFNetworking(在開啟新線程中添加自己的 runloop 監(jiān)聽事件)
RunLoop 在 Main Thread 堆棧中所處的位置
堆棧最底層是start(dyld),往上依次是main脓匿,UIApplication(main.m) -> GSEventRunModal(Graphic Services) -> RunLoop(包含CFRunLoopRunSpecific淘钟,CFRunLoopRun陪毡,__CFRunLoopDoSouces0米母,__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION) -> Handle Touch Event
使用 RunLoop 案例
- AFNetworking
使用NSOperation+NSURLConnection并發(fā)模型都會(huì)面臨NSURLConnection下載完成前線程退出導(dǎo)致NSOperation對象接收不到回調(diào)的問題毡琉。AFNetWorking解決這個(gè)問題的方法是按照官方的guid:
https://developer.apple.com...
上寫的NSURLConnection的delegate方法需要在connection發(fā)起的線程runloop中調(diào)用,于是AFNetWorking直接借鑒了Apple自己的一個(gè)Demohttps://developer.apple.com...的實(shí)現(xiàn)方法單獨(dú)起一個(gè)global thread桅滋,內(nèi)置一個(gè)runloop慧耍,所有的connection都由這個(gè)runloop發(fā)起,回調(diào)也是它接收芍碧,不占用主線程,也不耗CPU資源号俐。
+ (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;
}
-
TableView中實(shí)現(xiàn)平滑滾動(dòng)延遲加載圖片
利用CFRunLoopMode的特性泌豆,可以將圖片的加載放到NSDefaultRunLoopMode的mode里吏饿,這樣在滾動(dòng)UITrackingRunLoopMode這個(gè)mode時(shí)不會(huì)被加載而影響到。(意思就是滾動(dòng)時(shí)猪落,不進(jìn)行圖片的下載操作贞远,額,蓝仲,這個(gè)設(shè)計(jì)不是太合理)
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
接到程序崩潰時(shí)的信號(hào)進(jìn)行自主處理例如彈出提示等
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
異步測試
- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
__block Boolean fulfilled = NO;
void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
fulfilled = block();
if (fulfilled) {
CFRunLoopStop(CFRunLoopGetCurrent());
}
};
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// Run!
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
return fulfilled;
}