Objective-C之我所理解的Runloop

前言

RunLoop是iOS和OSX開發(fā)中非?;A(chǔ)的一個概念,學(xué)習(xí)Runloop能夠幫助我們更清楚的了解APP為何能夠持續(xù)運行。雖然在平時的工作場景中使用Runloop的機會很少,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程。網(wǎng)上關(guān)于Runloop的文章千篇一律,但"一千個讀者,就有一千個哈姆雷特",每個人都有自己不同的理解。


Runloop

通俗概念

  • 可以從字面上理解成“運行循環(huán)”、“跑圈”,通俗一點,它就是一個死循環(huán),相當(dāng)于一個do..while;
  • 正是因為這個死循環(huán)的存在,才能保持程序的持續(xù)運行,不會像一塊代碼,執(zhí)行完了就完了;
  • 有事情的時候做事情,沒事情的時候就休息,充分節(jié)省了CPU的性能;
  • 所謂的“事情”,實際上就是App中的各種事件(比如觸摸事件、定時器事件、Selector事件);

官方解釋

  • Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
  • Runloop是與多線程相關(guān)的、基礎(chǔ)框架中非常重要的一部分。 Runloop是你用來調(diào)度、協(xié)調(diào)當(dāng)前的到來事件的一個循環(huán)。Runloop的目的就是當(dāng)有任務(wù)到來的時候保持當(dāng)前線程處于繁忙狀態(tài), 當(dāng)沒有任務(wù)需要處理的時候讓當(dāng)前線程處于休眠狀態(tài)。

如果沒有Runloop

image
  • 代碼從上到下執(zhí)行,到第三行就結(jié)束了

有了Runloop以后

image
  • 由于main函數(shù)里面啟動了RunLoop,所以程序并不會馬上退出,保持持續(xù)運行狀態(tài)
  • 在代碼main.m里都默認(rèn)在主線程中啟動了runloop
  • 所以UIApplicationMain函數(shù)一直沒有返回,保持了程序的持續(xù)運行
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

RunLoop對象

  • iOS中為我們提供了兩套API訪問和操作runloop,一套是面向OC的,一套是基于C語言的
    • Foundation下的NSRunLoop
    • Core Foundation下的CFRunLoopRef
  • NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內(nèi)部結(jié)構(gòu),需要多研究CFRunLoopRef層面的API(Core Foundation層面)
  • 更多細(xì)節(jié)我們可以查閱蘋果官方文檔深入學(xué)習(xí)
  • 蘋果官方文檔
  • RunLoop 官方編程手冊翻譯

Runloop與線程的關(guān)系

  • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建(子線程的RunLoop默認(rèn)是關(guān)閉的,因為有時候開了個線程但卻沒有必要開一個RunLoop,不然反而浪費了資源)。
  • RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀(內(nèi)部實際上是一個懶加載)

獲得RunLoop對象

  • Foundation下的獲取方法
    • 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
      • [NSRunLoop currentRunLoop];
    • 獲得主線程的RunLoop對象
      • [NSRunLoop mainRunLoop];
  • Core Foundation下的獲取方法
    • 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
      • CFRunLoopGetCurrent();
    • 獲得主線程的RunLoop對象
      • CFRunLoopGetMain();

Runloop的相關(guān)類

  • 著重了解Core Foundation下的類
    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef
  • 它們的關(guān)系如圖所示
  • 一個runloop內(nèi)部包含若干個Mode,而每個Mode下又包含了若干個timer,observer,source.
  • 調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
  • 如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進(jìn)入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。
image

CFRunLoopRef

  • 即代表runloop本身

CFRunLoopModeRef

  • 代表RunLoop的運行模式
  • 系統(tǒng)默認(rèn)注冊了5個mode,前兩個常用,后三個基本用不到
    • kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個Mode下運行
    • UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
    • UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
    • GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
    • kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode

CFRunLoopSourceRef

  • 代表事件源(輸入源)
  • 以port進(jìn)行區(qū)分,而port可以為系統(tǒng)
  • 分為系統(tǒng)方法和自定義方法
    • sourse0:非基于port的,自定義方法,響應(yīng)
    • sourse1:基于port的,系統(tǒng)提供的方法

CFRunLoopTimerRef

  • 基于時間的觸發(fā)器
  • 基本上就等效于NSTimer

CFRunLoopObserverRef

  • 觀察者,用于監(jiān)聽RunLoop的狀態(tài)改變
  • 監(jiān)聽以下幾個狀態(tài)
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

圖解

image

image
  • 從圖上看就是一直在循環(huán),然后響應(yīng)對應(yīng)的事件,有就處理,沒有就休息。
  • 在即將進(jìn)入loop的時候會有一個判空操作,如果內(nèi)部沒有任何的source、timer、observer等待著處理,那么runloop會直接退出,所以當(dāng)我們在子線程開啟runloop的時候需要注意兩點:
// 1.要給runloop添加一個事件,讓它先跑起來再說
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
 // 2.需要手動開啟它
[[NSRunLoop currentRunLoop] run];

Runloop的應(yīng)用

1.處理NSTimer滑動暫停的問題。
// 通過timerWithTimeInterval創(chuàng)建出來的timer,默認(rèn)不會被添加到runloop,需要手動添加指定mode
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 通過timer的scheduledTimerWithTimeInterval創(chuàng)建出來的timer,默認(rèn)被添加到runloop的NSDefaultRunLoopMode下
// 當(dāng)滑動scrollView時,runloop會切換到UITrackingRunLoopMode
// 也就導(dǎo)致之前NSDefaultRunLoopMode下的timer暫停
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 解決方法,手動將timer添加到NSDefaultRunLoopMode下
// NSDefaultRunLoopMode表示timer既能響應(yīng)UITrackingRunLoopMode,
// 也能響應(yīng)NSDefaultRunLoopMode
// 相當(dāng)于將timer拷貝了一份放在這兩個mode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

2.在某個mode下調(diào)用方法。
// 只在NSDefaultRunLoopMode模式下顯示圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

3.更好的理解自動釋放池(@autoreleasepool)
  • @autoreleasepool會在runloop進(jìn)入休眠前統(tǒng)一釋放,在下一次即將進(jìn)入runloop時重新創(chuàng)建
  • 具體驗證可以通過創(chuàng)建observer來觀察
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
});

// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 釋放Observer
CFRelease(observer);

4. 創(chuàng)建一個常駐線程
  • 比如我們需要創(chuàng)建一個子線程,讓這個線程不死,一直循環(huán)做一些事情,比如說后臺不停的監(jiān)控用戶的網(wǎng)絡(luò)狀態(tài),掃描文件等。
  • 這時就可以為子線程創(chuàng)建一個runloop,讓它跑起來,有事情的時候做事情,沒事情的時候休息
#import "ViewController.h"

@interface ViewController ()
/** 線程對象 */
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
// 讓線程不死的一種取巧做法,
// 不停的開啟runloop,
// 每次runloop發(fā)現(xiàn)如果沒有任何的port就直接退出了,
// 當(dāng)我們調(diào)用touchesBegan為runloop添加了一個source時,runloop才正在跑起來了
    while (1) {
        [[NSRunLoop currentRunLoop] run];
    }
}

- (void)run1
{
    // 推薦開啟常駐線程的辦法
    // 手動添加一個port,讓它跑起來
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

- (void)test
{
    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}


關(guān)于runloop的面試題

1.什么是Runloop?

  • 從字面意思上理解為:運行循環(huán)、跑圈;
  • 其實它內(nèi)部是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Timer、Sources、Observer)
  • 一個線程對應(yīng)一個runloop,主線程的runloop默認(rèn)已經(jīng)啟動,子線程的runloop需要手動開啟(通過調(diào)用run方法)
  • runloop只能選擇一個mode啟動,如果當(dāng)前mode中沒有任何Timer、Sources、Observer。那么則直接退出runloop.

2.自動釋放池什么時候釋放?

  • 在runloop睡眠之前釋放(kCFRunLoopBeforeWaiting),在下一次跑圈的時候重新創(chuàng)建.

3.在開發(fā)中如何使用runloop?

  • 開啟一個常駐線程(即讓一個子線程不進(jìn)入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)
    • 在子線程開啟定時器
    • 在子線程進(jìn)行一些長期監(jiān)控(比如用戶的網(wǎng)絡(luò)狀態(tài),掃描用戶的文件等)
  • 可以控制定時器在特定mode下運行
  • 可以讓某些事件(行為、任務(wù))在特定mode下執(zhí)行
  • 可以添加observe監(jiān)聽runloop的一些狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)

4.ARC下是否還需要進(jìn)行內(nèi)存管理?

  • 需要。即便項目是ARC的情況下,針對Core Foundation下創(chuàng)建的對象也需要進(jìn)行內(nèi)存管理,因為ARC是針對OC而言的,而Core Foundation針對的是C語言.
    凡是帶有Create、Copy、Retain等字眼的函數(shù),最后都要記得調(diào)用CFRelease(對象)

5.NSTimer受runloop的影響,精度上存在誤差,如何解決?

  • 使用GCD創(chuàng)建timer,創(chuàng)建出來的timer不受runloop的影響,不會被添加到任何mode下,精度更高.

寫在最后

很久很久沒有更新博客,一直忙于日常的工作,有時候?qū)W了新東西想寫一寫,可能發(fā)現(xiàn)網(wǎng)上早已經(jīng)有了很多關(guān)于這方面的文章,于是便放棄了寫下去的念頭。其實,人都是有惰性的,總覺得看現(xiàn)成的比自己去寫一寫要來得快一些,但是整理知識點的過程,我們實際上也在加深一遍理解,而學(xué)習(xí)是一個不斷重復(fù)的過程。對于已經(jīng)掌握了相關(guān)知識的人,這種總結(jié)性文章可能毫無意義,但是對于想入門學(xué)習(xí)的人,文章能夠做到淺顯易懂,它就是有價值的。做了很久的開發(fā),發(fā)現(xiàn)實際編碼中我們真的很渺小,我們總是在搭建UI,創(chuàng)建model,網(wǎng)絡(luò)請求,數(shù)據(jù)填充,搞點炫酷的動畫,和產(chǎn)品經(jīng)理撕逼。即便你會各種黑魔法,各種超能力,能夠用到的機會其實并不多。所以越是基礎(chǔ)的東西越需要打牢,有了基礎(chǔ)才能舉一反三,才能一步一步的去解決更多刁鉆的需求。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容