iOS高性能OC五:Runloop

一.基本概念

1.runloop是一個(gè)事件驅(qū)動(dòng)的循環(huán),收到事件就去處理,沒有事件就進(jìn)入睡眠.
2.應(yīng)用一啟動(dòng)主線程被創(chuàng)建后,主線程對(duì)應(yīng)的runloop也被創(chuàng)建,runloop也保證了程序能夠一直運(yùn)行.之后創(chuàng)建的子線程默認(rèn)是沒有runloop的,只有當(dāng)調(diào)用[NSRunLoop currentRunLoop]去獲取的時(shí)候才被創(chuàng)建.
3.runloop的模式:
mode一共五種:

  • NSDefaultRunLoopMode 默認(rèn)
  • UITrackingRunLoopMode UI專用
  • UIInitializationRunLoopMode 在剛啟動(dòng)App時(shí)第進(jìn)入的第一個(gè)Mode君丁,啟動(dòng)完成后就不再使用
  • GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode
  • NSRunLoopCommonModes 這是一個(gè)占位用的Mode而线,不是一種真正的Mode

首先是這樣一個(gè)例子:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(tim) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
    UITextView *tv = [[UITextView alloc]init];
    tv.backgroundColor = [UIColor lightGrayColor];
    tv.text = @"aasdasdsdasdsdasasdasdasdasdasdasdasaasdasdsdasdsdasasdas";
    tv.frame = CGRectMake(0, 100, 50, 50);
    [self.view addSubview:tv];

timer添加在default模式中,默認(rèn)是正常執(zhí)行的,當(dāng)滑動(dòng)textView的時(shí)候,timer就停下來(lái)了.
模式類似于跑道,一個(gè)runloop同時(shí)只能在一個(gè)跑道上進(jìn)行,并且模式有優(yōu)先級(jí),UITrackingRunLoopMode是UI模式,是優(yōu)先級(jí)最高的模式,當(dāng)runloop遇到事件的時(shí)候,會(huì)根據(jù)事件指定的模式,去切換跑到,UITrackingRunLoopMode的優(yōu)先級(jí)最高,所以就切換到了這個(gè)模式上,這時(shí)候其他模式下的事件就不會(huì)被執(zhí)行.
并且UITrackingRunLoopMode只能被UI事件喚醒,如果將上面的timer指定的模式改成UI模式,那么默認(rèn)是不會(huì)執(zhí)行的,但是當(dāng)滑動(dòng)textView時(shí),跑到就切換到了UI模式,timer就可以執(zhí)行了,一旦停止滑動(dòng),timer又會(huì)停 下來(lái).
NSRunLoopCommonModes實(shí)質(zhì)就是NSDefaultRunLoopMode和UITrackingRunLoopMode,等同于在兩個(gè)模式上都添加一遍,不管是那個(gè)跑道,都會(huì)遇到timer的事件.

NSThread *th = [[NSThread alloc]initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(tim) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"runloop后面");
    }];
    [th start];

在子線程獲取runloop,由于UI只在主線程進(jìn)行,主線程的runloop和子線程的runloop自然是沒有關(guān)系的,此時(shí)使用NSDefaultRunLoopMode不再受到UI事件影響.
一旦調(diào)用run方法,runloop就會(huì)開始執(zhí)行,子線程也不會(huì)被銷毀,run后面的代碼在runloop停下來(lái)之前也不會(huì)被執(zhí)行.

NSThread *th = [[NSThread alloc]initWithBlock:^{
        self.shouldStop = NO;
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(tim) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        while(!self.shouldStop){
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.0001]];
        }
        NSLog(@"runloop后面");
    }];
    [th start];

換成runUntilDate方法,runloop只會(huì)執(zhí)行一段時(shí)間,放在while循環(huán)里就可以進(jìn)行控制.

二.Runloop的結(jié)構(gòu)

一個(gè)RunLoop包含了多個(gè)Mode客峭,也就是跑道,每個(gè)Mode又包含了若干個(gè)Source/Timer/Observer倍踪。每次調(diào)用 RunLoop的主函數(shù)時(shí)竟终,只能指定其中一個(gè)Mode,這個(gè)Mode被稱作CurrentMode退疫。如果需要切換 Mode参歹,只能退出Loop,再重新指定一個(gè)Mode.
runloop的結(jié)構(gòu)如下:

struct __CFRunLoop {
     pthread_t _pthread;//線程
    CFMutableSetRef _commonModes;     // commonModes下的兩個(gè)mode(kCFRunloopDefaultMode和UITrackingMode)
    CFMutableSetRef _commonModeItems; // 在commonModes狀態(tài)下運(yùn)行的對(duì)象(例如Timer)
    CFMutableSetRef _modes;           // 運(yùn)行的所有模式(CFRunloopModeRef類)
    CFRunLoopModeRef _currentMode;//在當(dāng)前l(fā)oop下運(yùn)行的mode
    ...
};

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

Source/Timer/Observer是事件源, 也就是要執(zhí)行的任務(wù),RunLoop里面保存的是RunLoopMode岔霸,而RunLoopMode中保存的才是實(shí)際執(zhí)行的任務(wù).

@property (nonatomic, strong) dispatch_source_t timer;

self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
  dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"lala");
    });
dispatch_resume(self.timer);

這段代碼使用GCD創(chuàng)建了一個(gè)timer,這里就出現(xiàn)了source
source分為兩種

  • source0:觸摸事件薛躬,PerformSelectors,非基于Port的
  • source1:系統(tǒng)內(nèi)核事件,基于Port的線程間通信

observer:
observer是runloop的監(jiān)聽者,能夠監(jiān)聽這幾種狀態(tài)

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入
    kCFRunLoopBeforeTimers = (1UL << 1),//即將處理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即將處理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即將進(jìn)入睡眠
    kCFRunLoopAfterWaiting = (1UL << 6),//剛從睡眠中蘇醒
    kCFRunLoopExit = (1UL << 7),//即將退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)
};

監(jiān)聽runloop需要使用到CoreFoundation 下的CFRunLoop,首先看創(chuàng)建CFRunLoopObserverRef的代碼

CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
static CFRunLoopObserverRef observer;
    observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
  • 首先需要獲取CFRunLoopRef指針,也就是當(dāng)前runloop
  • 然后調(diào)用函數(shù)CFRunLoopObserverCreate(<#CFAllocatorRef allocator#>, <#CFOptionFlags activities#>, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext context#>)
    CFAllocatorRef allocator直接使用NULL,
    CFOptionFlags activities指的是前面說(shuō)到的可以監(jiān)聽的幾種狀態(tài),
    Boolean repeats是否持續(xù)監(jiān)聽,
    CFIndex order用0,
    CFRunLoopObserverCallBack callout是回調(diào)函數(shù),需要一個(gè)C函數(shù)指針,函數(shù)的類型是typedef void (
    CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
    CFRunLoopObserverContext *context是上下文,它的結(jié)構(gòu)是這樣的
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;

CFRunLoopObserverContext的info就是函數(shù)CFRunLoopObserverCallBack的參數(shù)info.
最后調(diào)用CFRunLoopAddObserver函數(shù)來(lái)執(zhí)行

創(chuàng)建一個(gè)Observer觀察者添加到主線程RunLoop的CommonMode模式下觀察呆细,創(chuàng)建一個(gè)持續(xù)的子線程專門用來(lái)監(jiān)控主線程的RunLoop狀態(tài)型宝,一旦發(fā)現(xiàn)進(jìn)入睡眠前的KCFRunLoopBeforeSource狀態(tài),或者喚醒后的狀態(tài)KCFRunLoopAfterWaiting絮爷,在設(shè)置的時(shí)間閾值內(nèi)一直沒有變化趴酣,即可判斷為卡頓,根據(jù)堆棧的信息坑夯,可以分析出是哪個(gè)方法比較耗時(shí).

三.Runloop與其他模塊相關(guān)

  • 釋放池
    程序啟動(dòng)后,主線程 RunLoop 里注冊(cè)了兩個(gè) Observer岖寞,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler(),一是,監(jiān)測(cè)Entry(即將進(jìn)入Loop)狀態(tài),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池,其 order 是-2147483647柜蜈,優(yōu)先級(jí)最高慎璧,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前床嫌;另外一個(gè),Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池胸私;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池厌处。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低岁疼,保證其釋放池子發(fā)生在其他所有回調(diào)之后.

  • UI事件
    蘋果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件阔涉,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
    當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后捷绒,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收瑰排。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸暖侨,加速椭住,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程字逗。隨后蘋果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào)京郑,并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。
    _UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā)葫掉,其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等些举。通常事件比如 UIButton 點(diǎn)擊、touchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的俭厚。

  • 手勢(shì)
    _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí)户魏,其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理挪挤。蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件叼丑,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer扛门,并執(zhí)行GestureRecognizer的回調(diào)幢码。
    當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí),這個(gè)回調(diào)都會(huì)進(jìn)行相應(yīng)處理尖飞。

  • GCD
    當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí)症副,libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息,RunLoop會(huì)被喚醒政基,并從消息中取得這個(gè) block贞铣,并在回調(diào)里執(zhí)行這個(gè) block。Runloop只處理主線程的block沮明,dispatch 到其他線程仍然是由 libDispatch 處理的辕坝。

  • performSelector
    除了基于端口的源,Cocoa定義了自定義輸入源荐健,允許你在任何線程執(zhí)行selector方法酱畅。和基于端口的源一樣琳袄,執(zhí)行selector請(qǐng)求會(huì)在目標(biāo)線程上序列化,減緩許多在線程上允許多個(gè)方法容易引起的同步問(wèn)題纺酸。不像基于端口的源窖逗,一個(gè)selector執(zhí)行完后會(huì)自動(dòng)從run loop里面移除。
    當(dāng)在其他線程上面執(zhí)行selector時(shí)餐蔬,目標(biāo)線程須有一個(gè)活動(dòng)的run loop碎紊。對(duì)于你創(chuàng)建的線程,這意味著線程在你顯式的啟動(dòng)run loop之前是不會(huì)執(zhí)行selector方法的樊诺,而是一直處于休眠狀態(tài)仗考。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市词爬,隨后出現(xiàn)的幾起案子秃嗜,更是在濱河造成了極大的恐慌,老刑警劉巖顿膨,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锅锨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡虽惭,警方通過(guò)查閱死者的電腦和手機(jī)橡类,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門蛇尚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芽唇,“玉大人,你說(shuō)我怎么就攤上這事取劫〈殷裕” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵谱邪,是天一觀的道長(zhǎng)炮捧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)惦银,這世上最難降的妖魔是什么咆课? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮扯俱,結(jié)果婚禮上书蚪,老公的妹妹穿的比我還像新娘。我一直安慰自己迅栅,他們只是感情好殊校,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著读存,像睡著了一般为流。 火紅的嫁衣襯著肌膚如雪呕屎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天敬察,我揣著相機(jī)與錄音秀睛,去河邊找鬼。 笑死静汤,一個(gè)胖子當(dāng)著我的面吹牛琅催,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虫给,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼藤抡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了抹估?” 一聲冷哼從身側(cè)響起缠黍,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎药蜻,沒想到半個(gè)月后瓷式,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡语泽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年贸典,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片踱卵。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡廊驼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惋砂,到底是詐尸還是另有隱情妒挎,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布西饵,位于F島的核電站酝掩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏眷柔。R本人自食惡果不足惜期虾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驯嘱。 院中可真熱鬧镶苞,春花似錦、人聲如沸宙拉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至煌贴,卻和暖如春御板,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牛郑。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工怠肋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人淹朋。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓笙各,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親础芍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杈抢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348