學習 RunLoop (二)

上一節主要講了RunLoop的理論的基礎知識, 這一節講一講實踐:
修正一點: 根據源碼,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等滾動的控件, 然后我們一直滾動他, 發現2秒過去,圖片還不加載, 松手后才加載..那么結合上一節的知識, 我們知道performSelector也是默認在runloop的NSDefaultRunLoopMode模式下
也就是說,上面的代碼寫全其實是:

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

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

2. 常駐線程

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

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

#import "GYThread.h"

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

我們看看下面線程的執行:

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

- (void)run
{
    NSLog(@"----執行任務run----");
}

打印結果如下:

2016-06-19 15:26:29.001 runloopDemo[14322:161704] ----執行任務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] ----執行任務run----
2016-06-19 15:26:30.479 runloopDemo[14322:161711] <GYThread: 0x7fafc3424e40>{number = 3, name = (null)}-------dealloc

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

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

    // 如果不傳模式,不傳時間,默認為NSDefaultRunLoopMode,過期時間為distantFuture(遙遠的未來,不過期)
    [[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(@"----執行任務run----");
    
    // 創建RunLoop,并讓runloop常駐
    // 給runloop添加source或timer,才可以讓線程常駐
    // 添加port就相當于添加source,事件
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    [[NSRunLoop currentRunLoop] run];
    
    // 這句打印就不會執行了
    NSLog(@"----任務結束run----");
}

關閉runloop

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

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

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

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];
    // 添加到當前線程中(子線程)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // 當前的runloop中有timer了, 所以這個子線程的runloop可以常駐了,不會退出了
    [[NSRunLoop currentRunLoop] run];
    
    
    // 方法二:
    // 這個方法說明NSTimer加入到當前的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. 自動釋放池

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

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

說的詳細一點:

5. runloop面試題:

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

  • 1.什么是runloop?

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

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

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

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

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush()創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。


1.png

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


2.png

在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。

在自己創建線程時,需要手動創建自動釋放池AutoreleasePool

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Run loop 剖析:Runloop 接收的輸入事件來自兩種不同的源:輸入源(intput source)和定時...
    Mitchell閱讀 12,480評論 17 111
  • Runloop是iOS和OSX開發中非常基礎的一個概念,從概念開始學習。 RunLoop的概念 -般說,一個線程一...
    小貓仔閱讀 1,024評論 0 1
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,181評論 0 6
  • 得知姨公去世的消息時,我正在午夜的郊區,媽媽一字一頓的話好像一顆巨石,讓這個躁動的夏夜瞬間重了許多。姨公是在海灘邊...
    馬不理饅頭閱讀 425評論 0 0
  • 海上一草帽,帽下一人釣;漁舟隨風飄,她自任浪遙。
    逍遙修彤閱讀 206評論 0 3