iOS Runloop底層詳解、內部原理、結構框架

一:前言

RunLoop的基本作用

RunLoop對象

RunLoop與線程

二:Core Foundation中關於RunLoop的5個類

三:RunLoop的模式及狀態

runloop的狀態

每個模式做的事情

runloop model

四:RunLoop的運行邏輯

五:休眠的細節

六:蘋果用 RunLoop 實現的功能

AutoreleasePool

事件響應

手勢識別

界面更新

定時器

PerformSelecter

關於GCD

關於網絡請求

七:RunLoop在實際開中的應用

控制線程生命週期

解決NSTimer在滑動時停止工作的問題(在滾動時,nstimer會失效)

八:面試題

 

一:前言

顧名思義:運行循環:在程序運行過程中循環做一些事情

應用範疇:

  • 定時器(Timer)、PerformSelector
  • GCD Async Main Queue
  • 事件響應、手勢識別、界面刷新
  • 網絡請求
  • AutoreleasePool

命令行項目默認是沒有runloop的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSLog(@"Hello Word");
    }
    return 0;
}

如果沒有runloop,執行完nslog代碼行代碼後,會即將退出程序 

如果有runloop:程序並不會馬上退出,而是保持運行狀態

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

下面是僞代碼

一般來講,一個線程一次只能執行一個任務,執行完成後線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但並不退出,通常的代碼邏輯是這樣的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統和框架裏都有實現,比如 Node.js 的事件處理,比如 Windows 程序

的消息循環,

再比如 OSX/iOS 裏的 RunLoop。實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資

源佔用、在有消息到來時立刻被喚醒。

所以,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 “接受消息->等待->處理” 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。

 

RunLoop的基本作用

保持程序的持續運行

處理App中的各種事件(比如觸摸事件、定時器事件等)

節省CPU資源,提高程序性能:該做事時做事,該休息時休息

......

 

RunLoop對象

OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。來訪問和使用Runloop。

Foundation:NSRunLoop

Core Foundation:CFRunLoopRef

CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。

NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。

CFRunLoopRef是開源的:https://opensource.apple.com/tarballs/CF/

(Update: Swift 開源後,蘋果又維護了一個跨平臺的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/,這

個版本的源碼可能和現有 iOS 系統中的實現略不一樣,但更容易編譯,而且已經適配了 Linux/Windows。)

 

RunLoop與線程

首先,iOS 開發中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明瞭 NSThread 只是 pthread_t 的封裝,但那份

文檔已經失效了,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果並沒有提供這兩個對象相互轉換的接口,但不管怎麼

樣,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread]

來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基於 pthread 來管理的。

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內

部的邏輯大概是下面這樣:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,並先爲主線程創建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裏獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,創建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

從上面的代碼可以看出,

線程和 RunLoop 之間是一一對應的,其關係是保存在一個全局的 Dictionary 裏,線程作爲key,RunLoop作爲value。

線程剛創建時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。

RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。

你只能在一個線程的內部獲取其 RunLoop(主線程除外)。

主線程的RunLoop已經自動獲取(創建),子線程默認沒有開啓RunLoop

 

二:Core Foundation中關於RunLoop的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef (平時的事情,點擊事件 、刷新ui、滾動等)
  • CFRunLoopTimerRef    (定時器)
  • CFRunLoopObserverRef  (監聽器)
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;


struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;            // 線程對象
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; // 當前是什麼模式
    CFMutableSetRef _modes;        // set是一個無序集合:一堆CFRunLoopModeRef對象
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;          // 模式的名字UITrackingRunLoopMode或者kCFRunLoopDefaultMode或者是系統不常用的模式
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;    // 裝CFRunLoopSourceRef對象
    CFMutableSetRef _sources1;    // 裝CFRunLoopSourceRef對象
    CFMutableArrayRef _observers; // 裝CFRunLoopObserverRef對象
    CFMutableArrayRef _timers;    // 裝CFRunLoopTimerRef對象
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

上述關係相當於是

(上面假設只有兩個模式,這個模式就是指__CFRunLoopMode中_name,也就是上面紫色的區域,其實還有一個塊塊是代表模式)runloop中有很多模式(常用的只有兩個),但是在運行中,只會選擇一種模式來運行,比如上圖中可能是左邊 也可能是右邊,運行在那種模式下取決於currentModel.

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是爲了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

 

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的運行模式

一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer

RunLoop啓動時只能選擇其中一個Mode,作爲currentMode。如果需要切換Mode,只能退出當前Loop(切換模式,不會導致程序退出,而是在循環內部做的切換,模式起隔離的作用,比方說滾動下,只執行滾動模式下的邏輯,那默認模式下的邏輯就不會處理,這樣滾動起來就比較流暢),再重新選擇一個Mode進入。不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響

如果Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出

看下面的小問題

 

三:RunLoop的模式及狀態

bt:打印函數調用棧

每個模式做的事情:

CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。

Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

  • Source0

觸摸事件處理

performSelector:onThread:

Source1 包含了一個 mach_port 和一個回調(函數指針),被用於通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

  • Source1

基於Port的線程間通信

系統事件捕捉

CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。

  • Timers

NSTimer

performSelector:withObject:afterDelay:

CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。

  • Observers

用於監聽RunLoop的狀態

UI刷新(BeforeWaiting)

Autorelease pool(BeforeWaiting)

(在線程睡覺之前 刷新UI(這些不是立刻刷新的), 設置顏色這些)

可以觀測的時間點有以下幾個:

也是runloop的狀態:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

 

上面的 Source/Timer/Observer 被統稱爲 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

 

RunLoop模式:常見的2種Mode(前兩種模式),其他的(後三種)都是不常見的 系統的

a:kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認Mode,通常主線程是在這個Mode下運行(應用普通狀態下的)

b:UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響(當一旦滾動屏幕,就會自動切換到這個模式)

c :UIInitializationRunLoopMode: ()在剛啓動 App 時第進入的第一個 Mode,啓動完成後就不再使用。

d:GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。

e:kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際作用。

你可以在這裏看到更多的蘋果內部的 Mode,但那些 Mode 在開發中就很難遇到了。

我們可以查看一下代碼

  NSLog(@"%@", [NSRunLoop mainRunLoop]);
<CFRunLoop 0x6040001f8700 [0x1069d4c80]>{wakeup port = 0x2703, stopped = false, ignoreWakeUps = false, 
current mode = kCFRunLoopDefaultMode,  ////// 這裏選擇的是默認模式
common modes = <CFBasicHash 0x604000042af0 [0x1069d4c80]>{type = mutable set, count = 2,
entries =>    ////// 這個common mode在上模式設置爲common的時候就有用了,可以看到這裏面就有兩種模式
	0 : <CFString 0x107d44e88 [0x1069d4c80]>{contents = "UITrackingRunLoopMode"}
	2 : <CFString 0x1069aa818 [0x1069d4c80]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = <CFBasicHash 0x608000244950 [0x1069d4c80]>{type = mutable set, count = 14,
entries =>
	0 : <CFRunLoopSource 0x60800017bd80 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10b87f75a)}}
	1 : <CFRunLoopSource 0x60800017cbc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 14099, subsystem = 0x107cfbfe8, context = 0x0}}
	6 : <CFRunLoopSource 0x60000017bfc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2f03, callout = PurpleEventCallback (0x10b881bf7)}}
	7 : <CFRunLoopObserver 0x60c000133ec0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x10c6b04ce), context = <CFRunLoopObserver context 0x0>}
	11 : <CFRunLoopSource 0x60c00017c080 [0x1069d4c80]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x60c0000a6e40, callout = FBSSerialQueueRunLoopSourceHandler (0x10afea82f)}}
	12 : <CFRunLoopSource 0x60800017ce00 [0x1069d4c80]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080002454f0, callout = __handleHIDEventFetcherDrain (0x1074dfbbe)}}
	13 : <CFRunLoopObserver 0x608000134500 [0x1069d4c80]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1071686b3), context = <CFRunLoopObserver context 0x6040000d1100>}
	14 : <CFRunLoopObserver 0x608000134320 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x106bb1da1), context = <CFRunLoopObserver context 0x7fe618e00b90>}
	15 : <CFRunLoopObserver 0x608000134280 [0x1069d4c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (
	0 : <0x7fe61a002048>
)}}
	16 : <CFRunLoopObserver 0x6080001341e0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x106bb1e1c), context = <CFRunLoopObserver context 0x7fe618e00b90>}
	17 : <CFRunLoopSource 0x60800017d1c0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400015c460, callout = __handleEventQueue (0x1074dfbb2)}}
	18 : <CFRunLoopObserver 0x600000135360 [0x1069d4c80]>{valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x1066ae960), context = <CFRunLoopObserver context 0x60000004e970>}
	19 : <CFRunLoopObserver 0x608000134000 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (
	0 : <0x7fe61a002048>
)}}
	20 : <CFRunLoopSource 0x60800017c680 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 23043, subsystem = 0x107d16668, context = 0x600000025b60}}
}
,
////// 這裏是modes:
modes = <CFBasicHash 0x604000042b80 [0x1069d4c80]>{type = mutable set, count = 4,
entries =>

////// 模式一:UITrackingRunLoopMode: 可以看到有source0、sources1、observers、timers
	2 : <CFRunLoopMode 0x608000196a60 [0x1069d4c80]>{name = UITrackingRunLoopMode, port set = 0x1f03, queue = 0x60800015c3b0, source = 0x608000196b30 (not fired), timer port = 0x1d03, 
	sources0 = <CFBasicHash 0x6080002448f0 [0x1069d4c80]>{type = mutable set, count = 4,
entries =>
	0 : <CFRunLoopSource 0x60800017bd80 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10b87f75a)}}
	1 : <CFRunLoopSource 0x60c00017c080 [0x1069d4c80]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x60c0000a6e40, callout = FBSSerialQueueRunLoopSourceHandler (0x10afea82f)}}
	2 : <CFRunLoopSource 0x60800017ce00 [0x1069d4c80]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080002454f0, callout = __handleHIDEventFetcherDrain (0x1074dfbbe)}}
	5 : <CFRunLoopSource 0x60800017d1c0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400015c460, callout = __handleEventQueue (0x1074dfbb2)}}
}
,
	sources1 = <CFBasicHash 0x6080002444d0 [0x1069d4c80]>{type = mutable set, count = 3,
entries =>
	0 : <CFRunLoopSource 0x60800017cbc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 14099, subsystem = 0x107cfbfe8, context = 0x0}}
	1 : <CFRunLoopSource 0x60800017c680 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 23043, subsystem = 0x107d16668, context = 0x600000025b60}}
	2 : <CFRunLoopSource 0x60000017bfc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2f03, callout = PurpleEventCallback (0x10b881bf7)}}
}
,
	observers = (
    "<CFRunLoopObserver 0x608000134280 [0x1069d4c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fe61a002048>\n)}}",
    "<CFRunLoopObserver 0x608000134500 [0x1069d4c80]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1071686b3), context = <CFRunLoopObserver context 0x6040000d1100>}",
    "<CFRunLoopObserver 0x600000135360 [0x1069d4c80]>{valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x1066ae960), context = <CFRunLoopObserver context 0x60000004e970>}",
    "<CFRunLoopObserver 0x608000134320 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x106bb1da1), context = <CFRunLoopObserver context 0x7fe618e00b90>}",
    "<CFRunLoopObserver 0x60c000133ec0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x10c6b04ce), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x6080001341e0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x106bb1e1c), context = <CFRunLoopObserver context 0x7fe618e00b90>}",
    "<CFRunLoopObserver 0x608000134000 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fe61a002048>\n)}}"
),
	timers = (null),
	currently 559646742 (30436416494659) / soft deadline in: 1.84467136e+10 sec (@ -1) / hard deadline in: 1.84467136e+10 sec (@ -1)
},

////// 第二個模式:GSEventReceiveRunLoopMode :sources0、sources1、observers(空)、timers(空)
	3 : <CFRunLoopMode 0x608000196c00 [0x1069d4c80]>{name = GSEventReceiveRunLoopMode, port set = 0x2c03, queue = 0x60800015c460, source = 0x608000196cd0 (not fired), timer port = 0x2e03, 
	sources0 = <CFBasicHash 0x6080002448c0 [0x1069d4c80]>{type = mutable set, count = 1,
entries =>
	0 : <CFRunLoopSource 0x60800017bd80 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10b87f75a)}}
}
,
	sources1 = <CFBasicHash 0x608000244890 [0x1069d4c80]>{type = mutable set, count = 1,
entries =>
	2 : <CFRunLoopSource 0x60000017c080 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2f03, callout = PurpleEventCallback (0x10b881bf7)}}
}
,
	observers = (null),
	timers = (null),
	currently 559646742 (30436417513777) / soft deadline in: 1.84467136e+10 sec (@ -1) / hard deadline in: 1.84467136e+10 sec (@ -1)
},

////// 這裏給的第三個模式:kCFRunLoopDefaultMode :sources0、sources1、observers、timers。
	4 : <CFRunLoopMode 0x6040001964b0 [0x1069d4c80]>{name = kCFRunLoopDefaultMode, port set = 0x1a03, queue = 0x60400015c250, source = 0x604000196580 (not fired), timer port = 0x2503, 
	sources0 = <CFBasicHash 0x6080002444a0 [0x1069d4c80]>{type = mutable set, count = 4,
entries =>
	0 : <CFRunLoopSource 0x60800017bd80 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10b87f75a)}}
	1 : <CFRunLoopSource 0x60c00017c080 [0x1069d4c80]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x60c0000a6e40, callout = FBSSerialQueueRunLoopSourceHandler (0x10afea82f)}}
	2 : <CFRunLoopSource 0x60800017ce00 [0x1069d4c80]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080002454f0, callout = __handleHIDEventFetcherDrain (0x1074dfbbe)}}
	5 : <CFRunLoopSource 0x60800017d1c0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400015c460, callout = __handleEventQueue (0x1074dfbb2)}}
}
,
	sources1 = <CFBasicHash 0x608000244470 [0x1069d4c80]>{type = mutable set, count = 3,
entries =>
	0 : <CFRunLoopSource 0x60800017cbc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 14099, subsystem = 0x107cfbfe8, context = 0x0}}
	1 : <CFRunLoopSource 0x60800017c680 [0x1069d4c80]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 23043, subsystem = 0x107d16668, context = 0x600000025b60}}
	2 : <CFRunLoopSource 0x60000017bfc0 [0x1069d4c80]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2f03, callout = PurpleEventCallback (0x10b881bf7)}}
}
,
	observers = (
    "<CFRunLoopObserver 0x608000134280 [0x1069d4c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fe61a002048>\n)}}",
    "<CFRunLoopObserver 0x608000134500 [0x1069d4c80]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1071686b3), context = <CFRunLoopObserver context 0x6040000d1100>}",
    "<CFRunLoopObserver 0x600000135360 [0x1069d4c80]>{valid = Yes, activities = 0x4, repeats = No, order = 0, callout = _runLoopObserverWithBlockContext (0x1066ae960), context = <CFRunLoopObserver context 0x60000004e970>}",
    "<CFRunLoopObserver 0x608000134320 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x106bb1da1), context = <CFRunLoopObserver context 0x7fe618e00b90>}",
    "<CFRunLoopObserver 0x60c000133ec0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x10c6b04ce), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x6080001341e0 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x106bb1e1c), context = <CFRunLoopObserver context 0x7fe618e00b90>}",
    "<CFRunLoopObserver 0x608000134000 [0x1069d4c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x106b82d92), context = <CFArray 0x608000245f10 [0x1069d4c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fe61a002048>\n)}}"
),
	timers = <CFArray 0x6040000a5760 [0x1069d4c80]>{type = mutable-small, count = 2, values = (
	0 : <CFRunLoopTimer 0x60000017f980 [0x1069d4c80]>{valid = Yes, firing = No, interval = 0.5, tolerance = 0, next fire date = 559646742 (0.49159193 @ 30436910466673), callout = (NSTimer) [UITextSelectionView caretBlinkTimerFired:] (0x10580b48a / 0x10554337c) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit), context = <CFRunLoopTimer context 0x60000002c540>}
	1 : <CFRunLoopTimer 0x60400017bf00 [0x1069d4c80]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 559646743 (1.25698304 @ 30437676026131), callout = (Delayed Perform) UIApplication _accessibilitySetUpQuickSpeak (0x1057f6849 / 0x10707331b) (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit), context = <CFRunLoopTimer context 0x604000066840>}
)},
	currently 559646742 (30436417545494) / soft deadline in: 0.492921161 sec (@ 30436910466673) / hard deadline in: 0.492921134 sec (@ 30436910466673)
},

////// 這裏給出的第四個模式:kCFRunLoopCommonModes:可以看出來下面都是空的

	5 : <CFRunLoopMode 0x600000196650 [0x1069d4c80]>{name = kCFRunLoopCommonModes, port set = 0x480b, queue = 0x60000015c460, source = 0x6000001963e0 (not fired), timer port = 0x410b, 
	sources0 = (null),
	sources1 = (null),
	observers = (null),
	timers = (null),
	currently 559646742 (30436419063687) / soft deadline in: 1.84467136e+10 sec (@ -1) / hard deadline in: 1.84467136e+10 sec (@ -1)
},

}
}

可以看到上面的模式有四種:UITrackingRunLoopMode、GSEventReceiveRunLoopMode、kCFRunLoopDefaultMode、kCFRunLoopCommonModes。

而前面設置的currentmode  = kCFRunLoopDefaultMode,所以,會執行kCFRunLoopDefaultMode

 kCFRunLoopCommonModes默認包括kCFRunLoopDefaultMode、UITrackingRunLoopMode

- (void)viewDidLoad {
    // 1
    // 創建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 釋放
    CFRelease(observer);
}


NSMutableDictionary *runloops;

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;
    }
}
2018-09-26 14:21:02.217837+0800 Interview03-RunLoop[4744:336769] kCFRunLoopAfterWaiting
2018-09-26 14:21:02.218071+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.218648+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.220133+0800 Interview03-RunLoop[4744:336769] --===-[ViewController touchesBegan:withEvent:]
2018-09-26 14:21:02.221260+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.221541+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.221793+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.222085+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.222190+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.222517+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.223391+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeWaiting
2018-09-26 14:21:02.318849+0800 Interview03-RunLoop[4744:336769] kCFRunLoopAfterWaiting
2018-09-26 14:21:02.318979+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.319341+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.319715+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeTimers
2018-09-26 14:21:02.319782+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeSources
2018-09-26 14:21:02.319871+0800 Interview03-RunLoop[4744:336769] kCFRunLoopBeforeWaiting

證明模式切換 第二種方式:用block的方式


- (void)viewDidLoad
{
    //     第二種方式
    //    // 創建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中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 釋放
    CFRelease(observer);
}

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

}
2018-09-26 14:23:00.886113+0800 Interview03-RunLoop[4789:343545] 定時器-----------
2018-09-26 14:23:06.659947+0800 Interview03-RunLoop[4789:343545] kCFRunLoopExit - kCFRunLoopDefaultMode
2018-09-26 14:23:06.660184+0800 Interview03-RunLoop[4789:343545] kCFRunLoopEntry - UITrackingRunLoopMode
2018-09-26 14:23:07.075695+0800 Interview03-RunLoop[4789:343545] kCFRunLoopExit - UITrackingRunLoopMode
2018-09-26 14:23:07.075877+0800 Interview03-RunLoop[4789:343545] kCFRunLoopEntry - kCFRunLoopDefaultMode

一開始 oberver內部是有的

 

RunLoop 的 Mode

CFRunLoopMode 和 CFRunLoop 的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

這裏有個概念叫 “CommonModes”:一個 Mode 可以將自己標記爲”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode裏。

應用場景舉例:主線程的 RunLoop 裏有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記爲”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回調,並且也不會影響到滑動操作。

有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裏去。

當運行在commonModel標記下,會發現,currentModel的值和狀態依然沒變,在普通狀態下還是default,在滑動下還是track,但是不一樣的點在於,正常兩個狀態下的(不是在標記下),track內部是模式沒有timer的,timer默認是加在default下面的,所以滑動下才會產生不準,停頓的現象,之所以加入標記會好使是因爲:標記下會看到track模式的model裏也有timer了,也就是上面所說的會把:”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裏去。但是隨着狀態的切換,currentmodel依然在換,只是兩個model具有相同的東西就是commonModeItems的內容。

CFRunLoop對外暴露的管理 Mode 接口只有下面2個:

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

Mode 暴露的管理 mode item 的接口有下面幾個:

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

你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop會自動幫你創建對應的 CFRunLoopModeRef。對於一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。

蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應的 Mode。

同時蘋果還提供了一個操作 Common 標記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字符串來操作 Common Items,或標記一個 Mode 爲 “Common”。使用時注意區分這個字符串和其他 mode name。

 

四:RunLoop的運行邏輯

請看代碼執行流程 及源碼流程

打斷點看這裏 下面的函數棧執行流程就是下面源碼的流程過程。請對比

2018-09-26 15:21:45.901445+0800 Interview01-runloop流程[5242:400150] 11111111111
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001003ed69d Interview01-runloop流程`-[ViewController touchesBegan:withEvent:](self=0x00007fdc3ff02e90, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000608000104a40) at ViewController.m:39
    frame #1: 0x0000000101d03767 UIKit`forwardTouchMethod + 340
    frame #2: 0x0000000101d03602 UIKit`-[UIResponder touchesBegan:withEvent:] + 49
    frame #3: 0x0000000101b4be1a UIKit`-[UIWindow _sendTouchesForEvent:] + 2052
    frame #4: 0x0000000101b4d7c1 UIKit`-[UIWindow sendEvent:] + 4086
    frame #5: 0x0000000101af1310 UIKit`-[UIApplication sendEvent:] + 352
    frame #6: 0x00000001024326af UIKit`__dispatchPreprocessedEventFromEventQueue + 2796
    frame #7: 0x00000001024352c4 UIKit`__handleEventQueueInternal + 5949
    frame #8: 0x00000001015fbbb1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #9: 0x00000001015e04af CoreFoundation`__CFRunLoopDoSources0 + 271
    frame #10: 0x00000001015dfa6f CoreFoundation`__CFRunLoopRun + 1263
    frame #11: 0x00000001015df30b CoreFoundation`CFRunLoopRunSpecific + 635
    frame #12: 0x00000001067cda73 GraphicsServices`GSEventRunModal + 62
    frame #13: 0x0000000101ad6057 UIKit`UIApplicationMain + 159
    frame #14: 0x00000001003ed74f Interview01-runloop流程`main(argc=1, argv=0x00007ffeef811fb0) at main.m:14
    frame #15: 0x00000001050b6955 libdyld.dylib`start + 1
(lldb) 
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    // 來保存do while循環的結果
    int32_t retVal = 0;
    // 從這裏開始
    do {
        // 通知obervers:即將處理timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知obervers:即將處理sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 處理blocks
        __CFRunLoopDoBlocks(rl, rlm);

        // 處理source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            // 處理blocks
            __CFRunLoopDoBlocks(rl, rlm);
	}

        // 判斷有無source1:端口相關的東西
        msg = (mach_msg_header_t *)msg_buffer;
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有source1 就跳轉到handle_msg
            goto handle_msg;
        }

        didDispatchPortLastTime = false;

        // 通知obervers即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 休眠
        __CFRunLoopSetSleeping(rl);


        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

        do { // 循環在做一件事情

            // 做這件事情 :在等待別的消息 來喚醒當前線程。
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof((mach_msg_header_t *)msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        } while (1);

        // 不睡覺了
        __CFRunLoopUnsetSleeping(rl);
        // 通知obervers:結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        // 去判斷怎麼醒來的 下面是很多種情況
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);

        if (MACH_PORT_NULL == livePort) {  CFRUNLOOP_WAKEUP_FOR_NOTHING();
        } else if (livePort == rl->_wakeUpPort) {  CFRUNLOOP_WAKEUP_FOR_WAKEUP();
        } // 前面兩種是什麼事都不幹 下面這兩種就是:被timer喚醒 就會執行__CFRunLoopDoTimers

        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }

        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();

            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }

        else if (livePort == dispatchPort) { // 被GCD喚醒
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);

            // 處理GCD相關的事情
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {  // 剩下的是被source1喚醒
            // 處理source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        } 

    // 處理blocks
    __CFRunLoopDoBlocks(rl, rlm);

    // 設置返回值
	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);

    } while (0 == retVal);// 條件成立,又會執行裏面的東西

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

// runloop入口
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    // kCFRunLoopEntry通知oberver進入loop 已經傳入一種模式:modeName
	 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 最主要的邏輯,具體要做的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 通知obervers 退出loop
	 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    // 返回結果
    return result;
}
  • 01、通知Observers:進入Loop
  • 02、通知Observers:即將處理Timers
  • 03、通知Observers:即將處理Sources
  • 04、處理Blocks
  • 05、處理Source0(可能會再次處理Blocks)
  • 06、如果存在Source1,就跳轉到第8步
  • 07、通知Observers:開始休眠(等待消息喚醒)
  • 08、通知Observers:結束休眠(被某個消息喚醒)
    • 01> 處理Timer
    • 02> 處理GCD Async To Main Queue
    • 03> 處理Source1
  • 09、處理Blocks
  • 10、根據前面的執行結果,決定如何操作
    • 01> 回到第02步
    • 02> 退出Loop
  • 11、通知Observers:退出Loop

GCD很多東西是不依賴於runloop的,只是GCD有一種情況會交給runloop去處理:

  dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        // 處理一些子線程的邏輯
        
        // 回到主線程去刷新UI界面 :依賴runloop
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"11111111111");
        });
    });

 

五:休眠的細節

RunLoop 的底層實現

從上面代碼可以看到,RunLoop 的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。爲了解釋這個邏輯,下面稍微介紹一下 OSX/iOS 的系統架構。
RunLoop_3

蘋果官方將整個系統大致劃分爲上述4個層次:
應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard 等。
應用框架層即開發人員接觸到的 Cocoa 等框架。
核心框架層包括各種核心框架、OpenGL 等內容。
Darwin 即操作系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其所有源碼都可以在 opensource.apple.com 裏找到。

我們在深入看一下 Darwin 這個核心的架構:
RunLoop_4

其中,在硬件層上面的三個組成部分:Mach、BSD、IOKit (還包括一些上面沒標註的內容),共同組成了 XNU 內核。
XNU 內核的內環被稱作 Mach,其作爲一個微內核,僅提供了諸如處理器調度、IPC (進程間通信)等非常少量的基礎服務。
BSD 層可以看作圍繞 Mach 層的一個外環,其提供了諸如進程管理、文件系統和網絡等功能。
IOKit 層是爲設備驅動提供了一個面向對象(C++)的一個框架。

Mach 本身提供的 API 非常有限,而且蘋果也不鼓勵使用 Mach 的 API,但是這些API非常基礎,如果沒有這些API的話,其他任何工作都無法實施。在 Mach 中,所有的東西都是通過自己的對象實現的,進程、線程和虛擬內存都被稱爲”對象”。和其他架構不同, Mach 的對象間不能直接調用,只能通過消息傳遞的方式實現對象間的通信。”消息”是 Mach 中最基礎的概念,消息在兩個端口 (port) 之間傳遞,這就是 Mach 的 IPC (進程間通信) 的核心。

Mach 的消息定義是在 <mach/message.h> 頭文件的,很簡單:

typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;
 
typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;

一條 Mach 消息實際上就是一個二進制數據包 (BLOB),其頭部定義了當前端口 local_port 和目標端口 remote_port,
發送和接受消息是通過同一個 API 進行的,其 option 標記了消息傳遞的方向:

mach_msg_return_t mach_msg(
			mach_msg_header_t *msg,
			mach_msg_option_t option,
			mach_msg_size_t send_size,
			mach_msg_size_t rcv_size,
			mach_port_name_t rcv_name,
			mach_msg_timeout_t timeout,
			mach_port_name_t notify);

爲了實現消息的發送和接收,mach_msg() 函數實際上是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach 中等同於系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg() 函數會完成實際的工作,如下圖:
RunLoop_5

這些概念可以參考維基百科: System_callTrap_(computing)

開始睡覺 就是休眠 意味着:線程阻塞,不會往下走。cpu就不會給給資源。

實現方式 while(1)也可以實現阻塞,但是當前線程沒有休息,一直在執行代碼,這種不叫休眠。而runloop的那種是什麼事情都不會做,真正的休息了。那如何做到呢

__CFRunLoopServiceMachPort

要達到睡覺,只有內核(操作系統)層面的才能達到。

// 內核層面API(一般不開放)  應用層面API(我們程序員用的)

休眠的 實現原理

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {		/* In that sleep of death what nightmares may come ... */

...
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        // mach_msg 比較底層的函數
        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);

        // Take care of all voucher-related work right after mach_msg.
        // If we don't release the previous voucher we're going to leak it.
        voucher_mach_msg_revert(*voucherState);
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        *voucherState = voucher_mach_msg_adopt(msg);
...

    }
    HALT;
    return false;
}

 

RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裏跑起一個 iOS 的 App,然後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。

關於具體的如何利用 mach port 發送信息,可以看看 NSHipster 這一篇文章,或者這裏的中文翻譯 。

關於Mach的歷史可以看看這篇很有趣的文章:Mac OS X 背後的故事(三)Mach 之父 Avie Tevanian

 

六:蘋果用 RunLoop 實現的功能

AutoreleasePool

App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之後。

在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞着,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。

事件響應

蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細情況可以參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給需要的App進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。

蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer的回調。

當有 UIGestureRecognizer 的變化(創建/銷燬/狀態改變)時,這個回調都會進行相應處理。

界面更新

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。

蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。

這個函數內部的調用棧大概是這樣的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

定時器

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到後,容許有多少最大誤差。

如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是爲了解決界面卡頓的問題,其內部也用到了 RunLoop,這個稍後我會再單獨寫一頁博客來分析。

PerformSelecter

當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會創建一個 Timer 並添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。

當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。


+ (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)((id)self, sel);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)((id)self, sel, obj1, obj2);
}

- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

- (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2);
}

而  performSelector: withObject:  afterDelay: 這個需要注意 帶有afterDelay的都是跟runloop有關的,這個底層用到了NSTimer。這裏分不清的需要看一下timer的作用。

主線程幾乎所有的事情都是交給了runloop去做,比如UI界面的刷新、點擊時間的處理、performSelector等等

不是所有的代碼都是交給runloop去做的,比如:打印不是runloop去做的。

關於GCD

實際上 RunLoop 底層也會用到 GCD 的東西,比如 RunLoop 是用 dispatch_source_t 實現的 Timer(評論中有人提醒,NSTimer 是用了 XNU 內核的 mk_timer,我也仔細調試了一下,發現 NSTimer 確實是由 mk_timer 驅動,而非 GCD 驅動的)。但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。

關於網絡請求

iOS 中,關於網絡請求的接口自下至上有如下幾層:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

• CFSocket 是最底層的接口,只負責 socket 通信。
• CFNetwork 是基於 CFSocket 等接口的上層封裝,ASIHttpRequest 工作於這一層。
• NSURLConnection 是基於 CFNetwork 的更高層的封裝,提供面向對象的接口,AFNetworking 工作於這一層。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 並列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作於這一層。

下面主要介紹下 NSURLConnection 的工作過程。

通常使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 後,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,然後在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source)。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。

當開始網絡傳輸時,我們可以看到 NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,並通過之前添加的 Source0 通知到上層的 Delegate。

RunLoop_network

NSURLConnectionLoader 中的 RunLoop 通過一些基於 mach port 的 Source 接收來自底層 CFSocket 的通知。當收到通知後,其會在合適的時機向 CFMultiplexerSource 等 Source0 發送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執行實際的回調。


七:RunLoop在實際開中的應用

a、控制線程生命週期(線程保活,耗時操作,子線程的事情做完了就會自動銷燬,不希望早掛掉,就可以用runloop控制生命週期)

b、解決NSTimer在滑動時停止工作的問題(在滾動時,nstimer會失效)

c、監控應用卡頓

d、性能優化

 

b、首先講定時器失效的問題

因爲定時器是在默認模式下工作,不是在UITrackingRunLoopMode模式下工作

可以自己寫一個定時器 然後滾動頁面 可以看到定時器停止了。

 [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];

帶有scheduledTimer的方法,就會默認直接將nstimer對象添加到默認模式,如果自己想添加的話可以用下面的這種

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // NSDefaultRunLoopMode、UITrackingRunLoopMode纔是真正存在的模式
    // NSRunLoopCommonModes並不是一個真的模式,它只是一個標記
    // timer能在_commonModes數組中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

如果填寫的是NSRunLoopCommonModes,就是告訴runloop會運行到下面結構體的_commonModes這個數組中的模式(上面一個源碼中,我們也看到了,可以向上翻),而這個數組恰好裝着NSDefaultRunLoopMode和UITrackingRunLoopMode兩個,也就是標記爲通用的模式,所以就是可以運行到兩種模式下。平時runloop會切換到你傳入兩種模式的任何一個模式,但是你傳入common,也就是會到commonModes數組下的模式,數組中有兩個模式,所以兩個模式下都可以運行。而common並不是一種模式。

timer被標記爲NSRunLoopCommonModes,那麼能在commonModes模式工作,那這個timer就會被放到commonModeItems裏面去(因爲平常的模式都有對應的modes)。

從流程上說,timer可以喚醒runloop,這樣就開始處理timer了。

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;            // 線程對象
    uint32_t _winthread;  
    CFMutableSetRef _commonModes;  // 這是有common標記的
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; // 當前是什麼模式
    CFMutableSetRef _modes;        // set是一個無序集合:一堆CFRunLoopModeRef對象
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

 

a、控制線程生命週期(線程保活,AFNetWorking2.X)

任務可以接受串行,不是併發的,可以採用這種方式來做,因爲任務都是交給一個線程來做的(併發沒有意義)。

控制子線程聲明週期,讓子線程一直在內存中,這樣的好處是 經常要在子線程中處理事情。

子線程的特點:做完自己的事情,直接掛掉,我們現在不希望它立刻掛掉。

在當前線程獲取runloop就自動創建好runloop了。[NSRunLoop currentRunLoop]

還需要注意:如果Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出

port就是source1。port對象可以隨便寫。所以可以下面方式做, 在run中就不會死掉,真正想讓做的事情是在某個時刻做test方法。

#import <Foundation/Foundation.h>

@interface MJThread : NSThread

@end
#import "MJThread.h"

@implementation MJThread

- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子線程需要執行的任務
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

// 這個方法的目的:線程保活
- (void)run {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 往RunLoop裏面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__); // 所以一直不會執行這一行
}

@end
2018-09-27 10:34:56.829966+0800 Interview03-線程保活[1888:120690] -[ViewController run] <MJThread: 0x6040002793c0>{number = 3, name = (null)}
2018-09-27 10:35:16.747305+0800 Interview03-線程保活[1888:120690] -[ViewController test] <MJThread: 0x6040002793c0>{number = 3, name = (null)}
2018-09-27 10:35:17.561778+0800 Interview03-線程保活[1888:120690] -[ViewController test] <MJThread: 0x6040002793c0>{number = 3, name = (null)}
2018-09-27 10:35:19.930440+0800 Interview03-線程保活[1888:120690] -[ViewController test] <MJThread: 0x6040002793c0>{number = 3, name = (null)}

上面有一個問題就是:target強引用這控制器,控制器不會銷燬,線程也不會銷燬,因爲rubloop的任務一直沒結束。

實現


#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread; // 假設希望這個線程跟隨控制器
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop裏面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStoped) {
            // beforeData:超時     distantFuture:遙遠的未來
            // 一旦執行完任務,runloop就退出了。
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
        
        // NSRunLoop的run方法是無法停止的,它專門用於開啓一個永不銷燬的線程(NSRunLoop)
//                [[NSRunLoop currentRunLoop] run];
        /*
         it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
         In other words, this method effectively begins an infinite(無限的) loop that processes data from the run loop’s input sources and timers
         */
        
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"如果上面是NO,則不會等上面的子線程執行完,就先執行這個打印");
}

// 子線程需要執行的任務
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    // 在子線程調用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 用於停止子線程的RunLoop
- (void)stopThread
{
    // 設置標記爲YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
//    [self stop];
}

@end
2018-09-27 11:19:28.051542+0800 Interview03-線程保活[2280:165391] <MJThread: 0x600000074780>{number = 5, name = (null)}----begin----
2018-09-27 11:19:30.032698+0800 Interview03-線程保活[2280:165391] -[ViewController test] <MJThread: 0x600000074780>{number = 5, name = (null)}
2018-09-27 11:19:30.584124+0800 Interview03-線程保活[2280:165391] -[ViewController test] <MJThread: 0x600000074780>{number = 5, name = (null)}
2018-09-27 11:19:31.045202+0800 Interview03-線程保活[2280:165391] -[ViewController test] <MJThread: 0x600000074780>{number = 5, name = (null)}
2018-09-27 11:19:31.922878+0800 Interview03-線程保活[2280:165391] -[ViewController test] <MJThread: 0x600000074780>{number = 5, name = (null)}
2018-09-27 11:19:32.901617+0800 Interview03-線程保活[2280:165391] -[ViewController stopThread] <MJThread: 0x600000074780>{number = 5, name = (null)}
2018-09-27 11:19:32.901958+0800 Interview03-線程保活[2280:165391] <MJThread: 0x600000074780>{number = 5, name = (null)}----end----

如果想讓控制器銷燬 runloop也銷燬,則在dealloc裏打開stop方法: 但是打開後就崩潰了,原因:waitUntilDone:NO 這個參數

這個代表不等了,不等這個子線程執行方法,而主線程依然往下走,是兩條路。而當主線程直接走下去,而這個stop函數就結束了,所以dealloc就結束了,控制器就掛掉了,而與此同時,這個子線程在執行stropThread方法,這個方法是控制器的方法,還要執行控制器的屬性啥的,引用是runloop在處理這個子線程的訪問。

performSelector:onThread:底層就是通過子線程的runloop去執行的。[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];這個perform是執行stopThread方法到self.thread這個線程,那就是給這個線程發一個消息,就相當於是基於port的概念,要基於runloop去處理的,所以也就是說這個runloop在處理這個perform的時候出問題了,因爲runloop內部在處理performselector的時候需要拿到控制器對象來調用stopThread做事情,但是控制器已經銷燬了,所以就發生了壞內存訪問。所以這個壞內存指的是控制器壞掉了。所以就需要設置爲YES,代表讓子線程代碼執行完畢後纔會往下走。但是僅僅設置爲YES,又會發生另外一個問題,也就是weakSelf提前釋放的問題。當控制器退出的時候,weakself指向的對象銷燬了,所以weakself就會被釋放掉,這個時候

  while ( !weakSelf.isStoped) {

            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

        }

這個就是空的也就是NO,取反就是YES,所以就又會進入while循環重新runloop,所以會看到一直打印不了end,所以線程也釋放不了。

看解決方案,判斷一下weakself,判斷必須有值,


#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop裏面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while ( weakSelf  && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子線程需要執行的任務
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    
    // 在子線程調用stop(waitUntilDone設置爲YES,代表子線程的代碼執行完畢後,這個方法纔會往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用於停止子線程的RunLoop
- (void)stopThread
{
    // 設置標記爲YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

@end

但是又發現一個問題,就是點擊頁面,停止,會崩潰看下圖

發生上述的原因是因爲:進來 點擊 然後停止之後,就已經把runloop停止掉了,而且線程已經是空的了,當退出控制器的時候,會再次調用stop,再調用這個方法,因爲線程已經是空的了,所以就會爆壞內存訪問。所以修改如下:把線程置位空,並且判斷一下

//
//  ViewController.m
//  Interview03-線程保活
//
//  Created by MJ Lee on 2018/6/3.
//  Copyright © 2018年 MJ Lee. All rights reserved.
//

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop裏面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (!self.thread) return;
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子線程需要執行的任務
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    if (!self.thread) return;
    
    // 在子線程調用stop(waitUntilDone設置爲YES,代表子線程的代碼執行完畢後,這個方法纔會往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用於停止子線程的RunLoop
- (void)stopThread
{
    // 設置標記爲YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空線程
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

@end

用起來比較麻煩,現在封裝起來

#import <Foundation/Foundation.h>

typedef void (^MJPermenantThreadTask)(void);

@interface MJPermenantThread : NSObject

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

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

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

@end
#import "MJPermenantThread.h"

/** MJThread **/
@interface MJThread : NSThread
@end
@implementation MJThread
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

/** MJPermenantThread **/
@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.stopped = NO;
        
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        
        [self.innerThread start];
    }
    return self;
}

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

- (void)executeTask:(MJPermenantThreadTask)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
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

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

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

@end

調用

#import "ViewController.h"
#import "MJPermenantThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJPermenantThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJPermenantThread alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.thread executeTask:^{
        NSLog(@"執行任務 - %@", [NSThread currentThread]);
    }];
}

- (IBAction)stop {
    [self.thread stop];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end

當然也可以用c語言的來寫

- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            NSLog(@"begin----");
            
            // 創建上下文(要初始化一下結構體)
            CFRunLoopSourceContext context = {0};
            
            // 創建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,代表執行完source後就會退出當前loop
//                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
//            }
            
            NSLog(@"end----");
        }];
        
        [self.innerThread start];
    }
    return self;
}

看一下

AFNetworking

AFURLConnectionOperation 這個類是基於 NSURLConnection 構建的,其希望能在後臺線程接收 Delegate 回調。爲此 AFNetworking 單獨創建了一個線程,並在這個線程中啓動了一個 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 啓動前內部必須要有至少一個 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先創建了一個新的 NSMachPort 添加進去了。通常情況下,調用者需要持有這個 NSMachPort (mach_port) 並在外部線程通過這個 port 發送消息到 loop 內;但此處添加 port 只是爲了讓 RunLoop 不至於退出,並沒有用於實際的發送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

當需要這個後臺線程執行任務時,AFNetworking 通過調用 [NSObject performSelector:onThread:..] 將這個任務扔到了後臺線程的 RunLoop 中

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用於保持界面流暢性的框架,其原理大致如下:

UI 線程中一旦出現繁重的任務就會導致界面卡頓,這類任務通常分爲3類:排版,繪製,UI對象操作。

排版通常包括計算視圖大小、計算文本高度、重新計算子式圖的排版等操作。
繪製一般有文本繪製 (例如 CoreText)、圖片繪製 (例如預先解壓)、元素繪製 (Quartz)等操作。
UI對象操作通常包括 UIView/CALayer 等 UI 對象的創建、設置屬性和銷燬。

其中前兩類操作可以通過各種方法扔到後臺線程執行,而最後一類操作只能在主線程完成,並且有時後面的操作需要依賴前面操作的結果 (例如TextView創建時可能需要提前計算出文本的大小)。ASDK 所做的,就是儘量將能放入後臺的任務放入後臺,不能的則儘量推遲 (例如視圖的創建、屬性的調整)。

爲此,ASDK 創建了一個名爲 ASDisplayNode 的對象,並在內部封裝了 UIView/CALayer,它具有和 UIView/CALayer 相似的屬性,例如 frame、backgroundColor等。所有這些屬性都可以在後臺線程更改,開發者可以只通過 Node 來操作其內部的 UIView/CALayer,這樣就可以將排版和繪製放入了後臺線程。但是無論怎麼操作,這些屬性總需要在某個時刻同步到主線程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,實現了一套類似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer,監聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調時,遍歷所有之前放入隊列的待處理的任務,然後一一執行。
具體的代碼可以看這裏:_ASAsyncTransactionGroup

 

八:面試題

runloop如何響應用戶操作的,具體流程是什麼樣的

首先是由source1來把系統事件捕捉,也就是你一點擊屏幕,這個就是一個source1事件,source1會把它包裝成一個事件隊列EventQueue, 這個事件隊列又在source0裏面處理,是先由source1捕捉,再由source0去處理。

 

線程和runloop的關係?

一條線程對應一個runloop,默認情況下,線程的runloop是沒有創建的,在第一次獲取的時候創建runloop。

 

timer跟runloop的關係?

timer是運行在runloop裏面的,runloop來控制timer什麼時候執行。

 

runloop是如何響應用戶操作的,具體流程

首先是由source1來捕捉觸摸事件,再由source0去處理觸摸事件。

 

線程的任務一旦執行完畢,生命週期就結束,無法再使用當前線程。

 

保住線程的命爲什麼要用runloop,用強指針不就好了麼?

 準確來講,使用runloop是爲了讓線程保持激活狀態

強指針指向,當任務執行完畢,這個線程還在內存中,它沒有銷燬,但是它不會處於激活狀態,不能再做事情了。

 

部分轉載參考自https://blog.ibireme.com/2015/05/18/runloop/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章