iOS多線程--RunLoop

1 RunLoop簡介

神秘的RunLoop碧注。一個應用開始運行以后放在那里贪婉,如果不對它進行任何操作,這個應用就像靜止了一樣济炎,不會自發(fā)的有任何動作發(fā)生幻锁,但是如果我們點擊界面上的一個按鈕凯亮,這個時候就會有對應的按鈕響應事件發(fā)生。給我們的感覺就像應用一直處于隨時待命的狀態(tài)哄尔,在沒人操作的時候它一直在休息假消,在讓它干活的時候,它就能立刻響應岭接。其實富拗,這就是RunLoop的功勞。
RunLoop實際上是一個對象鸣戴,這個對象在循環(huán)中用來處理程序運行過程中出現的各種事件(比如說觸摸事件啃沪、UI刷新事件、定時器事件窄锅、Selector事件)创千,從而保持程序的持續(xù)運行;而且在沒有事件處理的時候入偷,會進入睡眠模式签餐,從而節(jié)省CPU資源,提高程序性能盯串。

2 RunLoop和線程

說說線程氯檐,有些線程執(zhí)行的任務是一條直線,起點到終點体捏;而另一些線程要干的活則是一個圓冠摄,不斷循環(huán)糯崎,直到通過某種方式將它終止。在iOS中河泳,圓型的線程就是通過RunLoop不停的循環(huán)實現的沃呢。RunLoop和線程是緊密相連的,可以這樣說RunLoop是為了線程而生拆挥,沒有線程薄霜,它就沒有存在的必要。

  • 一條線程對應一個RunLoop對象纸兔,每條線程(包括主線程)都有唯一一個與之對應的RunLoop對象惰瓜。
  • 我們只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop汉矿。
  • RunLoop對象在第一次獲取RunLoop時創(chuàng)建崎坊,銷毀則是在線程結束的時候。
  • 主線程的RunLoop對象系統(tǒng)自動幫助我們創(chuàng)建好了洲拇,而子線程的RunLoop對象需要我們主動創(chuàng)建奈揍。

RunLoop的偽代碼表現方式為如下:

int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 執(zhí)行各種任務,處理各種事件
        // ......
    } while (running);
    return 0;
}

2.1 主線程的RunLoop對象默認創(chuàng)建及啟動

iOS的應用程序里面赋续,程序啟動后會有一個如下的main() 函數:

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

重點是UIApplicationMain() 函數男翰,這個方法會為main thread 設置一個NSRunLoop 對象,這就解釋了本文開始說的為什么我們的應用可以在無人操作的時候休息纽乱,需要讓它干活的時候又能立馬響應奏篙。

2.2 子線程的RunLoop對象需手動創(chuàng)建及啟動

需要更多的線程交互則可以手動配置和啟動,如果線程只是去執(zhí)行一個長時間的已確定的任務則不需要迫淹。

//獲取(創(chuàng)建)當前線程的RunLoop
NSRunLoop  *runloop = [NSRunLoop currentRunLoop];
//啟動
[runloop run];

(有待后續(xù)例證秘通,先把概念貼上)Cocoa中的NSRunLoop類并不是線程安全的,我們不能在一個線程中去操作另外一個線程的run loop對象,那很可能會造成意想不到的后果敛熬。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的肺稀,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:- (CFRunLoopRef)getCFRunLoop;獲取對應的CFRunLoopRef類应民,來達到線程安全的目的话原。

3 RunLoop相關類和方法

下面我們來了解一下Core Foundation框架下關于RunLoop的5個類,只有弄懂這幾個類的含義诲锹,我們才能深入了解RunLoop運行機制繁仁。

CFRunLoopRef:代表RunLoop的對象
CFRunLoopModeRef:RunLoop的運行模式
CFRunLoopSourceRef:就是RunLoop模型圖中提到的輸入源/事件源
CFRunLoopTimerRef:就是RunLoop模型圖中提到的定時源
CFRunLoopObserverRef:觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變

NSRunLoop關系類圖.png

一個RunLoop對象(CFRunLoopRef)中包含若干個運行模式(CFRunLoopModeRef)归园,簡稱Mode黄虱。而每一個運行模式下又包含若干個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)庸诱、觀察者(CFRunLoopObserverRef)捻浦,這三個統(tǒng)稱為Mode Item晤揣。

  • 每次RunLoop啟動時,只能指定其中一個運行模式(CFRunLoopModeRef)朱灿,這個運行模式(CFRunLoopModeRef)被稱作CurrentMode昧识。如果需要切換運行模式(CFRunLoopModeRef),只能退出Loop盗扒,再重新指定一個運行模式(CFRunLoopModeRef)進入跪楞。這樣做主要是為了分隔開不同組的Mode Item,不同mode下的mode item互不影響侣灶。
  • 一個 Item可被加入不同的Mode甸祭。但一個 Item 被重復加入同一個 Mode 時是不會有效果的。如果一個 Mode 中一個 Item 都沒有炫隶,RunLoop退出。(不過如果僅僅依賴沒有Mode Item來讓RunLoop退出阎曹,這做法是不可靠的)

3.1 CFRunLoopRef RunLoop對象

CFRunLoopRef就是Core Foundation框架下RunLoop對象類伪阶。我們可通過以下方式來獲取RunLoop對象:

CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

在Foundation框架下獲取RunLoop對象類的方法如下:

[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象

由蘋果源碼(參看下圖),這兩個函數的內部實現看出:
(1)線程和RunLoop是一一對應的处嫌,這種對應關系用一個字典保存起來栅贴,key是pthread,value是CFRunLoopRef熏迹。
(2) RunLoop在第一次獲取時創(chuàng)建檐薯,然后在線程結束時銷毀。
(3)子線程如果不手動獲取RunLoop注暗,它是一直都不會有的坛缕。

image.png
image.png

3.2 CFRunLoopModeRef 運行模式

CFRunLoopMode 的結構大致如下:

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

系統(tǒng)默認定義了多種運行模式(CFRunLoopModeRef),如下:

  1. kCFRunLoopDefaultMode:App的默認運行模式捆昏,通常主線程是在這個運行模式下運行
  2. UITrackingRunLoopMode:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動赚楚,保證界面滑動時不受其他Mode影響)
  3. UIInitializationRunLoopMode:在剛啟動App時第進入的第一個 Mode,啟動完成后就不再使用
  4. GSEventReceiveRunLoopMode:接受系統(tǒng)內部事件骗卜,通常用不到
  5. kCFRunLoopCommonModes:是一種常用的模式集合宠页,包含 NSDefaultRunLoopMode 、UITrackingRunLoopMode寇仓。這個模式存在的好處是举户,如果現在異步線程有個timer啟動,不需要再所有的 RunLoop Mode 中都去加一遍遍烦,只需要直接在 NSRunLoopCommonModes 加一次即可俭嘁。

其中kCFRunLoopDefaultMode、UITrackingRunLoopMode服猪、kCFRunLoopCommonModes是我們開發(fā)中需要用到的模式兄淫。

RunLoop 其實內部就是do-while循環(huán)屯远,在這個循環(huán)內部不斷地處理各種任務(比如Source、Timer捕虽、Observer)慨丐,通過判斷result的值實現的。所以 可以看成是一個死循環(huán)泄私。如果沒有RunLoop房揭,UIApplicationMain 函數執(zhí)行完畢之后將直接返回,就是說程序一啟動然后就結束晌端。

//CFRunLoopRun
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunInMode方法可以設置runLoop運行在哪個mode下modeName捅暴,超時時間seconds,以及是否處理完事件就返回returnAfterSourceHandled咧纠。
但這兩個方法實際調用的是同一個方法CFRunLoopRunSpecific蓬痒,其返回是一個SInt32類型的值,根據返回值漆羔,來決定runLoop的運行狀況梧奢。

3.2.1 Mode 及操作接口

CFRunLoopModeRef 類并沒有對外暴露,只是通過 CFRunLoopRef 的接口進行了封裝演痒。CFRunLoopRef 獲取 Mode 的接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

我們沒有辦法直接創(chuàng)建一個CFRunLoopMode對象亲轨,但是我們可以調用CFRunLoopAddCommonMode 傳入一個字符串向 RunLoop 中添加 Mode,傳入的字符串即為 Mode 的名字鸟顺,Mode對象應該是此時在RunLoop內部創(chuàng)建的惦蚊。

這里看一下CFRunLoopAddCommonMode源碼。

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已經有這個mode讯嫂,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //這里調用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會調用
            //__CFRunLoopFindMode(rl, modeName, true)蹦锋,CFRunLoopMode對象在這個時候被創(chuàng)建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

可以看得出:

  • modeName不能重復,modeName是mode的唯一標識符
  • RunLoop的_commonModes數組存放所有被標記為common的mode的名稱
  • 添加commonMode會把commonModeItems數組中的所有source同步到新添加的mode中
  • CFRunLoopMode對象在CFRunLoopAddItemsToCommonMode函數中調用CFRunLoopFindMode時被創(chuàng)建

3.2.2 mode item 及操作接口

Source/Timer/Observer 被統(tǒng)稱為 mode item欧芽,一個 item 可以被同時加入多個 mode晕粪。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有渐裸,則 RunLoop 會直接退出巫湘,不進入循環(huán)。

Mode 暴露的管理 mode item 的接口有下面幾個昏鹃,通過他們我們可以為RunLoop 添加 Source(ModeItem)尚氛。

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)

你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時洞渤,RunLoop會自動幫你創(chuàng)建對應的 CFRunLoopModeRef阅嘶。對于一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。

這里只分析其中 CFRunLoopAddSource 的源碼

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在讯柔,則copy一個新的復制給set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems為空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把傳入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果剛才set copy到的數組里有數據
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //則把set里的所有mode都執(zhí)行一遍__CFRunLoopAddItemToCommonModes函數
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的邏輯就是抡蛙,如果你往kCFRunLoopCommonModes里面添加一個source,那么所有_commonModes里的mode都會添加這個source
    } else {
        //根據modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在魂迄,則初始化_sources0粗截,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含傳入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,則加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1捣炬,則加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此處只有在加到source1的時候才會把souce和一個mach_port_t對應起來
                    //可以理解為熊昌,source1可以通過內核向其端口發(fā)送消息來主動喚醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

通過添加source的這段代碼可以得出如下結論:

  • 如果modeName傳入kCFRunLoopCommonModes,則該source會被保存到RunLoop的_commonModeItems中
  • 如果modeName傳入kCFRunLoopCommonModes湿酸,則該source會被添加到所有commonMode中
  • 如果modeName傳入的不是kCFRunLoopCommonModes婿屹,則會先查找該Mode,如果沒有推溃,會創(chuàng)建一個
  • 同一個source在一個mode中只能被添加一次

3.3 輸入事件來源

RunLoop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)昂利,兩種源都使用程序的某一特定的處理例程來處理到達的事件。下圖是官方RunLoop模型圖铁坎,顯示了RunLoop的概念結構以及各種源蜂奸。
RunLoop就是線程中的一個循環(huán),RunLoop在循環(huán)中會不斷檢測厢呵,通過Input sources(輸入源)和Timer sources(定時源)兩種來源等待接受事件窝撵;然后對接受到的事件通知線程進行處理傀顾,并在沒有事件的時候進行休息襟铭。


官方RunLoop模型圖

在啟動RunLoop之前,必須添加監(jiān)聽的輸入源事件或者定時源事件短曾,否則調用[runloop run]會直接返回寒砖,而不會進入循環(huán)讓線程長駐。具體可參看基于端口的輸入源和定時源的demo實例嫉拐。

3.3.1 CFRunLoopSourceRef 輸入源

輸入源常見的有:基于端口的輸入源哩都、自定義輸入源、Cocoa上的Selector源婉徘。

3.3.1.1 基于端口的輸入源

Cocoa和Core Foundation內置支持使用端口相關的對象和函數來創(chuàng)建的基于端口的源漠嵌。例如,在Cocoa里面你從來不需要直接創(chuàng)建輸入源盖呼。你只要簡單的創(chuàng)建端口對象儒鹿,并使用NSPort的方法把該端口添加到RunLoop。端口對象會自己處理創(chuàng)建和配置輸入源几晤。

- (void)showDemo4
{
    // 創(chuàng)建線程约炎,并調用run1方法執(zhí)行任務
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void) run1
{
    // 這里寫任務
    NSLog(@"----run1-----%@",[NSThread currentThread]);
    //端口添加到RunLoop
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //啟動RunLoop(在啟動之前一定是添加了輸入源或者定時源的,[runloop run]會直接返回)
    [[NSRunLoop currentRunLoop] run];
    // 測試是否開啟了RunLoop,如果開啟RunLoop圾浅,則來不了這里掠手,因為RunLoop開啟了循環(huán)。
    NSLog(@"-------------");
}

#這段也可以只作為了解#在Core Foundation狸捕,你必須人工創(chuàng)建端口和它的RunLoop源喷鸽。我們可以使用端口相關的函數(CFMachPortRef,CFMessagePortRef府寒,CFSocketRef)來創(chuàng)建合適的對象魁衙。下面的例子展示了如何創(chuàng)建一個基于端口的輸入源,將其添加到RunLoop并啟動:

void createPortSource()
{
    CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);
    CFRunLoopSourceRef source =  CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

3.3.1.2 自定義輸入源(只作為了解即可株搔,具體還沒有實踐)

在Core Foundation程序中剖淀,必須使用CFRunLoopSourceRef類型相關的函數來創(chuàng)建自定義輸入源,接著使用回調函數來配置輸入源纤房。Core Fundation會在恰當的時候調用回調函數纵隔,處理輸入事件以及清理源。常見的觸摸炮姨、滾動事件等就是該類源捌刮,由系統(tǒng)內部實現。一般我們不會使用該種源舒岸,第三種情況已經滿足我們的需求绅作。

除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制蛾派。源的這部分運行在單獨的線程里面俄认,并負責在數據等待處理的時候傳遞數據給源并通知它處理數據。消息傳遞機制的定義取決于你洪乍,但最好不要過于復雜眯杏。創(chuàng)建并啟動自定義輸入源的示例如下:

void createCustomSource()
{
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

用button的點擊事件來舉個例子,看點擊時程序的調用棧壳澳,可以看到點擊事件是由source0來處理的岂贩。


button點擊是source0輸入源.png

3.3.1.3 Cocoa上的Selector源

Cocoa允許你在任何線程執(zhí)行selector方法。和基于端口的源一樣巷波,執(zhí)行selector請求會在目標線程上序列化萎津,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于端口的源抹镊,一個selector執(zhí)行完后會自動從RunLoop里面移除锉屈。

當在其他線程上面執(zhí)行selector時,目標線程須有一個活動的RunLoop髓考。對于你創(chuàng)建的線程部念,這意味著線程在你顯式的啟動RunLoop之前是不會執(zhí)行selector方法的,而是一直處于休眠狀態(tài)。
NSObject類提供了類似如下的selector方法:

/// 主線程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

/// 指定線程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

/// 針對當前線程
performSelector:withObject:afterDelay:         
performSelector:withObject:afterDelay:inModes:

/// 取消儡炼,在當前線程妓湘,和上面兩個方法對應
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

還要注意以下這些selector方法,它們是同步執(zhí)行的乌询,和線程無關榜贴,主線程子線程都可以用。不會添加到runloop妹田,而是直接執(zhí)行唬党,相當于是[self xxx]這樣調用,只不過是編譯期鬼佣、運行期處理的不同驶拱。

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

3.3.2 CFRunLoopTimerRef 定時源

CFRunLoopTimerRef 是基于時間的觸發(fā)器,它和 NSTimer 是 Toll-Free Bridged 的晶衷,可以混用蓝纲。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時晌纫,RunLoop會注冊對應的時間點税迷,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調锹漱。

需要注意的是箭养,盡管定時器可以產生基于時間的通知,但它并不是實時機制哥牍。和輸入源一樣毕泌,定時器也和你的RunLoop的特定模式相關。如果定時器所在的模式當前未被RunLoop監(jiān)視砂心,那么定時器將不會開始直到RunLoop運行在相應的模式下懈词。類似的蛇耀,如果定時器在RunLoop處理某一事件期間開始辩诞,定時器會一直等待直到下次RunLoop開始相應的處理程序。如果RunLoop不再運行纺涤,那定時器也將永遠不啟動译暂。

創(chuàng)建定時器源有兩種方法,
方法一:

NSTimer *timer = [NSTimer timerWithTimeInterval:4.0
                                                   target:self
                                                   selector:@selector(backgroundThreadFire:) 
                                                   userInfo:nil
                                                   repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

//scheduledTimer方式下撩炊,NSTimer會自動被加入到了RunLoop的NSDefaultRunLoopMode模式下
[NSTimer scheduledTimerWithTimeInterval:10
                                       target:self
                                       selector:@selector(backgroundThreadFire:)
                                       userInfo:nil
                                       repeats:YES];

具體使用舉例如下:

/**
 * 用來展示CFRunLoopModeRef和CFRunLoopTimerRef的結合使用
 */
- (void)showDemo1
{
    // 定義一個定時器外永,約定兩秒之后調用self的run方法
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    // 將定時器添加到當前RunLoop的NSDefaultRunLoopMode下,一旦RunLoop進入其他模式,定時器timer就不工作了
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // 將定時器添加到當前RunLoop的UITrackingRunLoopMode下拧咳,只在拖動情況下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // 將定時器添加到當前RunLoop的NSRunLoopCommonModes下伯顶,定時器就會跑在被標記為Common Modes的模式下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    // 調用了scheduledTimer返回的定時器,已經自動被加入到了RunLoop的NSDefaultRunLoopMode模式下。
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}

- (void)run
{
    NSLog(@"---run");
    
}

3.4 CFRunLoopSourceRef的數據結構source0祭衩、source1

雖然按3.3介紹的輸入源分好幾種灶体,但數據結構只有兩類(source0、source1)掐暮。
數據結構(source0/source1):

// source0 (manual): order(優(yōu)先級)蝎抽,callout(回調函數)
CFRunLoopSource {order =..., {callout =... }}

// source1 (mach port):order(優(yōu)先級),port:(端口), callout(回調函數)
CFRunLoopSource {order = ..., {port = ..., callout =...}

(1) source0 非基于port的:是app內部的消息機制路克,負責App內部事件樟结,由App負責管理觸發(fā),例如UIEvent精算、UITouch事件瓢宦。包含了一個回調,不能主動觸發(fā)事件灰羽。使用時刁笙,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理谦趣,然后手動調用 CFRunLoopWakeUp(runloop)來喚醒 RunLoop疲吸,讓其處理這個事件。

  • -performSelector:onThread:withObject:waitUntilDone: inModes:創(chuàng)建的是source0任務前鹅。

(2) source1 基于port的:是基于 mach_ports 的摘悴,用于通過內核和其他線程互相發(fā)送消息。包含一個 mach_port 和一個回調舰绘,可監(jiān)聽系統(tǒng)端口和通過內核和其他線程發(fā)送的消息蹂喻,能主動喚醒runloop,接收分發(fā)系統(tǒng)事件捂寿。

  • iOS / OSX 都是基于 Mach 內核口四,Mach 的對象間的通信是通過消息在兩個端口(port)之間傳遞來完成。
  • 很多時候我們的 app 都是處于什么事都不干的狀態(tài)秦陋,在空閑前指定用于喚醒的 mach port 端口蔓彩,然后在空閑時被 mach_msg() 函數阻塞著并監(jiān)聽喚醒端口, mach_msg() 又會調用 mach_msg_trap() 函數從用戶態(tài)切換到內核態(tài)驳概,這樣系統(tǒng)內核就將這個線程掛起赤嚼,一直停留在 mac_msg_trap 狀態(tài)。直到另一個線程向內核發(fā)送這個端口的 msg 后顺又, trap 狀態(tài)被喚醒更卒, RunLoop 繼續(xù)開始干活。

Source1和Timer都屬于端口事件源,不同的是所有的Timer都共用一個端口(Timer Port),而每個Source1都有不同的對應端口叠赐。

Source0屬于Input Source中的一部分牛柒,Input Source還包括custom自定義源冗酿,由其他線程手動發(fā)出煌寇。

3.5 CFRunLoopObserverRef 觀察者

CFRunLoopObserverRef是觀察者易茬,用來監(jiān)聽RunLoop的狀態(tài)改變而叼,CFRunLoopObserverRef可以監(jiān)聽的狀態(tài)改變有以下幾種:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即將進入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即將處理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即將處理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即將進入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即將從休眠中喚醒:64
    kCFRunLoopExit = (1UL << 7),                // 即將從Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 監(jiān)聽全部狀態(tài)改變  
};

具體使用舉例如下:

/**
 * 用來展示CFRunLoopObserverRef使用
 */
- (void)showDemo2
{
    // 創(chuàng)建觀察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"監(jiān)聽到RunLoop發(fā)生改變---%zd",activity);
    });
    
    // 添加觀察者到當前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    // 釋放observer
    CFRelease(observer);
}

4 RunLoop事件隊列原理

RunLoop運行邏輯圖1
RunLoop運行邏輯圖2

在每次運行開啟RunLoop的時候姿骏,所在線程的RunLoop會自動處理之前未處理的事件糖声,并且通知相關的觀察者。

具體的順序如下:

  1. 通知觀察者RunLoop已經啟動
  2. 通知觀察者即將要開始的定時器
  3. 通知觀察者任何即將啟動的非基于端口的源
  4. 啟動任何準備好的非基于端口的源
  5. 如果基于端口的源準備好并處于等待狀態(tài)分瘦,立即啟動蘸泻;并進入步驟9
  6. 通知觀察者線程進入休眠狀態(tài)
  7. 將線程置于休眠直到任一下面的事件發(fā)生:
    • 某一事件到達基于端口的源
    • 定時器啟動
    • RunLoop設置的時間已經超時
    • RunLoop被顯式喚醒
  8. 通知觀察者線程將被喚醒
  9. 處理未處理的事件
    • 如果用戶定義的定時器啟動,處理定時器事件并重啟RunLoop嘲玫。進入步驟2
    • 如果輸入源啟動悦施,傳遞相應的消息
    • 如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop去团。進入步驟2

10.通知觀察者RunLoop結束抡诞。

5 什么時候使用RunLoop

僅當在為你的程序創(chuàng)建輔助線程的時候,你才需要顯式運行一個RunLoop土陪。RunLoop是程序主線程基礎設施的關鍵部分昼汗。所以程序提供了代碼運行主程序的循環(huán)并自動啟動RunLoop。iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動步驟的一部分鬼雀,它在程序正常啟動的時候就會啟動程序的主循環(huán)顷窒。

對于輔助線程,你需要判斷一個RunLoop是否是必須的源哩。如果是必須的鞋吉,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的RunLoop励烦。比如谓着,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動RunLoop坛掠。RunLoop在你要和線程有更多的交互時才需要赊锚,比如以下情況:

  • 使用端口或自定義輸入源來和其他線程通信
  • 使用線程的定時器
  • Cocoa中使用任何performSelector…的方法
  • 使線程周期性工作

如果你決定在程序中使用RunLoop,那么它的配置和啟動都很簡單却音。和所有線程編程一樣改抡,你需要計劃好在輔助線程退出線程的情形矢炼。讓線程自然退出往往比強制關閉它更好系瓢。

5.1 實戰(zhàn)--Timer使用

實際應用開發(fā)中,會發(fā)現滑動事件會導致Timer暫停不執(zhí)行句灌,可以采用RunLoop的kCFRunLoopCommonModes模式解決此問題以及增加子線程來解決夷陋。

- (void)timerRunLoop:(UIButton *)btn {
    
//第一種方法:將timer加入到NSRunLoopCommonModes
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            self.count --;
            self.countDown.text = [NSString stringWithFormat:@"%d",self.count];
        }];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [timer fire];

- (void)timerRunLoop:(UIButton *)btn {
        //第二種方法:在子線程創(chuàng)建timer并將runloop run起來
        NSThread *sub = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadRun) object:nil];
        [sub start];
}

- (void)subThreadRun {
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        self.count --;
        dispatch_async(dispatch_get_main_queue(), ^{
            self.countDown.text = [NSString stringWithFormat:@"%d",self.count];
        });
    }];

    //必須讓runloop 運行起來欠拾,否則timer僅執(zhí)行一次
    [runloop run];
}

timer確實被添加到NSDefaultRunLoopMode中了,可是添加到子線程中的NSDefaultRunLoopMode里骗绕,無論如何滾動藐窄,timer都能夠很正常的運轉。這又是為啥呢酬土?

這就是多線程與runloop的關系了荆忍,每一個線程都有一個與之關聯(lián)的RunLoop,而每一個RunLoop可能會有多個Mode撤缴。CPU會在多個線程間切換來執(zhí)行任務刹枉,呈現出多個線程同時執(zhí)行的效果。執(zhí)行的任務其實就是RunLoop去各個Mode里執(zhí)行各個item屈呕。因為RunLoop是獨立的兩個微宝,相互不會影響,所以在子線程添加timer虎眨,滑動視圖時蟋软,timer能正常運行。

5.2 實戰(zhàn)--ImageView延遲顯示

實際應用開發(fā)中嗽桩,會遇到滑動時加上圖片加載會導致頁面卡頓的情況岳守,因此需要在滑動時延遲加載圖片,具體代碼示例如下:

  • 如程序的效果圖碌冶,頁面上布局了一個textView
  • 點擊屏幕后棺耍,立即拖動textView,此時RunLoop進入UITrackingRunLoopMode种樱,只要滑動不結束即不退出UITrackingRunLoopMode蒙袍,則永遠不會執(zhí)行setImage方法
  • 滑動結束后加載圖片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self showDemo3]; // 用來展示UIImageView的延遲顯示
}

/**
 * 用來展示UIImageView的延遲顯示
 */
- (void)showDemo3
{
    NSLog(@"showDemo3 begin");
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}
圖片延遲展示

5.3 實戰(zhàn)--后臺常駐線程

  • 創(chuàng)建一個后臺常駐線程害幅,執(zhí)行run1開始
  • 并在屏幕點擊方法中對該線程進行添加其他任務run2
- (void)showDemo4
{
    // 創(chuàng)建線程以现,并調用run1方法執(zhí)行任務
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void) run1
{
    // 這里寫任務
    NSLog(@"----run1-----%@",[NSThread currentThread]);
    
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    // 測試是否開啟了RunLoop记盒,如果開啟RunLoop,則來不了這里,因為RunLoop開啟了循環(huán)。
    NSLog(@"-------------");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 利用performSelector調用常駐線程self.thread的run2方法
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO]; // 用來展示常駐內存的方式
}

- (void) run2
{
    NSLog(@"----run2------");
}

經過運行測試,除了之前打印的----run1-----,每當我們點擊屏幕,都能調用----run2------。

5.4 PerformSelecter...

當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop本辐,則這個方法會失效。

當調用 performSelector:onThread: 時地梨,實際上其會創(chuàng)建一個 Timer 加到對應的線程去,同樣的赖钞,如果對應線程沒有 RunLoop 該方法也會失效献起。

5.5 GCD

RunLoop 底層會用到 GCD 的東西岂嗓,GCD 的某些 API 也用到了 RunLoop炫欺。如當調用了 dispatch_async(dispatch_get_main_queue(), block)時,主隊列會把該 block 放到對應的線程(恰好是主線程)中摩桶,主線程的 RunLoop 會被喚醒桥状,從消息中取得這個 block辅斟,回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 來執(zhí)行這個 block:

GCD.png

6 參考資料

iOS多線程--徹底學會多線程之『RunLoop』
關于Runloop的原理探究及基本使用
iOS開發(fā)-Runloop詳解(簡書)
iOS開發(fā)·RunLoop源碼與用法完全解析(輸入源笔刹,定時源萌壳,觀察者,線程間通信山孔,端口間通信懂讯,NSPort,NSMessagePort台颠,NSMachPort褐望,NSPortMessage)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市串前,隨后出現的幾起案子瘫里,更是在濱河造成了極大的恐慌,老刑警劉巖荡碾,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谨读,死亡現場離奇詭異,居然都是意外死亡坛吁,警方通過查閱死者的電腦和手機劳殖,發(fā)現死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拨脉,“玉大人哆姻,你說我怎么就攤上這事∶蛋颍” “怎么了矛缨?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我箕昭,道長灵妨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任落竹,我火速辦了婚禮泌霍,結果婚禮上,老公的妹妹穿的比我還像新娘筋量。我一直安慰自己烹吵,他們只是感情好碉熄,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布桨武。 她就那樣靜靜地躺著,像睡著了一般锈津。 火紅的嫁衣襯著肌膚如雪呀酸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天琼梆,我揣著相機與錄音性誉,去河邊找鬼。 笑死茎杂,一個胖子當著我的面吹牛错览,可吹牛的內容都是我干的。 我是一名探鬼主播煌往,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼倾哺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了刽脖?” 一聲冷哼從身側響起羞海,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎曲管,沒想到半個月后却邓,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡院水,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年腊徙,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片檬某。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡撬腾,死狀恐怖,靈堂內的尸體忽然破棺而出橙喘,到底是詐尸還是另有隱情时鸵,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站饰潜,受9級特大地震影響初坠,放射性物質發(fā)生泄漏。R本人自食惡果不足惜彭雾,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一碟刺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧薯酝,春花似錦半沽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至做葵,卻和暖如春占哟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酿矢。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工榨乎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘫筐。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓蜜暑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親策肝。 傳聞我的和親對象是個殘疾皇子肛捍,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容

  • 什么是RunLoop?從字面上來看是運行循環(huán)的意思.內部就是一個do{}while循環(huán),在這個循環(huán)里內部不斷的處理...
    deve_雨軒閱讀 29,508評論 1 32
  • 什么是RunLoop?從字面上來看是運行循環(huán)的意思. 內部就是一個do{}while循環(huán),在這個循環(huán)里內部不斷的處...
    sunmumu1222閱讀 435評論 0 0
  • 什么是RunLoop 從字面意思看 運行循環(huán) 跑圈 基本作用 保持程序的持續(xù)運行 處理App中的各種事件(比如觸摸...
    沉夢昂志__閱讀 341評論 0 0
  • 這是一篇對Run Loop開發(fā)文檔《Threading Program Guide:Run Loops》的翻譯,來...
    鴻雁長飛光不度閱讀 3,633評論 3 29
  • Run loop 剖析:Runloop 接收的輸入事件來自兩種不同的源:輸入源(intput source)和定時...
    Mitchell閱讀 12,430評論 17 111