1. 程式人生 > >RunLoop 總結:RunLoop的應用場景(二)

RunLoop 總結:RunLoop的應用場景(二)

上一篇講了使用RunLoop保證子執行緒的長時間存活,而不是執行完任務後就立刻銷燬的應用場景。這一篇就講述一下RunLoop如何保證NSTimer在檢視滑動時,依然能正常運轉。

參考資料

好的書籍都是值得反覆看的,那好的文章,好的資料也值得我們反覆看。我們在不同的階段來相同的文章或資料或書籍都能有不同的收穫,那它就是好文章,好書籍,好資料。
關於iOS 中的RunLoop資料非常的少,以下資料都是非常好的。

  • CF框架原始碼(這是一份很重要的原始碼,可以看到CF框架的每一次迭代,我們可以下載最新的版本來分析,或與以下文章對比學習。目前最新的是CF-1153.18.tar.gz)
  • RunLoop官方文件
    (學習iOS的任何技術,官方文件都是入門或深入的極好手冊;我們也可以在Xcode—>Help—>Docementation and API Reference —>搜尋RunLoop—> Guides(59)—>《Threading Programming Guide:Run Loops》這篇即是)
  • 深入理解RunLoop(不要看到右邊滾動條很長,其實文章佔篇幅2/5左右,下面有很多的評論,可見這篇文章的火熱)
  • RunLoop個人小結 (這是一篇總結的很通俗容易理解的文章)
  • iPhonedevwiki中的CFRunLoop(commonModes中其實包含了三種Mode,我們通常知道兩種,還有一種是啥,你知道麼?)

使用場景

1.我們經常會在應用中看到tableView 的header 上是一個橫向ScrollView,一般我們使用NSTimer,每隔幾秒切換一張圖片。可是當我們滑動tableView的時候,頂部的scollView並不會切換圖片,這可怎麼辦呢?
2.介面上除了有tableView,還有顯示倒計時的Label,當我們在滑動tableView時,倒計時就停止了,這又該怎麼辦呢?

場景中的程式碼實現

我們的定時器Timer是怎麼寫的呢?
一般的做法是,在主執行緒(可能是某控制器的viewDidLoad方法)中,建立Timer。
可能會有兩種寫法,但是都有上面的問題,下面先看下Timer的兩種寫法:

// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

上面的兩種寫法其實是等價的。第二種寫法,預設也是將timer新增到NSDefaultRunLoopMode下的,並且會自動fire。。
要驗證這一結論,我們只需要在timerUpdate方法中,將當前runLoop的currentMode打印出來即可。

- (void)timerUpdate
{
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
    NSLog(@"啟動RunLoop後--%@",[NSRunLoop currentRunLoop].currentMode);
//    NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.count ++;
        NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
        self.timerLabel.text = timerText;
    });
}
// 控制檯輸出結果:
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 當前執行緒:<NSThread: 0x600000065500>{number = 1, name = main}
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 啟動RunLoop後--kCFRunLoopDefaultMode

然後,我們在滑動tableView的時候timerUpdate方法,並不會呼叫。
* 原因是啥呢?*
原因是當我們滑動scrollView時,主執行緒的RunLoop 會切換到UITrackingRunLoopMode這個Mode,執行的也是UITrackingRunLoopMode下的任務(Mode中的item),而timer 是新增在NSDefaultRunLoopMode下的,所以timer任務並不會執行,只有當UITrackingRunLoopMode的任務執行完畢,runloop切換到NSDefaultRunLoopMode後,才會繼續執行timer。

* 要如何解決這一問題呢?*
解決方法很簡單,我們只需要在新增timer 時,將mode 設定為NSRunLoopCommonModes即可。

- (void)timerTest
{
    // 第一種寫法
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    [timer fire];
    // 第二種寫法,因為是固定新增到defaultMode中,就不要用了
}

RunLoop官方文件iPhonedevwiki中的CFRunLoop可以看出,NSRunLoopCommonModes並不是一種Mode,而是一種特殊的標記,關聯的有一個set ,官方文件說:For Cocoa applications, this set includes the default, modal, and event tracking modes by default.(預設包含NSDefaultRunLoopModeNSModalPanelRunLoopModeNSEventTrackingRunLoopMode
新增到NSRunLoopCommonModes中的還沒有執行的任務,會在mode切換時,再次新增到當前的mode中,這樣就能保證不管當前runloop切換到哪一個mode,任務都能正常執行。並且被新增到NSRunLoopCommonModes中的任務會儲存在runloop 的commonModeItems中。

其他一些關於timer的坑

我們在子執行緒中使用timer,也可以解決上面的問題,但是需要注意的是把timer加入到當前runloop後,必須讓runloop 執行起來,否則timer僅執行一次。

示例程式碼:

//首先是建立一個子執行緒
- (void)createThread
{
    NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
    [subThread start];
    self.subThread = subThread;
}

// 建立timer,並新增到runloop的mode中
- (void)timerTest
{
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
        NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
        // 第一種寫法,改正前
    //    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //    [timer fire];
        // 第二種寫法
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] run];
    }
}

//更新label
- (void)timerUpdate
{
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
    NSLog(@"啟動RunLoop後--%@",[NSRunLoop currentRunLoop].currentMode);
    NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.count ++;
        NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
        self.timerLabel.text = timerText;
    });
}

新增timer 前的控制檯輸出:

新增timer前的runloop

新增timer後的控制檯輸出:

新增timer後的runloop

從控制檯輸出可以看出,timer確實被新增到NSDefaultRunLoopMode中了。可是新增到子執行緒中的NSDefaultRunLoopMode裡,無論如何滾動,timer都能夠很正常的運轉。這又是為啥呢?

這就是多執行緒與runloop的關係了,每一個執行緒都有一個與之關聯的RunLoop,而每一個RunLoop可能會有多個Mode。CPU會在多個執行緒間切換來執行任務,呈現出多個執行緒同時執行的效果。執行的任務其實就是RunLoop去各個Mode裡執行各個item。因為RunLoop是獨立的兩個,相互不會影響,所以在子執行緒新增timer,滑動檢視時,timer能正常執行。

總結

1、如果是在主執行緒中執行timer,想要timer在某介面有檢視滾動時,依然能正常運轉,那麼將timer新增到RunLoop中時,就需要設定mode 為NSRunLoopCommonModes
2、如果是在子執行緒中執行timer,那麼將timer新增到RunLoop中後,Mode設定為NSDefaultRunLoopModeNSRunLoopCommonModes均可,但是需要保證RunLoop在執行,且其中有任務。