天天看點

執行個體化講解 RunLoop

執行個體化講解runloop

之前看過很多有關runloop的文章,其中要麼是主要介紹runloop的基本概念,要麼是主要講解runloop的底層原理,很少用真正的執行個體來講解runloop的,這其中有大部分原因是由于大家在項目中很少能用到runloop吧。基于這種原因,本文中将用很少的篇幅來對基礎内容做以介紹,然後主要利用執行個體來加深大家對runloop的了解,本文中的代碼已經上傳github,大家可以下載下傳檢視,有問題歡迎issue我。本文主要分為如下幾個部分:

runloop的基礎知識

初識runloop,如何讓runloop進駐線程

深入了解perform selector

一直”活着”的背景線程

深入了解nstimer

讓兩個背景線程有依賴性的一種方式

nsurlconnetction的内部實作

afnetworking中是如何使用runloop的?

其它:利用gcd實作定時器功能

延伸閱讀

一、runloop的基本概念:

什麼是runloop?提到runloop,我們一般都會提到線程,這是為什麼呢?先來看下官方對runloop的定義:runloop系統中和線程相關的基礎架構的組成部分(和線程相關),一個runloop是一個事件處理環,系統利用這個事件處理環來安排事務,協調輸入的各種事件。runloop的目的是讓你的線程在有工作的時候忙碌,沒有工作的時候休眠(和線程相關)。可能這樣說你還不是特别清楚runloop究竟是用來做什麼的,打個比方來說明:我們把線程比作一輛跑車,把這輛跑車的主人比作runloop,那麼在沒有’主人’的時候,這個跑車的生命是直線型的,其啟動,運作完之後就會廢棄(沒有人對其進行控制,’撞壞’被收回),當有了runloop這個主人之後,‘線程’這輛跑車的生命就有了保障,這個時候,跑車的生命是環形的,并且在主人有比賽任務的時候就會被runloop這個主人所喚醒,在沒有任務的時候可以休眠(在ios中,開啟線程是很消耗性能的,開啟主線程要消耗1m記憶體,開啟一個背景線程需要消耗512k記憶體,我們應當線上程沒有任務的時候休眠,來釋放所占用的資源,以便cpu進行更加高效的工作),這樣可以增加跑車的效率,也就是說runloop是為線程所服務的。這個例子有點不是很貼切,線程和runloop之間是以鍵值對的形式一一對應的,其中key是thread,value是runloop(這點可以從蘋果公開的源碼中看出來),其實runloop是管理線程的一種機制,這種機制不僅在ios上有,在node.js中的eventloop,android中的looper,都有類似的模式。剛才所說的比賽任務就是喚醒跑車這個線程的一個source;runloop mode就是,一系列輸入的source,timer以及observer,runloop mode包含以下幾種: nsdefaultrunloopmode,nseventtrackingrunloopmode,uiinitializationrunloopmode,nsrunloopcommonmodes,nsconnectionreplymode,nsmodalpanelrunloopmode,至于這些mode各自的含義,讀者可自己查詢,網上不乏這類資源;

二、初識runloop,如何讓runloop進駐線程

我們在主線程中添加如下代碼:

while (1) {     nslog(@"while begin");     // the thread be blocked here     nsrunloop *runloop = [nsrunloop currentrunloop];     [runloop runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]];     // this will not be executed     nslog(@"while end"); }

這個時候我們可以看到主線程在執行完[runloop runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]]; 之後被阻塞而沒有執行下面的nslog(@"while end");同時,我們利用gcd,将這段代碼放到一個背景線程中:

dispatch_async(dispatch_get_global_queue(dispatch_queue_priority_default, 0), ^{     while (1) {         nslog(@"while begin");         nsrunloop *subrunloop = [nsrunloop currentrunloop];         [subrunloop runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]];         nslog(@"while end");     } });

這個時候我們發現這個while循環會一直在執行;這是為什麼呢?我們先将這兩個runloop分别列印出來:

執行個體化講解 RunLoop

主線程的runloop

由于這個日志比較長,我就隻截取了上面的一部分。

我們再看我們建立的子線程中的runloop,列印出來之後:

執行個體化講解 RunLoop

backgroundthreadrunloop.png

從中可以看出來:我們建立的線程中:

sources0 = (null), sources1 = (null), observers = (null), timers = (null),

我們看到雖然有mode,但是我們沒有給它soures,observer,timer,其實mode中的這些source,observer,timer,統稱為這個mode的item,如果一個mode中一個item都沒有,則這個runloop會直接退出,不進入循環(其實線程之是以可以一直存在就是由于runloop将其帶入了這個循環中)。下面我們為這個runloop添加個source:

        while (1) {         nsport *macport = [nsport port];         [subrunloop addport:macport formode:nsdefaultrunloopmode];         nslog(@"%@",subrunloop);     }    

這樣我們可以看到能夠實作了和主線程中相同的效果,線程在這個地方暫停了,為什麼呢?我們明天讓runloop在distantfuture之前都一直run的啊?相信大家已經猜出出來了。這個時候線程被runloop帶到‘坑’裡去了,這個‘坑’就是一個循環,在循環中這個線程可以在沒有任務的時候休眠,在有任務的時候被喚醒;當然我們隻用一個while(1)也可以讓這個線程一直存在,但是這個線程會一直在喚醒狀态,及時它沒有任務也一直處于運轉狀态,這對于cpu來說是非常不高效的。

小結:我們的runloop要想工作,必須要讓它存在一個item(source,observer或者timer),主線程之是以能夠一直存在,并且随時準備被喚醒就是應為系統為其添加了很多item

三、深入了解perform selector

我們先在主線程中使用下performselector:<br/>

- (void)tryperformselectoronmianthread{ [self performselector:@selector(mainthreadmethod) withobject:nil]; } - (void)mainthreadmethod{ nslog(@"execute %s",__func__); // print: execute -[viewcontroller mainthreadmethod]

這樣我們在viewdidload中調用tryperformselectoronmianthread,就會立即執行,并且輸出:print: execute -[viewcontroller mainthreadmethod];

和上面的例子一樣,我們使用gcd,讓這個方法在背景線程中執行

- (void)tryperformselectoronbackgroundthread{ [self performselector:@selector(backgroundthread) onthread:[nsthread currentthread] withobject:nil waituntildone:no]; - (void)backgroundthread{ nslog(@"%u",[nsthread ismainthread]); nslog(@"execute %s",__function__);

同樣的,我們調用tryperformselectoronbackgroundthread這個方法,我們會發現,下面的backgroundthread不會被調用,這是什麼原因呢?

這是因為,在調用performselector:onthread: withobject: waituntildone的時候,系統會給我們建立一個timer的source,加到對應的runloop上去,然而這個時候我們沒有runloop,如果我們加上runloop:

nsrunloop *runloop = [nsrunloop currentrunloop]; [runloop run];

這時就會發現我們的方法正常被調用了。那麼為什麼主線程中的perfom selector卻能夠正常調用呢?通過上面的例子相信你已經猜到了,主線程的runloop是一直存在的,是以我們在主線程中執行的時候,無需再添加runloop。

小結:當perform selector在背景線程中執行的時候,這個線程必須有一個開啟的runloop

四、一直”活着”的背景線程

現在有這樣一個需求,每點選一下螢幕,讓子線程做一個任務,然後大家一般會想到這樣的方式:

@interface viewcontroller () @property(nonatomic,strong) nsthread *mythread; @end @implementation viewcontroller - (void)alwayslivebackgoundthread{ nsthread *thread = [[nsthread alloc]initwithtarget:self selector:@selector(mythreadrun) object:@"etund"]; self.mythread = thread; [self.mythread start]; - (void)mythreadrun{ nslog(@"my thread run"); - (void)touchesbegan:(nsset<uitouch *> *)touches withevent:(uievent *)event{     nslog(@"%@",self.mythread);     [self performselector:@selector(dobackgroundthreadwork) onthread:self.mythread withobject:nil waituntildone:no]; - (void)dobackgroundthreadwork{     nslog(@"do some work %s",__function__);

這個方法中,我們利用一個強引用來擷取了後天線程中的thread,然後在點選螢幕的時候,在這個線程上執行dobackgroundthreadwork這個方法,此時我們可以看到,在touchesbegin方法中,self.mythread是存在的,但是這是為是什麼呢?這就要從線程的五大狀态來說明了:建立狀态、就緒狀态、運作狀态、阻塞狀态、死亡狀态,這個時候盡管記憶體中還有線程,但是這個線程在執行完任務之後已經死亡了,經過上面的論述,我們應該怎樣處理呢?我們可以給這個線程的runloop添加一個source,那麼這個線程就會檢測這個source等待執行,而不至于死亡(有工作的強烈願望而不死亡):

[[nsrunloop currentrunloop] addport:[[nsport alloc] init] formode:nsdefaultrunloopmode]; [[nsrunloop currentrunloop] run]   nslog(@"my thread run");

這個時候再次點選螢幕,我們就會發現,背景線程中執行的任務可以正常進行了。

小結:正常情況下,背景線程執行完任務之後就處于死亡狀态,我們要避免這種情況的發生可以利用runloop,并且給它一個source這樣來保證線程依舊還在

五、深入了解nstimer

我們平時使用nstimer,一般是在主線程中的,代碼大多如下:

- (void)trytimeronmainthread{ nstimer *mytimer = [nstimer scheduledtimerwithtimeinterval:0.5 target:self           selector:@selector(timeraction) userinfo:nil repeats:yes]; [mytimer fire]; - (void)timeraction{ nslog(@"timer action");

這個時候代碼按照我們預定的結果運作,如果我們把這個tiemr放到背景線程中呢?

    nstimer *mytimer = [nstimer scheduledtimerwithtimeinterval:0.5 target:self selector:@selector(timeraction) userinfo:nil repeats:yes];     [mytimer fire];

這個時候我們會發現,這個timer隻執行了一次,就停止了。這是為什麼呢?通過上面的講解,想必你已經知道了,nstimer,隻有注冊到runloop之後才會生效,這個注冊是由系統自動給我們完成的,既然需要注冊到runloop,那麼我們就需要有一個runloop,我們在背景線程中加入如下的代碼:

    [runloop run];

這樣我們就會發現程式正常運作了。在timer注冊到runloop之後,runloop會為其重複的時間點注冊好事件,比如1:10,1:20,1:30這幾個時間點。有時候我們會在這個線程中執行一個耗時操作,這個時候runloop為了節省資源,并不會在非常準确的時間點回調這個timer,這就造成了誤差(timer有個備援度屬性叫做tolerance,它标明了目前點到後,容許有多少最大誤差),可以在執行一段循環之後調用一個耗時操作,很容易看到timer會有很大的誤差,這說明線上程很閑的時候使用nstiemr是比較傲你準确的,當線程很忙碌時候會有較大的誤差。系統還有一個cadisplaylink,也可以實作定時效果,它是一個和螢幕的重新整理率一樣的定時器。如果在兩次螢幕重新整理之間執行一個耗時的任務,那其中就會有一個幀被跳過去,造成界面卡頓。另外gcd也可以實作定時器的效果,由于其和runloop沒有關聯,是以有時候使用它會更加的準确,這在最後會給予說明。

六、讓兩個背景線程有依賴性的一種方式

給兩個背景線程添加依賴可能有很多的方式,這裡說明一種利用runloop實作的方式。原理很簡單,我們先讓一個線程工作,當工作完成之後喚醒另外的一線程,通過上面對runloop的說明,相信大家很容易能夠了解這些代碼:

- (void)runloopadddependance{ self.runloopthreaddidfinishflag = no; nslog(@"start a new run loop thread"); nsthread *runloopthread = [[nsthread alloc] initwithtarget:self selector:@selector(handlerunloopthreadtask) object:nil]; [runloopthread start]; nslog(@"exit handlerunloopthreadbuttontouchupinside"); dispatch_async(dispatch_get_global_queue(0, 0), ^{     while (!_runloopthreaddidfinishflag) {         self.mythread = [nsthread currentthread];         nslog(@"begin runloop");         nsrunloop *runloop = [nsrunloop currentrunloop];         nsport *myport = [nsport port];         [runloop addport:myport formode:nsdefaultrunloopmode];         [[nsrunloop currentrunloop] runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]];         nslog(@"end runloop");         [self.mythread cancel];         self.mythread = nil; - (void)handlerunloopthreadtask { nslog(@"enter run loop thread"); for (nsinteger i = 0; i < 5; i ++) {     nslog(@"in run loop thread, count = %ld", i);     sleep(1); #if 0 // 錯誤示範 _runloopthreaddidfinishflag = yes; // 這個時候并不能執行線程完成之後的任務,因為run loop所在的線程并不知道runloopthreaddidfinishflag被重新指派。run loop這個時候沒有被任務事件源喚醒。 // 正确的做法是使用 "selector"方法喚醒run loop。 即如下: #endif nslog(@"exit normal thread"); [self performselector:@selector(tryonmythread) onthread:self.mythread withobject:nil waituntildone:no]; // nslog(@"exit run loop thread");

七、nsurlconnection的執行過程

在使用nsurlconnection時,我們會傳入一個delegate,當我們調用了[connection start]之後,這個delegate會不停的收到事件的回調。實際上,start這個函數的内部會擷取currentrunloop,然後在其中的defaultmode中添加4個source。如下圖所示,cfmultiplexersource是負責各種delegate回調的,cfhttpcookiestorage是處理各種cookie的。如下圖所示:

執行個體化講解 RunLoop

nsurlconnection的執行過程

從中可以看出,當開始網絡傳輸是,我們可以看到nsurlconnection建立了兩個新的線程:com.apple.nsurlconnectionloader和com.apple.cfsocket.private。其中cfsocket是處理底層socket連結的。nsurlconnectionloader這個線程内部會使用runloop來接收底層socket的事件,并通過之前添加的source,來通知(喚醒)上層的delegate。這樣我們就可以了解我們平時封裝網絡請求時候常見的下面邏輯了:

while (!_isendrequest)     nslog(@"entered run loop");     [[nsrunloop currentrunloop] runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]]; nslog(@"main finished,task be removed"); - (void)connectiondidfinishloading:(nsurlconnection *)connection   _isendrequest = yes;

這裡我們就可以解決下面這些疑問了:

為什麼這個while循環不停的執行,還需要使用一個runloop? 程式執行一個while循環是不會耗費很大性能的,我們這裡的目的是想讓子線程在有任務的時候處理任務,沒有任務的時候休眠,來節約cpu的開支。

如果沒有為runloop添加item,那麼它就會立即退出,這裡的item呢? 其實系統已經給我們預設添加了4個source了。

既然[[nsrunloop currentrunloop] runmode:nsdefaultrunloopmode beforedate:[nsdate distantfuture]];讓線程在這裡停下來,那麼為什麼這個循環會持續的執行呢?因為這個一直在處理任務,并且接受系統對這個delegate的回調,也就是這個回調喚醒了這個線程,讓它在這裡循環。

八、afnetworking中是如何使用runloop的?

在afn中afurlconnectionoperation是基于nsurlconnection建構的,其希望能夠在背景線程來接收delegate的回調。

為此afn建立了一個線程,然後在裡面開啟了一個runloop,然後添加item

+ (void)networkrequestthreadentrypoint:(id)__unused object { @autoreleasepool {     [[nsthread currentthread] setname:@"afnetworking"];     [runloop addport:[nsmachport port] formode:nsdefaultrunloopmode]; + (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;

這裡這個nsmachport的作用和上文中的一樣,就是讓線程不至于在很快死亡,然後runloop不至于退出(如果要使用這個machport的話,調用者需要持有這個nsmachport,然後在外部線程通過這個port發送資訊到這個loop内部,它這裡沒有這麼做)。然後和上面的做法相似,在需要背景執行這個任務的時候,會通過調用:[nsobject performselector:onthread:..]來将這個任務扔給背景線程的runloop中來執行。

- (void)start { [self.lock lock]; if ([self iscancelled]) {     [self performselector:@selector(cancelconnection) onthread:[[self class] networkrequestthread] withobject:nil waituntildone:no modes:[self.runloopmodes allobjects]]; } else if ([self isready]) {     self.state = afoperationexecutingstate;     [self performselector:@selector(operationdidstart) onthread:[[self class] networkrequestthread] withobject:nil waituntildone:no modes:[self.runloopmodes allobjects]]; [self.lock unlock];

gcd定時器的實作

- (void)gcdtimer{ // get the queue dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // creat timer self.timer = dispatch_source_create(dispatch_source_type_timer, 0, 0, queue); // config the timer (starting time,interval) // set begining time dispatch_time_t start = dispatch_time(dispatch_time_now, (int64_t)(1.0 * nsec_per_sec)); // set the interval uint64_t interver = (uint64_t)(1.0 * nsec_per_sec); dispatch_source_set_timer(self.timer, start, interver, 0.0); dispatch_source_set_event_handler(self.timer, ^{     // the tarsk needed to be processed async     dispatch_async(dispatch_get_global_queue(0, 0), ^{         for (int i = 0; i < 100000; i++) {             nslog(@"gcdtimer");         } dispatch_resume(self.timer);