Runloop基本概要
- Runloop就是一個do…while 循環(huán)
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
只有主線程默認打開了Runloop苔悦,子線程需要手動打開
Runloop一共有5個mode
银酗,其中給我們使用的有三個
- NSDefaultRunLoopMode 默認模式
- UITrackingRunLoopMode scrollView進入拖拽的時候會進入的模式
- NSRunLoopCommonModes 占位模式
dispatch_async 函數(shù)分發(fā)到全局隊列不一定會新建線程執(zhí)行任務
- dispatch_async 函數(shù)分發(fā)到全局隊列不一定會新建線程執(zhí)行任務扩然,全局隊列底層有一個的線程池共耍,如果線程池滿了余佛,那么后續(xù)的任務會被 block 住绩社,等待前面的任務執(zhí)行完成者春,才會繼續(xù)執(zhí)行膘侮。如果線程池中的線程長時間不結(jié)束,后續(xù)堆積的任務會越來越多,此時就會存在 APP crash的風險租悄。
例如:
fileprivate func dispatchTest1() {
for i in 0..<10000 {
DispatchQueue.global().async {
self.dispatchTask(i)
}
}
}
fileprivate func dispatchTask(_ index: NSInteger) {
//模擬耗時操作谨究,比如DB,網(wǎng)絡,文件讀寫等等
sleep(30)
}
以上邏輯會造成凍屏泣棋,測試時可以伴隨前后臺切換胶哲,crash幾率增大。
下面做一下分析:
DISPATCH_NOINLINE
static void
_dispatch_queue_wakeup_global_slow(dispatch_queue_t dq, unsigned int n)
{
dispatch_root_queue_context_t qc = dq->do_ctxt;
uint32_t i = n;
int r;
_dispatch_debug_root_queue(dq, __func__);
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init);#if HAVE_PTHREAD_WORKQUEUES#if DISPATCH_USE_PTHREAD_POOL
if (qc->dgq_kworkqueue != (void*)(~0ul))#endif
{
_dispatch_root_queue_debug("requesting new worker thread for global "
"queue: %p", dq);#if DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
if (qc->dgq_kworkqueue) {
pthread_workitem_handle_t wh;
unsigned int gen_cnt;
do {
r = pthread_workqueue_additem_np(qc->dgq_kworkqueue,
_dispatch_worker_thread4, dq, &wh, &gen_cnt);
(void)dispatch_assume_zero(r);
} while (--i);
return;
}#endif//DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK#if HAVE_PTHREAD_WORKQUEUE_SETDISPATCH_NP
if (!dq->dq_priority) {
r = pthread_workqueue_addthreads_np(qc->dgq_wq_priority,
qc->dgq_wq_options, (int)i);
(void)dispatch_assume_zero(r);
return;
}#endif#if HAVE_PTHREAD_WORKQUEUE_QOS
r = _pthread_workqueue_addthreads((int)i, dq->dq_priority);
(void)dispatch_assume_zero(r);#endif
return;
}#endif // HAVE_PTHREAD_WORKQUEUES#if DISPATCH_USE_PTHREAD_POOL
dispatch_pthread_root_queue_context_t pqc = qc->dgq_ctxt;
if (fastpath(pqc->dpq_thread_mediator.do_vtable)) {
while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
if (!--i) {
return;
}
}
}
uint32_t j, t_count;
// seq_cst with atomic store to tail t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
do {
if (!t_count) {
_dispatch_root_queue_debug("pthread pool is full for root queue: "
"%p", dq);
return;
}
j = i > t_count ? t_count : I;
} while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
t_count - j, &t_count, acquire));
pthread_attr_t *attr = &pqc->dpq_thread_attr;
pthread_t tid, *pthr = &tid;#ifDISPATCH_ENABLE_PTHREAD_ROOT_QUEUES
if (slowpath(dq == &_dispatch_mgr_root_queue)) {
pthr = _dispatch_mgr_root_queue_init();
}#endif
do {
_dispatch_retain(dq);
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--j);#endif // DISPATCH_USE_PTHREAD_POOL}
參看 GCD 源碼我們可以看到全局隊列的相關源碼
對于執(zhí)行的任務來說潭辈,所執(zhí)行的線程具體是哪個線程鸯屿,則是通過 GCD 的線程池(Thread Pool)來進行調(diào)度,正如Concurrent Programming: APIs and Challenges文章里給的示意圖所示:
上面貼的源碼把敢,我們關注如下的部分:
其中有一個用來記錄線程池大小的字段 dgq_thread_pool_size寄摆。這個字段標記著GCD線程池的大小。摘錄上面源碼的一部分:
uint32_t j, t_count;
// seq_cst with atomic store to tail t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
do {
if (!t_count) {
_dispatch_root_queue_debug("pthread pool is full for root queue: "
"%p", dq);
return;
}
j = i > t_count ? t_count : i;
} while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
t_count - j, &t_count, acquire));
也就是說:
全局隊列的底層是一個線程池修赞,向全局隊列中提交的 block婶恼,都會被放到這個線程池中執(zhí)行,如果線程池已滿柏副,后續(xù)再提交 block 就不會再重新創(chuàng)建線程勾邦。這就是為什么 Demo 會造成卡頓甚至凍屏的原因。
避免使用 GCD Global 隊列創(chuàng)建 Runloop 常駐線程
在做網(wǎng)路請求時我們常常創(chuàng)建一個 Runloop 常駐線程用來接收搓扯、響應后續(xù)的服務端回執(zhí)检痰,比如NSURLConnection、AFNetworking等等锨推,我們可以稱這種線程為 Runloop 常駐線程铅歼。
正如上文所述,用 GCD Global 隊列創(chuàng)建線程進行耗時操作是存在風險的换可。那么我們可以試想下椎椰,如果這個耗時操作變成了 runloop 常駐線程,會是什么結(jié)果沾鳄?下面做一下分析:
先介紹下 Runloop 常駐線程的原理慨飘,在開發(fā)中一般有兩種用法:
- 單一 Runloop 常駐線程:在 APP 的生命周期中開啟了唯一的常駐線程來進行網(wǎng)絡請求,常用于網(wǎng)絡庫译荞,或者有維持長連接需求的庫瓤的,比如: AFNetworking.
- 多個 Runloop 常駐線程:每進行一次網(wǎng)絡請求就開啟一條 Runloop 常駐線程,這條線程的生命周期的起點是網(wǎng)絡請求開始吞歼,終點是網(wǎng)絡請求結(jié)束圈膏,或者網(wǎng)絡請求超時。
單一 Runloop 常駐線程
以 AFNetworking 為例篙骡,AFURLConnectionOperation 這個類是基于 NSURLConnection 構(gòu)建的稽坤,其希望能在后臺線程接收 Delegate 回調(diào)丈甸。為此 AFNetworking 單獨創(chuàng)建了一個線程,并在這個線程中啟動了一個 RunLoop:
+ (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;
}
多個 Runloop 常駐線程
我們模擬了一個場景:假設所有的網(wǎng)絡請求全部超時尿褪,或者服務端根本不響應睦擂,然后網(wǎng)絡庫超時檢測機制的做法:
@interface Foo() {
NSRunLoop *_runloop;
NSTimer *_timeoutTimer;
NSTimeInterval _timeoutInterval;
dispatch_semaphore_t _sem;
}
@end
@implementation Foo
- (instancetype)init {
if (!(self = [super init])) {
return nil;
}
_timeoutInterval = 1 ;
_sem = dispatch_semaphore_create(0);
// Do any additional setup after loading the view, typically from a nib.
return self;
}
- (id)test {
// 第一種方式:
// NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint0:) object:nil];
// [networkRequestThread start];
//第二種方式:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self networkRequestThreadEntryPoint0:nil];
});
dispatch_semaphore_wait(_sem, DISPATCH_TIME_FOREVER);
return @(YES);
}
- (void)networkRequestThreadEntryPoint0:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"CYLTest"];
_runloop = [NSRunLoop currentRunLoop];
[_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(stopLoop) userInfo:nil repeats:NO];
[_runloop addTimer:_timeoutTimer forMode:NSRunLoopCommonModes];
[_runloop run];//在實際開發(fā)中最好使用這種方式來確保能runloop退出,做雙重的保障[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutInterval+5)]];
}
}
- (void)stopLoop {
CFRunLoopStop([_runloop getCFRunLoop]);
dispatch_semaphore_signal(_sem);
}
@end
如果:
for (int i = 0; i < 300 ; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[[Foo new] test];
NSLog(@"????類名與方法名:%@(在第%@行)杖玲,描述:%@", @(PRETTY_FUNCTION), @(LINE), @"");
});
}
測試時可以伴隨前后臺切換顿仇,crash幾率更大。
其中我們采用了 GCD 全局隊列的方式來創(chuàng)建常駐線程摆马,因為在創(chuàng)建時可能已經(jīng)出現(xiàn)了全局隊列的線程池滿了的情況夺欲,所以 GCD 派發(fā)的任務,無法執(zhí)行今膊,而且我們把超時檢測的邏輯放進了這個任務中,所以導致的情況就是伞剑,有很多任務的超時檢測功能失效了斑唬。此時就只能依賴于服務端響應來結(jié)束該任務(服務端響應能結(jié)束該任務的邏輯在 Demo 中未給出),但是如果再加之服務端不響應黎泣,那么任務就永遠不會結(jié)束恕刘。后續(xù)的網(wǎng)絡請求也會就此 block 住,造成 crash抒倚。
如果我們把 GCD 全局隊列換成 NSThread 的方式褐着,那么就可以保證每次都會創(chuàng)建新的線程。
Demo 的這種模擬可能比較極端托呕,但是如果你維護的是一個像 AFNetworking 這樣的一個網(wǎng)絡庫含蓉,你會放心把創(chuàng)建常駐線程這樣的操作交給 GCD 全局隊列嗎?因為整個 APP 是在共享一個全局隊列的線程池项郊,那么如果 APP 把線程池沾滿了馅扣,甚至線程池長時間占滿且不結(jié)束,那么 AFNetworking 就自然不能再執(zhí)行任務了着降,所以我們看到差油,即使是只會創(chuàng)建一條常駐線程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局隊列這種方式任洞。
+ (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;
}
正如你所看到的蓄喇,沒有任何一個庫會用 GCD 全局隊列來創(chuàng)建常駐線程,而你也應該
避免使用 GCD Global 隊列來創(chuàng)建 Runloop 常駐線程交掏。
定時器
let timer = Timer(timeInterval: 1, repeats: true) { (timer) in
print("hello")
}
RunLoop.main.add(timer, forMode: .defaultRunLoopMode)
RunLoop.current.add(timer, forMode: .commonModes)
小知識
RunLoop.current
RunLoop.main
在主線程中上面獲取的Runloop是同一個妆偏,在子線程中不同。
RunLoop.main 表示獲取主線程的Runloop對象
RunLoop.current 表示獲取當前線程的Runloop對象
- NULL 空指針
- nil 空對象
啟動RunLoop的方式
通過RunLoop.current或者CFRunLoopGetCurrent()可以獲取當前線程的runloop耀销。
啟動一個runloop有以下三種方法:
open func run()
open func run(until limitDate: Date)
open func run(mode: RunLoopMode, before limitDate: Date) -> Bool
這三種方式無論通過哪一種方式啟動runloop楼眷,如果沒有一個輸入源或者timer附加于runloop上铲汪,runloop就會立刻退出。
(1) 第一種方式罐柳,runloop會一直運行下去掌腰,在此期間會處理來自輸入源的數(shù)據(jù),并且會在NSDefaultRunLoopMode模式下重復調(diào)用runMode:beforeDate:方法张吉;
(2) 第二種方式齿梁,可以設置超時時間,在超時時間到達之前肮蛹,runloop會一直運行勺择,在此期間runloop會處理來自輸入源的數(shù)據(jù),并且也會在NSDefaultRunLoopMode模式下重復調(diào)用runMode:beforeDate:方法伦忠;
(3) 第三種方式省核,runloop會運行一次,超時時間到達或者第一個input source被處理昆码,則runloop就會退出儒飒。
前兩種啟動方式會重復調(diào)用runMode:beforeDate:方法佩捞。