iOS-Runloop1-Runloop

一. RunLoop相關(guān)

什么是Runloop绿满?
顧名思義,Runloop就是運行循環(huán),就是在程序運行過程中循環(huán)做一些事情。

RunLoop.png

RunLoop的基本作用:

保持程序的持續(xù)運行
處理App中的各種事件(比如觸摸事件戚篙、定時器事件等)
節(jié)省CPU資源,提高程序性能:該做事時做事挽封,該休息時休息
......

1. UIApplicationMain里面的RunLoop

如果沒有RunLoop

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

執(zhí)行完打印已球,就會退出程序臣镣。

如果有了RunLoop

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

UIApplicationMain函數(shù)里面就是創(chuàng)建一個RunLoop對象辅愿,然后RunLoop對象一直循環(huán)等待和處理消息,偽代碼如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            //睡眠中等待消息
            int message = sleep_and_wait();
            //處理消息
            retVal = process_message(message);
        } while (0 == retVal);
        return 0;
    }
}

這樣就保證了程序并不會馬上退出忆某,一直保持運行狀態(tài)点待。

2. RunLoop對象

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

Foundation:NSRunLoop
Core Foundation:CFRunLoopRef

獲取RunLoop對象:

Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象

Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
  1. NSRunLoop和CFRunLoopRef都代表著RunLoop對象
  2. NSRunLoop是基于CFRunLoopRef的一層OC包裝
  3. CFRunLoopRef是開源的:Core Foundation源碼

他們兩者之間的關(guān)系如下圖:

NSRunLoop和CFRunLoopRef

NSRunLoop就是對CFRunLoopRef的一層包裝,就好像NSArray是對CFArrayRef的封裝弃舒,NSString是對CFStringRef的封裝一樣癞埠。

獲取當(dāng)前線程、主線程RunLoop方法如下:

NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
NSLog(@"%@", [NSRunLoop mainRunLoop]);

打恿亍:

0x600001141260 0x600001141260
0x600000940a00 0x600000940a00
<CFRunLoop 0x600000940a00 [0x1089e6ae8]>{wakeup port = 0x1d07, stopped = false, ignoreWakeUps = false, 
current mode = kCFRunLoopDefaultMode,
......

打印的地址不一樣苗踪,可以發(fā)現(xiàn)使用%@打印mainRunLoop,里面裝的的確是CFRunLoop削锰,通過CFRunLoop的地址也可以驗證:NSRunLoop的確是堆CFRunLoopRef的一層封裝通铲,所以地址不一樣。

3. RunLoop與線程

  1. 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象器贩,主線程的RunLoop已經(jīng)自動獲取(創(chuàng)建)颅夺,子線程默認(rèn)沒有開啟RunLoop,RunLoop會在線程結(jié)束時銷毀蛹稍。
  2. RunLoop是懶加載的吧黄,線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建唆姐。
  3. 主線程幾乎所有的事情都是交給了runloop去做拗慨,比如UI界面的刷新、點擊時間的處理奉芦、performSelector等等赵抢。
  4. RunLoop保存在一個全局的Dictionary里,線程作為key仗阅,RunLoop作為value昌讲。

下載Core Foundation源碼,拖到新創(chuàng)建的命令行項目中减噪,查看CFRunLoopGetCurrent源碼實現(xiàn):

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

進入_CFRunLoopGet0:

static CFMutableDictionaryRef __CFRunLoops = NULL; //是個字典短绸,線程作為key车吹,取出對應(yīng)的RunLoop
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //將字典和key(線程)傳入CFDictionaryGetValue函數(shù),獲取RunLoop對象
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) { //如果RunLoop不存在
    CFRunLoopRef newLoop = __CFRunLoopCreate(t); //就創(chuàng)建RunLoop
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且將RunLoop保存到字典里面
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

其中__CFRunLoops是個字典,線程作為key醋闭,取出對應(yīng)的RunLoop窄驹,如果沒有,再創(chuàng)建证逻,然后保存到字典中乐埠,驗證了RunLoop對象的確是懶加載的。

4. RunLoop相關(guān)的類

Core Foundation中關(guān)于RunLoop的5個類

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

在源碼中查看囚企,CFRunLoopRef:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

可以發(fā)現(xiàn)CFRunLoopRef是指向__CFRunLoop結(jié)構(gòu)體的指針丈咐,找到__CFRunLoop結(jié)構(gòu)體源碼:

struct __CFRunLoop {
    ......
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;//當(dāng)前模式
    CFMutableSetRef _modes; //是個集合,無序的龙宏,里面裝的是CFRunLoopModeRef類型的對象
    ......
};

__CFRunLoop里面有個_modes棵逊,它是個集合,里面裝的是一堆CFRunLoopModeRef類型的對象银酗,當(dāng)前的模式是_currentMode辆影。

集合是無序的,數(shù)組是有序的黍特,集合只能通過anyObject取值蛙讥,數(shù)組可以通過索引取值,如下:

// 有序的
NSMutableArray *array;
[array addObject:@"123"];
array[0];

// 無序的
NSMutableSet *set;
[set addObject:@"123"];
[set anyObject];

進入CFRunLoopModeRef:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    ......
    CFStringRef _name;//名稱
    CFMutableSetRef _sources0;//里面裝的是CFRunLoopSourceRef類型的對象
    CFMutableSetRef _sources1;//里面裝的是CFRunLoopSourceRef類型的對象
    CFMutableArrayRef _observers;//里面裝的是CFRunLoopObserverRef類型的對象
    CFMutableArrayRef _timers;//里面裝的是CFRunLoopTimerRef類型的對象
    ......
};

① 5個類之間的關(guān)系:

CFRunLoopRef里面有個_modes集合灭衷,里面裝好多CFRunLoopModeRef類型的模式次慢,_currentMode是當(dāng)前模式。

模式里面有name今布,_sources0经备、_sources1集合(里面裝的是CFRunLoopSourceRef類型的東西),_observers數(shù)組(里面裝的是CFRunLoopObserverRef類型的東西)部默,_timers數(shù)組(里面裝的是CFRunLoopTimerRef類型的東西)侵蒙。

如下圖所示:

結(jié)構(gòu).png
  1. CFRunLoopModeRef代表RunLoop的運行模式
  2. 一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer
  3. RunLoop啟動時只能選擇其中一個Mode傅蹂,作為currentMode纷闺,如果需要切換Mode,只能退出當(dāng)前Loop份蝴,再重新選擇一個Mode進入
  4. 不同組的Source0/Source1/Timer/Observer能分隔開來犁功,互不影響
  5. 如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
  6. 其中Timer是定時器婚夫,平時創(chuàng)建的一些定時器都放在這里浸卦,Observer是監(jiān)聽器,source0/Source1是事件案糙,比如點擊事件限嫌、performSelector等等靴庆。

② 為什么多種模式要分開呢?

比如scrollView滾動的時候讓它切換到滾動模式怒医,那么在滾動模式下炉抒,scrollView就專心處理滾動相關(guān)的就可以了,以前模式下的事情就不處理了稚叹。如果不滾動焰薄,在正常模式下,就專心處理正常模式下的事情就好了扒袖,這樣可以做到流暢不卡頓塞茅。

5. 常見的兩種Mode

  1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認(rèn)Mode,通常主線程是在這個Mode下運行僚稿。

  2. UITrackingRunLoopMode:界面跟蹤 Mode凡桥,用于 ScrollView 追蹤觸摸滑動蟀伸,保證界面滑動時不受其他 Mode 影響蚀同。

常用的就是默認(rèn)模式和界面跟蹤模式,其他模式基本用不到啊掏,都懶得說了蠢络。

6. Source0、Source1迟蜜、Timers刹孔、Observers

RunLoop切換到某個模式就開始處理那個模式的Source0、Source1娜睛、Timers髓霞、Observers,那么這些東西分別代表什么呢畦戒?

Source0:觸摸事件處理方库、performSelector:onThread:。
Source1:基于Port(端口)的線程間通信障斋、系統(tǒng)事件捕捉 (比如點擊事件纵潦,通過Source1捕捉,然后包裝成Source0進行處理)垃环。
Timers:NSTimer邀层、performSelector:withObject:afterDelay:(底層就是NSTimer)。
Observers:用于監(jiān)聽RunLoop的狀態(tài)遂庄、UI刷新(BeforeWaiting)寥院、Autorelease pool(BeforeWaiting)。

比如涛目,點擊界面空白就是Source0事件秸谢,驗證如下:

在touchesBegan:withEvent:方法打斷點

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
 //打斷點
}

查看函數(shù)調(diào)用棧:

函數(shù)調(diào)用棧.png

可以發(fā)現(xiàn)的確是通過source0處理事件经磅,最后才調(diào)用到touchesBegan:withEvent:方法的。

關(guān)于Observers監(jiān)聽UI刷新(BeforeWaiting):
比如以下代碼就屬于UI刷新:

self.view.backgroundColor = [UIColor redColor];

這句代碼并不是立馬執(zhí)行钮追,Observers會先記下來预厌,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop將要睡覺之前執(zhí)行(刷新UI)元媚。

同理Autorelease pool也是一樣轧叽,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop睡覺之前釋放對象刊棕。

7. RunLoop狀態(tài)

上面說了炭晒,Observers可以監(jiān)聽RunLoop的狀態(tài),那么RunLoop有幾種狀態(tài)呢甥角?

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

8. 如何監(jiān)聽RunLoop的狀態(tài)网严?

Observers數(shù)組里面有系統(tǒng)創(chuàng)建的一些Observer,用于監(jiān)聽RunLoop狀態(tài)進行UI刷新嗤无、Autorelease pool等震束,如果我們自己想監(jiān)聽RunLoop狀態(tài)肯定要自己創(chuàng)建Observer。

// 創(chuàng)建Observer
/*
參數(shù)一:一般傳默認(rèn)的:kCFAllocatorDefault
參數(shù)二:傳入你想監(jiān)聽什么狀態(tài)
參數(shù)三:是否重復(fù)監(jiān)聽
參數(shù)四:順序0当犯,不需要考慮順序
參數(shù)五:監(jiān)聽函數(shù)名
參數(shù)六:context垢村,會傳入監(jiān)聽函數(shù)的info里面,一般傳NULL
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式嚎卫,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);

//監(jiān)聽RunLoop的函數(shù)嘉栓,這個函數(shù)要求傳入三個參數(shù)
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}

當(dāng)然也可以通過block監(jiān)聽,和上面一樣的:

// 創(chuàng)建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
       ......
    }
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式拓诸,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
① 監(jiān)聽點擊空白事件

點擊空白侵佃,打印如下:

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources 即將處理source事件
-[ViewController touchesBegan:withEvent:]
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
......

可以看出先是kCFRunLoopBeforeSources狀態(tài),之后再執(zhí)行點擊事件方法奠支,這也驗證了點擊事件是source0事件馋辈。

② 監(jiān)聽定時器事件

添加定時器:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定時器-----------");
    }];
//    NSLog(@"%s",__func__);
}

點擊空白,打优呋隆:

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting 先睡覺
kCFRunLoopAfterWaiting 3秒后喚醒
定時器-----------
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources

可以發(fā)現(xiàn)首有,先睡覺,睡了3秒后再執(zhí)行定時器事件枢劝。

③ 監(jiān)聽模式切換

主view里面添加一個textView井联,寫如下代碼監(jiān)聽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;
        }
        default:
            break;
    }
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);

滑動textView您旁,打永映!:

kCFRunLoopExit - kCFRunLoopDefaultMode
kCFRunLoopEntry - UITrackingRunLoopMode
kCFRunLoopExit - UITrackingRunLoopMode
kCFRunLoopEntry - kCFRunLoopDefaultMode

可以發(fā)現(xiàn),先退出默認(rèn)模式進入滑動模式,再退出滑動模式進入默認(rèn)模式蚕脏。

二. RunLoop的運行流程

1. 流程圖

蘋果官方的流程圖:

RunLoop的運行流程.png

官方的圖有點抽象侦副,看MJ老師的圖:

RunLoop的運行流程.png

對于第4步的block就是下面的block

CFRunLoopPerformBlock(<#CFRunLoopRef rl#>, <#CFTypeRef mode#>, ^{
    //傳入RunLoop和模式,RunLoop就會執(zhí)行這個block里面的代碼
})

2. 源碼驗證

仔細(xì)分析上圖流程之后驼鞭,下面我們查看源碼秦驯,看看到底是不是這樣的,但是RunLoop的入口是哪個呢挣棕?打斷點译隘,查看函數(shù)調(diào)用棧,如下:

函數(shù)調(diào)用棧

可以發(fā)現(xiàn)第一個關(guān)于RunLoop的函數(shù)是CFRunLoopRunSpecific洛心,是Core Foundation框架下的函數(shù)固耘。在Core Foundation源碼中搜索“ CFRunLoopRunSpecific”,由于原函數(shù)比較復(fù)雜词身,精簡后如下:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    //通知Observers進入Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //具體要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知Observers退出Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    //返回事情做完后的結(jié)果
    return result;
}

進入__CFRunLoopRun函數(shù)厅目,這個函數(shù)更復(fù)雜,精簡后如下:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {
        // 通知Observers法严,即將處理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知Observers损敷,即將處理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 處理Source0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 如果返回YES,處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        // 判斷有無Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有Source1就跳轉(zhuǎn)handle_msg
            goto handle_msg;
        }
        
        // 通知Observers渐夸,即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 開始休眠
        __CFRunLoopSetSleeping(rl);
        
        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
        
        // 等待別的消息來喚醒當(dāng)前線程嗤锉,往下走代表有人喚醒它了
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        // 結(jié)束休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知Observers,結(jié)束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:
        
       if (被Timer喚醒) {
           // 處理Timers
           __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD喚醒) {
           // 處理GCD相關(guān)
           __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Source1喚醒
           // 處理Source1
           __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }
        
        // 再處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
    
        // 設(shè)置返回值retVal
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
        
        // 如果retVal等于0墓塌,繼續(xù)執(zhí)行循環(huán)
    } while (0 == retVal);
    
    return retVal;
}

上面源代碼對比流程圖可知,流程的確如圖所示奥额。

3. 小細(xì)節(jié)

__CFRunLoopDoObservers函數(shù)中真正做通知Observers相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函數(shù)

__CFRunLoopDoBlocks函數(shù)中真正做Block相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK函數(shù)

__CFRunLoopDoSources0函數(shù)中真正做Source0相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION函數(shù)

__CFRunLoopDoTimers函數(shù)中真正做定時器相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函數(shù)

__CFRunLoopDoSource函數(shù)中真正做Source1相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION函數(shù)

我們也可以通過函數(shù)調(diào)用棧來驗證:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123"); // 斷點
    }];
}

在斷點處查看函數(shù)調(diào)用棧:

定時器函數(shù)調(diào)用棧

可以發(fā)現(xiàn)的確調(diào)用了CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函數(shù)苫幢,之后Core Foundation框架的函數(shù)就調(diào)用完了。

一般情況下GCD的東西是GCD來處理的垫挨,不會交給RunLoop韩肝。GCD是GCD,RunLoop是RunLoop九榔,他們互不干擾哀峻,但是有一種情況下GCD是交給RunLoop處理的,如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // 子線程處理一些邏輯

        // 回到主線程去刷新UI界面
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"11111111111"); // 斷點
        });
    });
}

函數(shù)調(diào)用棧:

GCD處理

當(dāng)我們在子線程處理一些邏輯然后回到主線程去刷新UI界面哲泊,這種情況就會交給RunLoop去處理GCD相關(guān)的東西剩蟀,然后再回到GCD。

③ 線程休眠

當(dāng)RunLoop事情處理完發(fā)現(xiàn)沒有事情干切威,就會進入休眠育特,這時候你可以認(rèn)為是“線程阻塞”,這時候代碼就不會往下走了先朦,直到被喚醒缰冤,才會繼續(xù)往下走犬缨。

這種休眠和while(1){};不一樣,這種休眠是當(dāng)前線程休息了棉浸,CPU不再給當(dāng)前線程分配資源怀薛,但是while(1){};這種阻塞是一個死循環(huán),這時候線程沒有休息迷郑,還在一直執(zhí)行while(1){}乾戏。

那么線程休眠是怎么做到的呢?
同樣查看源碼三热,進入__CFRunLoopServiceMachPort函數(shù):

......
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
......

上面的mach_msg是內(nèi)核層面的API鼓择,內(nèi)核層面的API是不給我們程序員使用的,因為比較危險就漾,給我們使用的都是應(yīng)用層面的API呐能。

RunLoop休眠的實現(xiàn)原理,如下圖:

RunLoop休眠的實現(xiàn)原理.png

當(dāng)用戶態(tài)調(diào)用mach_msg()函數(shù)會自動切換到內(nèi)核態(tài)抑堡,會真正調(diào)用內(nèi)核態(tài)的mach_msg()函數(shù)摆出,內(nèi)核態(tài)mach_msg做的事就是:沒有消息就讓線程休眠,有消息就喚醒線程首妖,喚醒線程后就又回到用戶態(tài)偎漫。所以線程休眠就是因為用戶態(tài)和內(nèi)核態(tài)的切換

三. RunLoop在實際開發(fā)中的應(yīng)用

  1. 解決NSTimer在滑動時停止工作的問題
  2. 控制線程生命周期(線程庇欣拢活)
  3. 監(jiān)控應(yīng)用卡頓
  4. 性能優(yōu)化

先講前兩種象踊,后面兩種講到性能優(yōu)化再說。

1. 解決NSTimer在滑動時停止工作的問題

定時器失效問題:
在view上添加一個scrollView棚壁,寫如下代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
}

不拖動scrollView杯矩,定時器一直在打印,拖動scrollView發(fā)現(xiàn)定時器停止工作了袖外。
這是因為RunLoop只會在一種模式下工作史隆,默認(rèn)情況下定時器被添加到NSDefaultRunLoopMode模式下,但是拖動的時候RunLoop切換成UITrackingRunLoopMode模式曼验,但是定時器沒有添加到這個模式下泌射,所以定時器不會工作。

解決辦法也很簡單鬓照,就是在兩種模式下都添加定時器熔酷,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];

    // NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
    // NSRunLoopCommonModes并不是一個真的模式颖杏,它只是一個標(biāo)記
    // timer能在_commonModes數(shù)組中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

{
    //scheduledTimerWithTimeInterval默認(rèn)會把定時器添加到默認(rèn)模式下纯陨,所以用timerWithTimeInterval
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
}

注意,NSRunLoopCommonModes并不是一個真的模式,它只是一個標(biāo)記翼抠,定時器能在_commonModes數(shù)組中存放的模式下工作咙轩。

如果timer被標(biāo)記為commonModes,那么timer就能在_commonModes數(shù)組中存放的模式下工作阴颖,而_commonModes數(shù)組中存放的恰好是NSDefaultRunLoopMode和UITrackingRunLoopMode模式活喊,當(dāng)然這兩種模式也在_modes數(shù)組里面。
能在commonModes“模式”下工作的東西都會被添加到_commonModeItems數(shù)組中量愧,比如上面的timer钾菊。

再看一下__CFRunLoop結(jié)構(gòu)體源碼:

struct __CFRunLoop {
    ......
    pthread_t _pthread;
    CFMutableSetRef _commonModes; //就是這個_commonModes數(shù)組
    CFMutableSetRef _commonModeItems; //能在commonModes“模式”下工作的東西都會被添加到這個數(shù)組中
    CFRunLoopModeRef _currentMode;//當(dāng)前模式
    CFMutableSetRef _modes; //是個集合,無序的偎肃,里面裝的是CFRunLoopModeRef類型的對象
    ......
};

如果timer是NSDefaultRunLoopMode或者UITrackingRunLoopMode模式煞烫,那么它就會被添加到自己模式下的_timers數(shù)組中。
進入CFRunLoopModeRef:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    ......
    CFStringRef _name;//名稱
    CFMutableSetRef _sources0;//里面裝的是CFRunLoopSourceRef類型的對象
    CFMutableSetRef _sources1;//里面裝的是CFRunLoopSourceRef類型的對象
    CFMutableArrayRef _observers;//里面裝的是CFRunLoopObserverRef類型的對象
    CFMutableArrayRef _timers;//里面裝的是CFRunLoopTimerRef類型的對象
    ......
};

關(guān)于線程崩鬯蹋活請看下篇文章滞详。

四. 面試題

  1. 講講 RunLoop,項目中有用到嗎紊馏?

  2. runloop內(nèi)部實現(xiàn)邏輯料饥?
    看流程圖

  3. runloop和線程的關(guān)系?
    一對一的關(guān)系

  4. timer 與 runloop 的關(guān)系朱监?
    ① RunLoop對象里面有個_modes數(shù)組岸啡,里面放一堆模式,模式里面會放timer赫编,如果timer被標(biāo)記為commonModes巡蘸,那么timer就能在_commonModes數(shù)組中存放的模式下工作,能在commonModes“模式”下工作的東西都會被添加到_commonModeItems數(shù)組里中沛慢。
    ② 如果線程休眠了赡若,timer也可以喚醒休眠的RunLoop。

  5. 程序中添加每3秒響應(yīng)一次的NSTimer团甲,當(dāng)拖動tableview時timer可能無法響應(yīng)要怎么解決?
    將timer被標(biāo)記為commonModes

  6. runloop 是怎么響應(yīng)用戶操作的黍聂,具體流程是什么樣的躺苦?
    當(dāng)用戶有個點擊事件,這個系統(tǒng)事件會先被Source1捕捉产还,Source1捕捉之后會包裝成事件隊列(EventQuene)匹厘,再放到Source0里面進行處理,然后RunLoop循環(huán)再處理Source0里面的事件脐区。

  7. 說說runLoop的幾種狀態(tài)

  8. runloop的mode作用是什么愈诚?
    不同模式的Source0/Source1/Timer/Observer能分隔開來,互不影響

Demo地址:RunLoop1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市炕柔,隨后出現(xiàn)的幾起案子酌泰,更是在濱河造成了極大的恐慌,老刑警劉巖匕累,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陵刹,死亡現(xiàn)場離奇詭異,居然都是意外死亡欢嘿,警方通過查閱死者的電腦和手機衰琐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炼蹦,“玉大人羡宙,你說我怎么就攤上這事∑” “怎么了狗热?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瑟枫。 經(jīng)常有香客問我斗搞,道長,這世上最難降的妖魔是什么慷妙? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任僻焚,我火速辦了婚禮,結(jié)果婚禮上膝擂,老公的妹妹穿的比我還像新娘虑啤。我一直安慰自己,他們只是感情好架馋,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布狞山。 她就那樣靜靜地躺著,像睡著了一般叉寂。 火紅的嫁衣襯著肌膚如雪萍启。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天屏鳍,我揣著相機與錄音勘纯,去河邊找鬼。 笑死钓瞭,一個胖子當(dāng)著我的面吹牛驳遵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播山涡,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼堤结,長吁一口氣:“原來是場噩夢啊……” “哼唆迁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竞穷,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤唐责,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后来庭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妒蔚,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年月弛,在試婚紗的時候發(fā)現(xiàn)自己被綠了肴盏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡帽衙,死狀恐怖菜皂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厉萝,我是刑警寧澤恍飘,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站谴垫,受9級特大地震影響章母,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翩剪,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一乳怎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧前弯,春花似錦蚪缀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浙巫,卻和暖如春金蜀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背的畴。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工廉油, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苗傅。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像班巩,于是被迫代替她去往敵國和親渣慕。 傳聞我的和親對象是個殘疾皇子嘶炭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容