iOS復習-Runloop常駐線程

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文章里給的示意圖所示:

GCD.jpg

上面貼的源碼把敢,我們關注如下的部分:
其中有一個用來記錄線程池大小的字段 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:方法佩捞。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子喳钟,更是在濱河造成了極大的恐慌树肃,老刑警劉巖遍略,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舱卡,死亡現(xiàn)場離奇詭異,居然都是意外死亡陪毡,警方通過查閱死者的電腦和手機米母,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缤骨,“玉大人爱咬,你說我怎么就攤上這事“砥穑” “怎么了精拟?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長虱歪。 經(jīng)常有香客問我蜂绎,道長,這世上最難降的妖魔是什么笋鄙? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任师枣,我火速辦了婚禮,結(jié)果婚禮上萧落,老公的妹妹穿的比我還像新娘践美。我一直安慰自己洗贰,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布陨倡。 她就那樣靜靜地躺著敛滋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兴革。 梳的紋絲不亂的頭發(fā)上绎晃,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音杂曲,去河邊找鬼庶艾。 笑死,一個胖子當著我的面吹牛擎勘,可吹牛的內(nèi)容都是我干的咱揍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼棚饵,長吁一口氣:“原來是場噩夢啊……” “哼述召!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蟹地,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎藤为,沒想到半個月后怪与,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡缅疟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年分别,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片存淫。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡耘斩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桅咆,到底是詐尸還是另有隱情括授,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布岩饼,位于F島的核電站荚虚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏籍茧。R本人自食惡果不足惜版述,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寞冯。 院中可真熱鬧渴析,春花似錦晚伙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恢恼,卻和暖如春民傻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背场斑。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工漓踢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人漏隐。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓喧半,卻偏偏與公主長得像,于是被迫代替她去往敵國和親青责。 傳聞我的和親對象是個殘疾皇子挺据,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

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