RunLoop 學(xué)習(xí)及常見(jiàn)問(wèn)題

什么是 RunLoop

通常在終端中輸入命令,執(zhí)行任務(wù)的線程執(zhí)行完就退出了,等我們?cè)俅屋斎朊?,終端再開(kāi)始執(zhí)行任務(wù)。但在我們的 app 中,要保持一直運(yùn)行(除非app被掛起),不斷接受用戶的輸入,循環(huán)的接受、處理事件,類似于這樣:

while(AppIsRunning){  //只要 app 處于運(yùn)行狀態(tài),就要不斷等待著處理事件
    id whoWakesMe = SleepForWakingUp();
    id event = GetEvent(whoWakesMe);
    HandleEvent(event);
}

RunLoop 來(lái)幫助線程管理一個(gè)或多個(gè)事件或消息,接受用戶輸入等事件源,在事件到達(dá)時(shí),RunLoop 立刻喚醒線程來(lái)處理事件;沒(méi)有事件需要處理時(shí),RunLoop 幫助線程休眠,避免其占用資源,這里是幫助其休眠,而不是直接退出。
RunLoop 還決定了程序在何時(shí)應(yīng)該處理那些事件,并且為被調(diào)用的對(duì)象維護(hù)一個(gè)消息隊(duì)列,被調(diào)用方從這個(gè)消息隊(duì)列中取出需要他處理的事件。

主線程的 RunLoop 默認(rèn)開(kāi)啟,而子線程需要調(diào)用[NSRunLoop currentRunLoop]創(chuàng)建和獲取 RunLoop,RunLoop 的銷毀發(fā)生在線程結(jié)束時(shí)。

RunLoop 與線程的關(guān)系
每個(gè)線程創(chuàng)建的時(shí)候,都有一個(gè) RunLoop 循環(huán),與線程一一對(duì)應(yīng)。

RunLoop 構(gòu)成

RunLoop構(gòu)成

如圖可以看到 RunLoop 的大致構(gòu)成,它與線程一一對(duì)應(yīng),而擁有多個(gè)CFRunLoopMode,mode 是一系列輸入事件源、計(jì)時(shí)器、runLoop 觀察者的集合。

RunLoop Mode

RunLoop 只能選擇一個(gè) Mode 啟動(dòng),同時(shí)在“跑”的時(shí)候,總是在特定的唯一的 mode 下,每次運(yùn)行 RunLoop 都要顯式或隱式的指定運(yùn)行 mode。這個(gè) mode 包含了當(dāng)前需要處理的 Source/Timer/Observer,所以 RunLoop 在時(shí)刻內(nèi),僅能處理與當(dāng)前 mode 相關(guān)聯(lián)的事件,只有和模式相關(guān)的源才會(huì)被監(jiān)視,并允許他們傳遞事件消息。

為了保證其中的 Source/Timer/Observer 與其他 mode 的相隔離,切換 mode 時(shí),只能先退出當(dāng)前RunLoop,再以要切換的 mode 重新進(jìn)入RunLoop。

開(kāi)發(fā)中,通常會(huì)遇到這幾種Mode:

  • kCFRunLoopDefaultMode:app的默認(rèn) Mode,通常主線程在這個(gè) Mode 下運(yùn)行。
  • UITrackingRunLoopMode:界面跟蹤 Mode,ScrollView 的觸摸滑動(dòng) mode (在iOS中,觸摸滑動(dòng)很流暢的原因是在滑動(dòng)時(shí),只處理此 mode 下的事件且不受其他mode影響)。
  • UIInitializationRunLoopMode:剛啟動(dòng) app 進(jìn)入的第一個(gè) mode,起到過(guò)渡的作用,啟動(dòng)完成后不再使用。
  • GSEventReceiveRunLoopMode: Graphic 相關(guān)事件的 mode,通常用不到。
  • kCFRunLoopCommonModes:將 mode 標(biāo)記為"common"屬性,當(dāng) RunLoop 運(yùn)行在標(biāo)記為"common"屬性的任一 mode 下,發(fā)生事件時(shí),里面的 mode 都會(huì)被觸發(fā)。
RunLoop Source

線程的異步事件源,數(shù)據(jù)源。有兩種Source,可以用是否基于Mach Port(進(jìn)程間通訊接口)區(qū)分:

  • source0:不基于Mach Port,處理app內(nèi)部事件,用戶自定義的thread發(fā)出。當(dāng)我們使用 NSObject 中的 performSelector 系列方法時(shí),都是source0 事件源。
  • source1:基于Mach Port,是由RunLoop和內(nèi)核管理的。
RunLoop Timer

線程的同步事件源,在預(yù)設(shè)的時(shí)間點(diǎn)到了之后同步的發(fā)給線程處理此事件。

RunLoop Observer

Observer 可對(duì) RunLoop 的狀態(tài)變化進(jìn)行觀察,可觀察的變化:

  • 剛進(jìn)入此 RunLoop 中
  • RunLoop 準(zhǔn)備處理一個(gè) Timer
  • RunLoop 準(zhǔn)備處理一個(gè) Input Source
  • RunLoop 準(zhǔn)備進(jìn)入睡眠
  • RunLoop 將被喚醒處理事件之前
  • RunLoop 準(zhǔn)備退出

因?yàn)镺bserver可對(duì)這些事件進(jìn)行觀察追蹤,所以也可被看作是一種事件源。

RunLoop處理的流程

RunLoop_1.png

第7步中,當(dāng)線程進(jìn)入休眠,發(fā)生下列事件,線程將被喚醒:

  • 基于 Port 的事件發(fā)生
  • 計(jì)時(shí)器到時(shí)
  • 被代碼顯式喚醒

第9步中,處理喚醒時(shí)收到的消息,并且:

  • 如果是用戶定義的計(jì)時(shí)器到時(shí),處理事件并重啟 RunLoop
  • 如果有input 事件源,傳遞這個(gè)消息
  • 如果runloop顯式被喚醒,且沒(méi)有超時(shí),重啟RunLoop
    之后,跳回第2步

RunLoop應(yīng)用舉例

在漫長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)的理論說(shuō)明后,讓我們看看實(shí)際開(kāi)發(fā)中,有哪些地方會(huì)用到 RunLoop 呢?

解決 NSTimer "不準(zhǔn)"的問(wèn)題

我們有時(shí)候會(huì)發(fā)現(xiàn) NSTimer "不太準(zhǔn)",明明時(shí)間已經(jīng)到了,該執(zhí)行的回調(diào)卻未發(fā)生,這是因?yàn)槲覀兂3?NSTimer 默認(rèn)設(shè)置為default mode,如果這時(shí)屏幕滾動(dòng),mode切換為TrackingMode,時(shí)間到了,但是 TrackingMode 無(wú)法處理 defaultMode下的回調(diào),造成"不準(zhǔn)"。
在 SVProgressHUD 中,我們可以設(shè)置轉(zhuǎn)圈的提示框自動(dòng)消失,可開(kāi)啟一個(gè)定時(shí)器,在到了設(shè)定的時(shí)間點(diǎn)后消失,如下

strongSelf.fadeOutTimer = [NSTimer timerWithTimeInterval:duration target:strongSelf selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:strongSelf.fadeOutTimer forMode:NSRunLoopCommonModes];

strongSelf 即為提示框,將它消失的定時(shí)器添加在 RunLoop 的common 模式下,不管時(shí)間點(diǎn)到了的那一時(shí)刻 RunLoop 運(yùn)行在哪個(gè)mode下,都會(huì)處理消失的回調(diào),"準(zhǔn)點(diǎn)消失"。

用 dispatch_after 定時(shí),就準(zhǔn)了嗎
我發(fā)現(xiàn)有很多博客寫(xiě),NSTimer 造成定時(shí)不準(zhǔn)的問(wèn)題可以通過(guò) GCD 中的 dispatch_after 來(lái)解決,但是 dispatch_after 并不是說(shuō)在指定時(shí)間后執(zhí)行處理,而只是在指定時(shí)間將操作追加到 Dispatch Queue 中。如果指定時(shí)間到了,需要加入的隊(duì)列正在進(jìn)行耗時(shí)操作,定時(shí)操作并不能立即執(zhí)行,也會(huì)造成不準(zhǔn)。
驗(yàn)證如下:

    //獲取主隊(duì)列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //定時(shí)時(shí)間
    int64_t delay = 5 * NSEC_PER_SEC;
    //定時(shí)時(shí)間,即從現(xiàn)在到定時(shí)的時(shí)間
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);

    NSLog(@"開(kāi)始計(jì)時(shí): %@", [NSDate date]);

    dispatch_after(delayTime, mainQueue, ^{
        NSLog(@"時(shí)間到: %@", [NSDate date]);
    });

    //在這里設(shè)置一些復(fù)雜操作,比方來(lái)10000次網(wǎng)絡(luò)請(qǐng)求


可以看到雖然我們只設(shè)置延遲5秒進(jìn)行,但事實(shí)上,在10秒才進(jìn)行了延遲操作。但是日常的開(kāi)發(fā)中,碰到這么這么復(fù)雜的情況應(yīng)該是比較少的,所以 dispatch_after 也可以一用~~~
GCD 中除了主要的 Dispatch Queue 之外,還對(duì) BSD 系內(nèi)核慣有功能 kqueue 進(jìn)行包裝,可處理內(nèi)核中發(fā)生的各種事件及方法。
其中的 DISPATCH_SOURCE_TYPE_TIMER 可作為定時(shí)器,幫助我們延遲調(diào)用:


    //獲取主隊(duì)列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //新生成一個(gè)定時(shí)器,且此定時(shí)器不能為局部變量,否則方法執(zhí)行完就被銷毀了,還怎么做定時(shí)后的回調(diào)呢?
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
    //定時(shí)時(shí)間
    int64_t delay = 5 * NSEC_PER_SEC; 
    //一定容差范圍時(shí)間
    int64_t leeway = 0.1 * NSEC_PER_SEC; 
    //定時(shí)時(shí)間,即從現(xiàn)在到定時(shí)的時(shí)間
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);

    //設(shè)置定時(shí)器
    //下一次回調(diào)為DISPATCH_TIMER_FOREVER,表示不需要重復(fù)
    dispatch_source_set_timer(self.timer, delayTime,DISPATCH_TIMER_FOREVER, leeway);

    //設(shè)置時(shí)間到了后的回調(diào)
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.timer, ^{
        typeof(self) strongSelf = weakSelf;
        NSLog(@"計(jì)時(shí)結(jié)束: %@", [NSDate date]);
        dispatch_source_cancel(strongSelf.timer);
    });

    //啟動(dòng)定時(shí)器
    dispatch_resume(self.timer);

保證線程的持續(xù)運(yùn)行

在 AFNetworking 2.3 中,需要一個(gè)自定義線程接受 connection 回調(diào),一開(kāi)始初始化線程時(shí),沒(méi)有需要執(zhí)行的操作,線程會(huì)退出(RunLoop中沒(méi)有source/timer/observer 會(huì)立即退出)。為其添加一個(gè)MachPort,為了保證線程的存活。

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        //初始化線程時(shí),調(diào)用networkRequestThreadEntryPoint方法
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        //為線程創(chuàng)建RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        //為RunLoop添加事件,保證其持續(xù)運(yùn)行
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}
解決TableView加載圖片時(shí),滑動(dòng)很卡

TableView 需要加載大量圖片時(shí),滑動(dòng)后,界面會(huì)卡,這是因?yàn)榇藭r(shí)RunLoop 運(yùn)行在 UITrackingRunLoopMode 下,圖片加載在當(dāng)前mode下,cpu 又要處理加載圖片事件,又要處理滑動(dòng)事件,造成卡頓。
可以顯式地將圖片的加載設(shè)置在 NSDefaultRunLoopMode 下,滑動(dòng)時(shí)的 UITrackingRunLoopMode 并不會(huì)去加載圖片,解決卡頓問(wèn)題。

[self.imageView performSelector:@selector(setImage:) withObject:downloadImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
自動(dòng)釋放池到底在何時(shí)釋放?

我們知道,手動(dòng)指定 autoreleasepool 中的對(duì)象,會(huì)在作用域結(jié)束時(shí)釋放掉。而設(shè)置為 autorelease 的對(duì)象是在出了作用域之后,被自動(dòng)添加到最近創(chuàng)建的自動(dòng)釋放池中。那么這個(gè)自動(dòng)釋放池遲早有被撐滿需要釋放的時(shí)刻,這個(gè)自動(dòng)釋放池具體是什么時(shí)候被釋放呢?

在Cocoa框架中,相當(dāng)于程序主循環(huán)的NSRunLoop或者在其他程序可運(yùn)行的地方,對(duì) NSAutoreleasePool 對(duì)象進(jìn)行生成、持有和廢棄處理。
---引自《Objective-C 高級(jí)編程》
而它能夠釋放的原因是系統(tǒng)在每個(gè) runloop 迭代中都加入了自動(dòng)釋放池 Push 和 Pop

下面我們舉例討論下:

@property (nonatomic,weak)NSString * weakStr;

- (void)viewDidLoad {
        [super viewDidLoad];
        NSString *string = [NSString stringWithFormat:@"這個(gè)string要設(shè)置的很長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)"];
        //因?yàn)樘O(píng)果引用Tagged Pointer專門存儲(chǔ)小的對(duì)象,直接存儲(chǔ)其值,而不是存儲(chǔ)地址
        //如果string很短,用Tagged Pointer存儲(chǔ),無(wú)法驗(yàn)證其自動(dòng)釋放,地址被收回的過(guò)程
        weakStr = string;

        NSLog(@"viewDidLoad:%@",weakStr);
        NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@",weakStr);
    NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@",weakStr);
    NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}

輸出如圖:


在mode改變,RunLoop一次循環(huán)結(jié)束后,autorelease對(duì)象被銷毀
觀察 weakStr 設(shè)置方法何時(shí)被調(diào)用
在viewWillAppear調(diào)用結(jié)束后,左邊的堆棧中出現(xiàn)了一次AutoreleasePoolPage pop操作

我們?cè)趘iewDidLoad方法中,用stringWithFormat類方法生成一個(gè)字符串,這種方法生成的字符串默認(rèn)被添加進(jìn) autoreleasepool 中。
viewDidLoad 和 viewWillAppear 還在app初始化的 UIInitializationRunLoopMode 下,而 viewDidAppear 已經(jīng)進(jìn)入了默認(rèn)mode下了。期間,autoreleasepool 出現(xiàn)了一次銷毀,其中的對(duì)象也就被銷毀了。
所以說(shuō),在沒(méi)有手加Autorelease Pool的情況下,Autorelease對(duì)象是在當(dāng)前的runloop迭代結(jié)束時(shí)釋放的,而它能夠釋放的原因是系統(tǒng)在每個(gè)runloop迭代中都加入了自動(dòng)釋放池Push和Pop。

關(guān)于RunLoop的一道題
NSRunLoop 的描述正確的是( )
A. RunLoop 決定程序在何時(shí)應(yīng)該處理哪些 Event
B. Cocoa 中的 NSRunLoop 類并不是線程安全的
C. RunLoop 可以使程序一直運(yùn)行接受用戶輸入
D. RunLoop 起到了調(diào)用解耦的作用
我怎么覺(jué)得 ABCD 四個(gè)選項(xiàng)都對(duì)嘞……

參考文章:
RunLoops 官方文檔
深入理解RunLoop
黑幕背后的Autorelease
Objective-C Autorelease Pool 的實(shí)現(xiàn)原理
RunLoop個(gè)人小結(jié)

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

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

  • 前言 最近離職了,可以盡情熬夜寫(xiě)點(diǎn)總結(jié),不用擔(dān)心第二天上班爽并蛋疼著,這篇的主角 RunLoop 一座大山,涵蓋的...
    zerocc2014閱讀 12,546評(píng)論 13 67
  • Runloop是iOS和OSX開(kāi)發(fā)中非常基礎(chǔ)的一個(gè)概念,從概念開(kāi)始學(xué)習(xí)。 RunLoop的概念 -般說(shuō),一個(gè)線程一...
    小貓仔閱讀 1,111評(píng)論 0 1
  • 轉(zhuǎn)載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,558評(píng)論 0 13
  • 彈出框在移動(dòng)端開(kāi)發(fā)中使用是比較頻繁的控件之一 1、在iOS8.0之前使用最多的原生彈出框控件是:UIAlertVi...
    郭偉_技術(shù)與產(chǎn)品閱讀 1,095評(píng)論 0 1
  • 長(zhǎng)大時(shí)念及故鄉(xiāng),更多的是對(duì)家鄉(xiāng)親人的牽掛和對(duì)兒時(shí)往事的依戀了。—— 題記 又是豌豆花開(kāi)時(shí)節(jié),想來(lái)我的家鄉(xiāng)又是田間地...
    紫如意閱讀 2,108評(píng)論 15 5

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