學(xué)習(xí) RunLoop (二)

上一節(jié)主要講了RunLoop的理論的基礎(chǔ)知識, 這一節(jié)講一講實踐:
修正一點: 根據(jù)源碼,runloop要跑起來先判斷mode是否為空,如果為空退出,
然后判斷source0是否為空,如果為空退出,然后判斷source1是否為空,如果為空退出,然后判斷是否有timer,如果沒有就退出,并沒有判斷是否有observer,所以runloop如果要跑起來,必須有source或者timer的其中一個

源碼如下:

static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
    CHECK_FOR_FORK();
    if (NULL == rlm) return true;
#if DEPLOYMENT_TARGET_WINDOWS
    if (0 != rlm->_msgQMask) return false;
#endif
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
    if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
    if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
    if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
    struct _block_item *item = rl->_blocks_head;
    while (item) {
        struct _block_item *curr = item;
        item = item->_next;
        Boolean doit = false;
        if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
            doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
        } else {
            doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
        }
        if (doit) return false;
    }
    return true;
}
1. imageView

如果我們想讓圖片延時加載, 我們一般這樣寫:

 [self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0];

如果界面上有個TextView等滾動的控件, 然后我們一直滾動他, 發(fā)現(xiàn)2秒過去,圖片還不加載, 松手后才加載..那么結(jié)合上一節(jié)的知識, 我們知道performSelector也是默認(rèn)在runloop的NSDefaultRunLoopMode模式下
也就是說,上面的代碼寫全其實是:

[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];

應(yīng)用場景: 如果我們在滾動tableView,如果想讓圖片顯示在tableView的imageView上,如果圖片比較大,渲染時間長,那時候就tableView滾動就會比較卡, 所以有的解決方案是:推遲image的顯示,滾動tableView的時候,雖然圖片下載完了,但是圖片暫時不讓它顯示,等手指松開,停止?jié)L動,再顯示圖片

2. 常駐線程

例如:想創(chuàng)建一個子線程,一直在后臺監(jiān)控用戶的一些行為,所以我們需要創(chuàng)建的這個線程一直不能死

首先我們看看線程是怎么工作的:
先繼承于NSThread, 創(chuàng)建一個我自己的線程(GYThread), 重寫dealloc方法,這樣這個線程如果被銷毀了,我們可以打印監(jiān)聽到

#import "GYThread.h"

- (void)dealloc
{
    NSLog(@"%@-------dealloc",self);
}

我們看看下面線程的執(zhí)行:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{    
    GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    [thread start];
}

- (void)run
{
    NSLog(@"----執(zhí)行任務(wù)run----");
}

打印結(jié)果如下:

2016-06-19 15:26:29.001 runloopDemo[14322:161704] ----執(zhí)行任務(wù)run----
2016-06-19 15:26:29.003 runloopDemo[14322:161704] <GYThread: 0x7fafc3705490>{number = 2, name = (null)}-------dealloc
2016-06-19 15:26:30.478 runloopDemo[14322:161711] ----執(zhí)行任務(wù)run----
2016-06-19 15:26:30.479 runloopDemo[14322:161711] <GYThread: 0x7fafc3424e40>{number = 3, name = (null)}-------dealloc

則 發(fā)現(xiàn)每次執(zhí)行完任務(wù), Thread就會被dealloc, 而每次開啟內(nèi)存地址都不同
那我弄一個strong的全局變量記錄這個Thread,不讓他釋放, 每次點擊調(diào)用一下線程開始的方法怎么樣? 答案是否定的,第一次點擊完,任務(wù)執(zhí)行完,確實Thread不會被dealloc, 但是點擊第二次讓他直接開啟時,就會崩潰,因為執(zhí)行完任務(wù),雖然Thread沒有被釋放,還處于內(nèi)存中,但是它處于消亡狀態(tài), 蘋果不允許線程這樣做..會報錯attempt to start the thread again(嘗試重新開啟線程)

    // 下面這三句代碼是等價的, 這樣runloop跑起來會立刻退出,因為我們還要往runloop中添加observe,timer,source,否則runloop跑起來會立刻退出

    // 如果不傳模式,不傳時間,默認(rèn)為NSDefaultRunLoopMode,過期時間為distantFuture(遙遠(yuǎn)的未來,不過期)
    [[NSRunLoop currentRunLoop] run];
    
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
正確的添加常駐線程的做法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{    
    GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    [thread start];
}

- (void)run
{
    NSLog(@"----執(zhí)行任務(wù)run----");
    
    // 創(chuàng)建RunLoop,并讓runloop常駐
    // 給runloop添加source或timer,才可以讓線程常駐
    // 添加port就相當(dāng)于添加source,事件
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    [[NSRunLoop currentRunLoop] run];
    
    // 這句打印就不會執(zhí)行了
    NSLog(@"----任務(wù)結(jié)束run----");
}

關(guān)閉runloop

    /* 應(yīng)用場景:
     一直在后臺檢測用戶的行為,掃描用戶的操作,檢查操作,更新操作,檢查聯(lián)網(wǎng)狀態(tài)
    */
    
    // 如果想退出runloop, 只要關(guān)閉這條線程,或者讓runloop中沒有port,source
    // 方式一:
    [NSThread exit];
    // 方式二:
    [[NSRunLoop currentRunLoop] removePort:[NSPort port] forMode:NSDefaultRunLoopMode];
奇葩的添加常駐線程的做法(不推薦)
 // 在子線程的任務(wù)中添加, 想關(guān)閉的時候,讓flag=0即可

int flag = 1;
    while (flag) {
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"----runloop退出----");
    }

缺點: 上面的代碼會一直打印----runloop退出----,說明子線程的runloop一直進入,然后退出,再進入再退出, 因為這個runloop中沒有timer,source的其中任何一個, 只有點擊了給他下達了任務(wù)(比如上面的-(void)run方法, 或者[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];),才會給它一個事件(source), 在這個時刻 , 就不會一直打印----runloop退出----了, 這時候相當(dāng)于給這個runloop,添加了source,所以這個runloop會進入循環(huán), 就不會停止了,不會退出了

3. 給子線程添加NSTimer
- (void)viewDidLoad {
    [super viewDidLoad];

    // 給子線程添加NSTimer
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAddTimer) object:nil];
    [thread start];   
}

// 給子線程添加NSTimer
- (void)threadAddTimer
{
    @autoreleasepool {

    // 方法一:
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
    // 添加到當(dāng)前線程中(子線程)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // 當(dāng)前的runloop中有timer了, 所以這個子線程的runloop可以常駐了,不會退出了
    [[NSRunLoop currentRunLoop] run];
    
    
    // 方法二:
    // 這個方法說明NSTimer加入到當(dāng)前的runloop中的NSDefaultRunLoopMode的模式中,所以再加上一句runloop啟動就和上面的方法一樣了
//    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
//    [[NSRunLoop currentRunLoop] run];
    }
}

- (void)addTimer
{
    NSLog(@"----這是子線程的定時器----");
}

給子線程添加了NSTimer, 如果我再滑動TableView,則子線程的NSTimer還是正常運行的..這種方式也解決了以前滑動定時器不好使的問題
子線程的定時器的模式跑在NSDefaultRunLoopMode模式下,
滑動TableView是使主線程跑在了UITrackingRunLoopMode模式下, 兩個線程影響

4. 自動釋放池

自動釋放池: 將一些對象扔到這個池子中, 當(dāng)這個池子被釋放的時候, 讓這個池子的所有對象都調(diào)用release方法
面試的時候經(jīng)常會問到自動釋放池什么時候死呢(被釋放呢)?
答案就是: runloop在睡眠之前會被釋放,因為runloop睡眠可能會睡很長時間,時間不定,如果睡眠時間很長,也不讓自動釋放池釋放掉,則內(nèi)存會堆扎,所以runloop在每次睡覺之前會被清理一次..
在runloop進入下一次循環(huán)被喚醒之前,又會創(chuàng)建一個新的釋放池, 中間創(chuàng)建的臨時變量就會放到這個池子中

一個runloop對應(yīng)一個線程, 所以我們在子線程中創(chuàng)建runloop的時候,最好用創(chuàng)建一個自動釋放池包裹住創(chuàng)建的runloop,如上面的代碼..
因為我們看main.m中 就是用一個自動釋放池包裹住的主線程的runloop, 這是一個安全的做法

說的詳細(xì)一點:

5. runloop面試題:

一些面試官會問一些runloop的問題- -!
比如:

  • 1.什么是runloop?

    • 從字面意思說是: 運行循環(huán), 跑圈
    • 其實它的內(nèi)部是一個高級的do-while循環(huán), 在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(source, timer, observe)
    • 一個線程對應(yīng)一個runloop, 源碼中有一個可變字典,key是線程,value是runloop對象
    • 主線程的runloop默認(rèn)已經(jīng)啟動,在main函數(shù)中, 子線程需要自己手動啟動(調(diào)用run方法), 子線程的創(chuàng)建[NSRunLoop currentRunLoop]
    • runloop只能選擇一個模式啟動, 如果想用其他模式,只能退出當(dāng)前循環(huán),再進入新的模式, 如果當(dāng)前模式中, 沒有source,timer其中任何一個,那么就直接退出runloop
  • 2.在開發(fā)中如何使用runloop, 使用場景:

    • 開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài), 等待其他線程發(fā)來的消息,處理其他事件)
    • 在子線程中開啟一個定時器
    • 在子線程中長期監(jiān)控一些行為(比如沙盒的檢測掃描)
    • 可以控制定時器在那種模式下運行(Tranking,Default)
    • 可以讓某些事件(行為,任務(wù)),在特定模式下執(zhí)行
    • 可以添加observe監(jiān)聽runloop的一些狀態(tài)(我們可以在處理所有點擊事件,UI事件之前做一些事情)
    • 我們可以自定義源(source)給他發(fā)送消息, CFRunLoopSourceCreate(..)函數(shù)創(chuàng)建source源 , 這個和[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];比較相似
  • 3.自動釋放池什么時候釋放
    自動釋放池釋放的時間和RunLoop的關(guān)系:

注意,這里的自動釋放池指的是主線程的自動釋放池,我們看不見它的創(chuàng)建和銷毀。自己手動創(chuàng)建@autoreleasepool {}是根據(jù)代碼塊來的,出了這個代碼塊就釋放了。

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。


1.png

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準(zhǔn)備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。


2.png

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。

在自己創(chuàng)建線程時,需要手動創(chuàng)建自動釋放池AutoreleasePool

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,285評論 2 376

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