先不扯什么概念茬贵,因為自己之前對概念理解不深刻,只有在項目中真正用到了才能真正體會移袍。
使用場景一:NSTimer 倒計時
- 倒計時有兩種創(chuàng)建形式
- 第一種 需要手動將定時器添加到NSRunLoop
[NSThread detachNewThreadSelector:@selector(startSteamTimer) toTarget:self withObject:nil];
- (NSTimer *) startSteamTimer{
return [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
//這樣的run方法永遠(yuǎn)不會調(diào)用必須 加入下面的代碼
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];
//若在非主線程 需要自己啟動RunLoop [[NSRunLoop currentRunLoop] run]
- 第二種 創(chuàng)建NSTimer 自動添加NSRunLoop
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:
@selector(_steamTimerAction) userInfo:nil repeats:YES];
使用場景二:用FTP 協(xié)議上傳和下載文件時 用到了NSRunLoop
CFReadStreamRef readStreamRef = CFReadStreamCreateWithFTPURL(NULL, ( __bridge CFURLRef) url);
CFReadStreamSetProperty(readStreamRef,
kCFStreamPropertyFTPAttemptPersistentConnection,
kCFBooleanFalse);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUsePassiveMode, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPFetchResourceInfo, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUserName, (__bridge CFStringRef) self.ftpUsername);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPPassword, (__bridge CFStringRef) self.ftpPassword);
//
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPProxy, kCFBooleanTrue);
//
self.dataStream = ( __bridge_transfer NSInputStream *) readStreamRef;
self.dataStream.delegate = self;
if (self.dataStream == nil) {
[self.delegate ftpError:self withErrorCode:FTPClientCantReadStream];
}
//這里是重點(diǎn)
[self performSelector:@selector(scheduleInCurrentThread:)
onThread:[[self class] networkThread]
withObject:self.dataStream
waitUntilDone:YES];
// [self.dataStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.dataStream open];
#pragma thread management
+ (NSThread *)networkThread {
static NSThread *networkThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
networkThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkThreadMain:)
object:nil];
[networkThread start];
});
return networkThread;
}
+ (void)networkThreadMain:(id)unused {
do {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
}
- (void)scheduleInCurrentThread:(NSStream*)aStream
{
[aStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
}
看了下方法的含義:Unless the client is polling the stream, it is responsible for ensuring that the stream is scheduled on at least one run loop and that at least one of the run loops on which the stream is scheduled is being run
確保流至少在一個運(yùn)行循環(huán)上調(diào)度解藻,并且流調(diào)度所在的至少一個運(yùn)行循環(huán)正在運(yùn)行
在流對象放入run loop且有流事件(有可讀數(shù)據(jù))發(fā)生時,流對象會向代理對象發(fā)送stream:handleEvent:消息葡盗。在打開流之前螟左,我們需要調(diào)用流對象的scheduleInRunLoop:forMode:方法,這樣做可以避免在沒有數(shù)據(jù)可讀時阻塞代理對象的操作觅够。我們需要確保的是流對象被放入正確的run loop中胶背,即放入流事件發(fā)生的那個線程的run loop中。
那為什么要RunLoop
字面意思運(yùn)行的循環(huán)蔚约,就像操作系統(tǒng)一樣奄妨,手機(jī)一有電話就有反應(yīng),系統(tǒng)里面在循環(huán)“跑圈”苹祟,也借助do-while 死循環(huán)理解
- RunLoop 實際上是一個對象砸抛,這個對象在循環(huán)中用來處理程序運(yùn)行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件树枫、定時器事件直焙、Selector事件),從而保持程序的持續(xù)運(yùn)行砂轻。
- RunLoop 在沒有事件處理的時候奔誓,會使線程進(jìn)入睡眠模式,從而節(jié)省 CPU 資源搔涝,提高程序性能厨喂。
RunLoop 和線程
RunLoop 和線程是息息相關(guān)的,我們知道線程的作用是用來執(zhí)行特定的一個或多個任務(wù)庄呈,在默認(rèn)情況下蜕煌,線程執(zhí)行完之后就會退出,就不能再執(zhí)行任務(wù)了诬留。這時我們就需要采用一種方式來讓線程能夠不斷地處理任務(wù)斜纪,并不退出。所以文兑,我們就有了 RunLoop盒刚。
一條線程對應(yīng)一個RunLoop對象绿贞,每條線程都有唯一一個與之對應(yīng)的 RunLoop 對象。
RunLoop 并不保證線程安全籍铁。我們只能在當(dāng)前線程內(nèi)部操作當(dāng)前線程的 RunLoop 對象靠柑,而不能在當(dāng)前線程內(nèi)部去操作其他線程的 RunLoop 對象方法。
RunLoop 對象在第一次獲取 RunLoop 時創(chuàng)建吓懈,銷毀則是在線程結(jié)束的時候。
主線程的 RunLoop 對象系統(tǒng)自動幫助我們創(chuàng)建好了(原理如 1.3 所示)耻警,而子線程的 RunLoop對象需要我們主動創(chuàng)建和維護(hù)。
官方模型
通過 Input sources(輸入源)和 Timer sources(定時源)兩種來源等待接受事件甘穿;然后對接受到的事件通知線程進(jìn)行處理腮恩,并在沒有事件的時候讓線程進(jìn)行休息
RunLoop 相關(guān)類
下面我們來了解一下Core Foundation框架下關(guān)于 RunLoop 的 5 個類,只有弄懂這幾個類的含義温兼,我們才能深入了解 RunLoop 的運(yùn)行機(jī)制募判。
CFRunLoopRef:代表 RunLoop 的對象
CFRunLoopModeRef:代表 RunLoop 的運(yùn)行模式
CFRunLoopSourceRef:就是 RunLoop 模型圖中提到的輸入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型圖中提到的定時源
CFRunLoopObserverRef:觀察者,能夠監(jiān)聽 RunLoop 的狀態(tài)改變
一個RunLoop對象(CFRunLoopRef)中包含若干個運(yùn)行模式(CFRunLoopModeRef)届垫。而每一個運(yùn)行模式下又包含若干個輸入源(CFRunLoopSourceRef)装处、定時源(CFRunLoopTimerRef)误债、觀察者(CFRunLoopObserverRef)妄迁。
- 每次 RunLoop 啟動時登淘,只能指定其中一個運(yùn)行模式(CFRunLoopModeRef),這個運(yùn)行模式(CFRunLoopModeRef)被稱作當(dāng)前運(yùn)行模式(CurrentMode)形帮。
- 如果需要切換運(yùn)行模式(CFRunLoopModeRef)周叮,只能退出當(dāng)前 Loop仿耽,再重新指定一個運(yùn)行模式(CFRunLoopModeRef)進(jìn)入。
- 這樣做主要是為了分隔開不同組的輸入源(CFRunLoopSourceRef)项贺、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef)棕叫,讓其互不影響
CFRunLoopRef 類 代表 RunLoop 的對象
獲取方式
- Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的 RunLoop 對象
CFRunLoopGetMain(); // 獲得主線程的 RunLoop 對象
當(dāng)然,在Foundation 框架下獲取 RunLoop 對象類的方法如下:
- Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的 RunLoop 對象
[NSRunLoop mainRunLoop]; // 獲得主線程的 RunLoop 對象
CFRunLoopModeRef. 運(yùn)行模式
1疗认、kCFRunLoopDefaultMode:App的默認(rèn)運(yùn)行模式伏钠,通常主線程是在這個運(yùn)行模式下運(yùn)行
2、UITrackingRunLoopMode:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動缎浇,保證界面滑動時不受其他Mode影響)
3赴肚、UIInitializationRunLoopMode:在剛啟動App時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
4尊蚁、GSEventReceiveRunLoopMode:接受系統(tǒng)內(nèi)部事件横朋,通常用不到
5、kCFRunLoopCommonModes:偽模式琴锭,不是一種真正的運(yùn)行模式(后邊會用到)
其中kCFRunLoopDefaultMode决帖、UITrackingRunLoopMode、kCFRunLoopCommonModes是我們開發(fā)中需要用到的模式
CFRunLoopTimerRef
CFRunLoopTimerRef是定時源(RunLoop模型圖中提到過)地回,理解為基于時間的觸發(fā)器刻像,基本上就是NSTimer(哈哈,這個理解就簡單了吧)
下面我們來演示下CFRunLoopModeRef和CFRunLoopTimerRef結(jié)合的使用用法细睡,從而加深理解
UITextView *tv = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width/2, self.view.frame.size.height/2)];
tv.text =@"放很多字出現(xiàn)滾動條";
tv.backgroundColor = UIColor.redColor;
[self.view addSubview:tv];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];
然后運(yùn)行溜徙,這個時候我們發(fā)現(xiàn)如何我們不拖動UITextView的滾動條犀填,定時器會穩(wěn)定的每隔2秒調(diào)用run方法打印
但拖動的時候嗓违,我們發(fā)現(xiàn)沒有打印
這是因為:
當(dāng)我們不做任何操作的時候,RunLoop處于NSDefaultRunLoopMode下
而當(dāng)我們拖動UITextView 的時候比庄,RunLoop就結(jié)束NSDefaultRunLoopMode乏盐,切換到了UITrackingRunLoopMode模式下,這種模式下沒有添加NSTimer,所有我們定時器不工作了
但當(dāng)我送松開滾動條神凑,RunLoop就結(jié)束UITrackingRunLoopMode模式何吝,又切換回NSDefaultRunLoopMode模式,所以NSTimer就又開始正常工作了你可以試著將上述代碼中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];語句換為[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];瓣喊,也就是將定時器添加到當(dāng)前RunLoop的UITrackingRunLoopMode下黔酥,你就會發(fā)現(xiàn)定時器只會在拖動Text View的模式下工作,而不做操作的時候定時器就不工作棵帽。
那難道我們就不能在這兩種模式下讓NSTimer都能正常工作嗎渣玲?當(dāng)然可以,這就用到了我們之前說過的偽模式(kCFRunLoopCommonModes)逾苫,這其實不是一種真實的模式枚钓,而是一種標(biāo)記模式,意思就是可以在打上Common Modes標(biāo)記的模式下運(yùn)行
CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(RunLoop模型圖中提到過),CFRunLoopSourceRef有兩種分類方法指煎。
- 第一種按照官方文檔來分類(就像RunLoop模型圖中那樣):
Port-Based Sources(基于端口)
Custom Input Sources(自定義)
Cocoa Perform Selector Sources
- 第二種按照函數(shù)調(diào)用棧來分類:
Source0 :非基于Port
Source1:基于Port,通過內(nèi)核和其他線程通信威始,接收像街、分發(fā)系統(tǒng)事件
這兩種分類方式其實沒有區(qū)別,只不過第一種是通過官方理論來分類脓斩,第二種是在實際應(yīng)用中通過調(diào)用函數(shù)來分類畴栖。
備注:斷點(diǎn)可以看到函數(shù)調(diào)用棧“Source0”
CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者燎猛,用來監(jiān)聽RunLoop的狀態(tài)改變
CFRunLoopObserverRef可以監(jiān)聽的狀態(tài)改變有以下幾種:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒:64
kCFRunLoopExit = (1UL << 7), // 即將從Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 監(jiān)聽全部狀態(tài)改變
};
- 1照皆、在ViewController.m中添加如下代碼
// 創(chuàng)建觀察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"監(jiān)聽到RunLoop發(fā)生改變---%zd",activity);
});
// 添加觀察者到當(dāng)前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放observer膜毁,最后添加完需要釋放掉
CFRelease(observer);
可以看到RunLoop的狀態(tài)在不斷的改變,最終變成了狀態(tài) 32爽茴,也就是即將進(jìn)入睡眠狀態(tài)室奏,說明RunLoop之后就會進(jìn)入睡眠狀態(tài)
RunLoop原理
好了,五個類都講解完了胧沫,下邊開始放大招了绒怨。這下我們就可以來理解RunLoop的運(yùn)行邏輯了。
這張圖對于我們的理解RunLoop來說太有幫助了犬金,下邊我們可以理解RunLoop邏輯
每次在開啟RunLoop的時候,所在線程所在線程的RunLoop會自動處理之前未處理的事件晚顷,并且通知相關(guān)的觀察者
具體的順序如下:
- 1该默、通知觀察者RunLoop已經(jīng)啟動
- 2、通知觀察者即將要開始的定時器
- 3栓袖、通知觀察者任何即將啟動的非基于端口的源
- 4啟動任何準(zhǔn)備好的非基于端口的源
- 5裹刮、如果基于端口的源準(zhǔn)備好并處于等待狀態(tài),立即啟動必指;并進(jìn)入步驟9
- 6塔橡、通知觀察者線程進(jìn)入休眠狀態(tài)
- 7、將線程置于休眠知道任一下面的事件發(fā)生:
某一事件到達(dá)基于端口的源
定時器啟動
RunLoop設(shè)置的時間已經(jīng)超時
RunLoop被顯示喚醒
- 8葛家、通知觀察者線程將被喚醒
- 9癞谒、處理未處理的事件
如果用戶定義的定時器啟動,處理定時器事件并重啟RunLoop弹砚。進(jìn)入步驟2
如果輸入源啟動桌吃,傳遞相應(yīng)的消息
如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop茅诱。進(jìn)入步驟2
- 10瑟俭、通知觀察者RunLoop結(jié)束。
runLoop一般的使用
我們在開發(fā)應(yīng)用程序的過程中摆寄,如果后臺操作特別頻繁坯门,經(jīng)常會在子線程做一些好事的操作(下載文件田盈、后臺播放音樂等)缴阎,我們最好能讓這條線程永遠(yuǎn)常駐內(nèi)存
那么怎么做呢简软?其實開頭案例我有使用
添加一條常駐內(nèi)存的子線程,在該線程的RunLoop下添加一Sources,開啟RunLoop
具體實現(xiàn)過程如下:
_threadP = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[_threadP start];
}
- (void)run1 {
NSLog(@"runn1");
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"runloop沒啟動成功");
NSLog(@"---run:%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(run2222) onThread:self.threadP withObject:nil waitUntilDone:NO];
NSLog(@"runw222");
}
- (void)run2222 {
NSLog(@"我在這個線程想干啥就可以干啥");
NSLog(@"---run:%@",[NSThread currentThread]);
}
運(yùn)行之后發(fā)現(xiàn)打印了----run1-----建炫,而未開啟RunLoop則未打印