10_RunLoop詳解

RunLoop簡介

  • RunLoop 是一個運行循環(huán),內部類似do-while循環(huán)饼问,線程進入并使用它來運行響應輸入事件的事件處理程序。
  • 作用是保持程序的持續(xù)運行〉樵撸可以處理 App 中的各種事件(如觸摸事件浅悉、定時器事件趟据、selector 事件),節(jié)省了 cpu 資源术健,提高了程序的性能汹碱,該做事時做事該休息時候休息。
  • 在程序的入口UIApplicationMain函數內部就啟動了一個RunLoop荞估,所以函數一直沒有返回咳促,保持了程序的持續(xù)運行。

RunLoop與線程的關系

  • 每條線程都有唯一一個與之對應的 RunLoop 對象勘伺。
  • 主線程的RunLoop已經自動創(chuàng)建好了跪腹,子線程的RunLoop會在第一次獲取它時創(chuàng)建并啟動,在線程結束時銷毀飞醉。
  • RunLoop保存在一個全局的Dictionary里冲茸,線程作為key,RunLoop作為value缅帘。

RunLoop相關API

iOS中有2套API來訪問和使用RunLoop轴术,

  • Core Foundation: CFRunLoopRef
  • Foundation: NSRunLoop (NSRunLoop是基于CFRunLoopRef的一層 OC 包裝)

RunLoop相關類

Core Foundation中提供了關于RunLoop的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

內存結構和關系如下圖:


image.png
image.png
屏幕快照 2018-07-18 下午3.18.24.png
CFRunLoopModeRef
  • CFRunLoopModeRef代表Runloop的運行模式。
  • runloop啟動之后會選擇一種運行模式钦无,只能指定其中一個 Mode逗栽,作為 currentMode。
  • 一個RunLoop包含若干個 Mode铃诬,每個 Mode 又包含若干個Source0/Source1/Timer/Observer祭陷。
  • 如果當前Mode中沒有任何Sources0/Sources1/Timer/Observer苍凛,RunLoop會立馬退出。
  • 如果需要切換Mode兵志,只能退出Loop醇蝴,再重新指定一個Mode進入,以保證不同Mode的Source/Timer/Observer分隔開來,互不影響想罕。
  • 系統(tǒng)默認提供的Run Loop Modes有:
    1悠栓、kCFRunLoopDefaultMode(NSDefaultRunLoopMode):系統(tǒng)默認的Runloop Mode,例如進入iOS程序默認不做任何操作就處于這種Mode中按价。
    2惭适、 UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動楼镐,保證界面滑動時不受其他 Mode 影響癞志。(除非你將其他Source/Timer設置到UITrackingRunLoopMode下)
    3、kCFRunLoopCommonModes(NSRunLoopCommonModes): 其實這個并不是某種具體的Mode框产,而是一個占位用的Mode凄杯,是一種模式組合,在iOS系統(tǒng)中默認包含了NSDefaultRunLoopMode和UITrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下秉宿,而是相當于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode
    4戒突、 UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用描睦。
    5膊存、 GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內部 Mode,通常用不到忱叭。
CFRunLoopSourceRef

事件源(輸入源)
在 iOS 中有兩種分類方法隔崎,按照以前的分類方法可以分為:①基于端口的;②自定義的窑多;③performSelector事件仍稀;
按照函數調用棧來劃分,可以分為source0和soucr1埂息。

  • source0:非基于端口的事件源,(負責App內部事件遥巴,由App負責管理觸發(fā)千康,例如UITouch事件)
  • Source1:端口事件源,可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息铲掐,基于Port的線程間通信
    系統(tǒng)事件捕捉拾弃,它能夠主動喚醒RunLoop(由操作系統(tǒng)內核進行管理,例如CFMessagePort消息)摆霉。
CFRunLoopTimerRef
  • CFRunLoopTimerRef基于時間的觸發(fā)器豪椿,基本上來說就是NSTimer奔坟,受RunLoop的Model的影響。
  • 注意:GCD的定時器不受RunLoop的Model的影響
  • NSTimer添加定時器有兩種方法:
    1搭盾、timerWithXXX
   + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
   + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
   + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

創(chuàng)建 timer咳秉,需要手動把 timer 添加到 RunLoop 中,可以指定添加到哪種模式下鸯隅。
2澜建、 scheduledTimerWithXXX

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

這種方法除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當前線程RunLoop中,但是如果滾動UIScrollView(UITableView蝌以、UICollectionview等類似的)是無法正常工作的炕舵,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作。

注意:
  • 非主線程的RunLoop并不會自動創(chuàng)建跟畅,直到第一次使用咽筋,RunLoop運行必須要在加入NSTimer輸入后運行否則會直接退出。
  • performSelector:withObject:afterDelay:執(zhí)行的本質還是通過創(chuàng)建一個NSTimer然后加入到當前線程徊件。類似的方法還有performSelector:onThread:withObject:afterDelay:晤硕,它會在另一個線程的RunLoop中創(chuàng)建一個Timer),此方法事實上在任務執(zhí)行完之前會對觸發(fā)對象形成引用庇忌,任務執(zhí)行完進行釋放舞箍。(注意:performSelector: withObject:等方法則等同于直接調用,原理與此不同)皆疹。

CFRunLoopObserverRef

相當于消息循環(huán)中的一個監(jiān)聽器疏橄,隨時通知外部當前RunLoop的運行狀態(tài)(它包含一個函數指針callout將當前狀態(tài)及時告訴觀察者)。具體的Observer狀態(tài)如下

         kCFRunLoopEntry = (1UL << 0),        即將進入runloop
         kCFRunLoopBeforeTimers = (1UL << 1), 即將處理timer事件
         kCFRunLoopBeforeSources = (1UL << 2),即將處理source事件
         kCFRunLoopBeforeWaiting = (1UL << 5),即將進入睡眠,一般在這個狀態(tài)進行UI界面的刷新和對自動釋放池進行release操作
         kCFRunLoopAfterWaiting = (1UL << 6), 被喚醒
         kCFRunLoopExit = (1UL << 7),         runloop退出
         kCFRunLoopAllActivities = 0x0FFFFFFFU

和定時器類似,runLoop 觀察者可以只用一次或循環(huán)使用略就。若只用一次,那么在 它啟動后,會把它自己從 runLoop 里面移除,而循環(huán)的觀察者則不會捎迫。你在創(chuàng)建 runLoop 觀察者的時候需要指定它是運行一次還是多次。
給runloop添加觀察者代碼:

    // 創(chuàng)建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
            {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
            }
                break;
            case kCFRunLoopExit:
            {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
            }
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting");
                break;
            default:
                break;
        }
    });
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 釋放
    CFRelease(observer);

Run Loop 的運行邏輯

屏幕快照 2018-07-18 下午3.33.50.png

RunLoop休眠的實現原理

屏幕快照 2018-07-18 下午3.39.58.png

在開發(fā)中如何使用RunLoop表牢?什么應用場景窄绒?

  • 解決NSTimer在滑動時停止工作的問題
  • 監(jiān)控應用卡頓
  • 性能優(yōu)化
  • 控制線程生命周期(線程保活)崔兴,封裝代碼如下
//LZPermenantThread.h
#import <Foundation/Foundation.h>

typedef void (^LZPermenantThreadTask)(void);
@interface LZPermenantThread : NSObject

/**
 開啟線程
 */
//- (void)run;

/**
 在當前子線程執(zhí)行一個任務
 */
- (void)executeTask:(LZPermenantThreadTask)task;

/**
 結束線程
 */
- (void)stop;

@end

//LZPermenantThread.m
#import "LZPermenantThread.h"

@interface LZPermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end

@implementation LZPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[NSThread alloc] initWithBlock:^{
            
            // 創(chuàng)建上下文(要初始化一下結構體)
            CFRunLoopSourceContext context = {0};
            
            // 創(chuàng)建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            // 往Runloop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            // 銷毀source
            CFRelease(source);
            
            // 啟動
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
            
//            while (weakSelf && !weakSelf.isStopped) {
//                // 第3個參數:returnAfterSourceHandled彰导,設置為true,代表執(zhí)行完source后就會退出當前l(fā)oop
//                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
//            }
        }];
        
        [self.innerThread start];
    }
    return self;
}

//- (void)run
//{
//    if (!self.innerThread) return;
//
//    [self.innerThread start];
//}

- (void)executeTask:(LZPermenantThreadTask)task
{
    if (!self.innerThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(LZPermenantThreadTask)task
{
    task();
}

@end

自動釋放池與RunLoop

kCFRunLoopEntry; // 當runloop進入的時候會創(chuàng)建一個自動釋放
kCFRunLoopBeforeWaiting; // 當runloop即將進入休眠的時候會把之前的自動釋放池先銷毀敲茄,然后創(chuàng)建一個新的自動釋放池位谋。
kCFRunLoopExit; // 當runloop退出的時候會把之前的自動釋放池銷毀。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末堰燎,一起剝皮案震驚了整個濱河市掏父,隨后出現的幾起案子,更是在濱河造成了極大的恐慌秆剪,老刑警劉巖赊淑,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爵政,死亡現場離奇詭異,居然都是意外死亡陶缺,警方通過查閱死者的電腦和手機钾挟,發(fā)現死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來组哩,“玉大人等龙,你說我怎么就攤上這事×娣。” “怎么了蛛砰?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長黍衙。 經常有香客問我泥畅,道長,這世上最難降的妖魔是什么琅翻? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任位仁,我火速辦了婚禮,結果婚禮上方椎,老公的妹妹穿的比我還像新娘聂抢。我一直安慰自己,他們只是感情好棠众,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布琳疏。 她就那樣靜靜地躺著,像睡著了一般闸拿。 火紅的嫁衣襯著肌膚如雪空盼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天新荤,我揣著相機與錄音揽趾,去河邊找鬼。 笑死苛骨,一個胖子當著我的面吹牛篱瞎,可吹牛的內容都是我干的。 我是一名探鬼主播智袭,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼奔缠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吼野?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤两波,失蹤者是張志新(化名)和其女友劉穎瞳步,沒想到半個月后闷哆,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡单起,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年抱怔,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘀倒。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡屈留,死狀恐怖,靈堂內的尸體忽然破棺而出测蘑,到底是詐尸還是另有隱情灌危,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布碳胳,位于F島的核電站勇蝙,受9級特大地震影響脚囊,放射性物質發(fā)生泄漏蓖墅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一谬莹、第九天 我趴在偏房一處隱蔽的房頂上張望诫惭。 院中可真熱鬧翁锡,春花似錦、人聲如沸夕土。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隘弊。三九已至哈踱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梨熙,已是汗流浹背开镣。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咽扇,地道東北人邪财。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像质欲,于是被迫代替她去往敵國和親树埠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內容