RunLoop這個名詞對於iOS開發來講應該是一個聽膩了的詞彙,並且只知其一不知其二,本篇章就來再深刻複習一下RunLoopphp
通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出,這種模型一般被稱做 Event Loop。 Event Loop 在不少系統和框架裏都有實現,好比 Node.js 的事件處理,好比 Windows 程序的消息循環,再好比 OSX/iOS 裏的 RunLoop
。html
實現這種模型的關鍵點在於:管理事件/消息,讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。數組
咱們先經過API內一張圖片來簡單看一下RunLoop內部運行原理bash
瞭解了RunLoop的做用,那麼在蘋果系統中,爲何使用RunLoop呢?主要有一下幾點服務器
咱們仍是查看源碼來進行探究網絡
NSRunloop
:最上層的NSRunloop層其實是對C語言實現的CFRunloop的一個封裝,實際上它沒幹什麼事,好比CFRunloop有一個過時時間是double類型,NSRunloop把它變味了NSDate類型;CFRunloop
:這是真正幹事的一層,源代碼是開源的,而且是跨平臺的;系統層
:底層實現用到了GCD,mach kernel是蘋果的內核,好比runloop的睡眠和喚醒就是用mach kernel來實現的。在OC代碼中,Runloop是由系統默認開啓的,就再main函數中,會開啓主線程和RunLoop。若是沒有Runloop,那麼main函數執行完畢後,程序就退出了,這說明在UIApplicationMain函數中,開啓了一個和主線程相關的RunLoop,致使UIApplicationMain不會返回,一直在運行中,也就保證了程序的持續運行。數據結構
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
接着咱們查看源碼,發現CFRunLoopRun
的底層實現結構也很是簡單,就是一個do...while
循環,咱們能夠把RunLoop當作一個死循環。若是沒有RunLoop,UIApplicationMain函數執行完畢以後將直接返回,也就沒有程序持續運行一說了。app
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製代碼
首先,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()
。 這兩個函數內部的邏輯大概是下面這樣:iphone
✅// 得到當前線程的RunLoop對象,內部調用_CFRunLoopGet0函數
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
✅// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
✅// 若是爲空則t設置爲主線程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
✅// 若是不存在runloop,則建立
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
✅// 根據傳入的主線程獲取主線程對應的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
✅// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
✅// 從字典裏面拿,將線程做爲key從字典裏獲取一個loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
✅// 若是loop爲空,則建立一個新的loop,因此runloop會在第一次獲取的時候建立
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
✅// 建立好以後,以線程爲key runloop爲value,一對一存儲在字典中,下次獲取的時候,則直接返回字典內的runloop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
✅//線程結束是銷燬loop
CFRelease(newLoop);
}
✅// 若是傳入線程和當前線程相同
if (pthread_equal(t, pthread_self())) {
✅// 註冊一個回調,當線程銷燬時,順便也銷燬對應的RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
複製代碼
經過源碼分析能夠看出,線程和RunLoop
之間是一一對應的,其關係是保存在一個Dictionary
字典裏。因此咱們建立子線程RunLoop
時,只需在子線程中獲取當前線程的RunLoop
對象便可[NSRunLoop currentRunLoop]
;。若是不獲取,那子線程就不會建立與之相關聯的RunLoop,而且只能在一個線程的內部獲取其RunLoop。
當經過調用[NSRunLoop currentRunLoop]
;方法獲取RunLoop時,會先看一下字典裏有沒有子線程對應的RunLoop,若是有則直接返回RunLoop,若是沒有則會建立一個,並將與之對應的子線程存入字典中。當線程結束時,RunLoop會被銷燬。
總結一下Runloop與線程的關係
CFRunLoopRef
經過源碼咱們找到__CFRunLoop
結構體
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop
{
// CoreFoundation 中的 runtime 基礎信息
CFRuntimeBase _base;
// 針對獲取 mode 列表操做的鎖
pthread_mutex_t _lock; /* locked for accessing mode list */
// 喚醒端口
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
// 是否使用過
Boolean _unused;
// runloop 運行會重置的一個數據結構
volatile _per_run_data *_perRunData; // reset for runs of the run loop
// runloop 所對應線程
pthread_t _pthread;
uint32_t _winthread;
// 存放 common mode 的集合
CFMutableSetRef _commonModes;
// 存放 common mode item 的集合
CFMutableSetRef _commonModeItems;
// runloop 當前所在 mode
CFRunLoopModeRef _currentMode;
// 存放 mode 的集合
CFMutableSetRef _modes;
// runloop 內部 block 鏈表表頭指針
struct _block_item *_blocks_head;
// runloop 內部 block 鏈表表尾指針
struct _block_item *_blocks_tail;
// 運行時間點
CFAbsoluteTime _runTime;
// 休眠時間點
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
// 每次 RunLoop 運行後會重置
typedef struct _per_run_data
{
uint32_t a;
uint32_t b;
uint32_t stopped; // runloop 是否中止
uint32_t ignoreWakeUps; // runloop 是否已喚醒
} _per_run_data;
// 鏈表節點
struct _block_item
{
// 指向下一個 _block_item
struct _block_item *_next;
// 要麼是 string 類型,要麼是集合類型,也就是說一個 block 可能對應單個或多個 mode
CFTypeRef _mode; // CFString or CFSet
// 存放的真正要執行的 block
void (^_block)(void);
};
};
複製代碼
經過查看RunLoop的底層結構,咱們發現了RunLoop也是一個結構體對象,其中有幾個主要的變量:
CFRunLoopModeRef _currentMode
:runloop 當前所在 modeCFMutableSetRef _modes
:存放 mode 的集合經過上述變量,咱們能夠知道:
CFRunLoopModeRef
CFRunLoopModeRef 實際上是指向__CFRunLoopMode
結構體的指針,__CFRunLoopMode
結構體源碼以下
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode
{
// CoreFoundation 中的 runtime 基礎信息
CFRuntimeBase _base;
// 互斥鎖,加鎖前須要 runloop 先加鎖
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
// mode 的名稱
CFStringRef _name;
// mode 是否中止
Boolean _stopped;
char _padding[3];
// source0
CFMutableSetRef _sources0;
// source1
CFMutableSetRef _sources1;
// observers
CFMutableArrayRef _observers;
// timers
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
// port 的集合
__CFPortSet _portSet;
// observer 的 mask
CFIndex _observerMask;
// 若是定義了 GCD 定時器
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// GCD 定時器
dispatch_source_t _timerSource;
// 隊列
dispatch_queue_t _queue;
// 當 GCD 定時器觸發時設置爲 true
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
// 若是使用 MK_TIMER
#if USE_MK_TIMER_TOO
// MK_TIMER 的 port
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 */
};
複製代碼
咱們發現,一個CFRunLoopModeRef
也包含不少變量,主要有_sources0
,_sources0
兩個集合和_observers
,_timers
兩個數組。
這說明一個mode能夠包含多種items模式
CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源(輸入源)。經過源碼能夠發現,其分爲source0
和source1
兩個。
source0
:處理App內部事件,App本身負責管理(觸發),如UIEvent
,CFSocket
等;source1
:由Runloop和內核管理,mach port
驅動,如CFMachPort(輕量級的進程間通訊的方式,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是經過它來作的),CFMessagePort
;typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource
{
// CoreFoundation 中的 runtime 基礎信息
CFRuntimeBase _base;
uint32_t _bits;
// 互斥鎖
pthread_mutex_t _lock;
// source 的優先級,值爲小,優先級越高
CFIndex _order; /* immutable */
// runloop 集合
CFMutableBagRef _runLoops;
// 一個聯合體,說明 source 要麼爲 source0,要麼爲 source1
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
typedef struct {
CFIndex version;
// source 的信息
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
// 判斷 source 相等的函數
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
// source 要執行的任務塊
void (*perform)(void *info);
} CFRunLoopSourceContext;
複製代碼
CFRunLoopObserverRef
CFRunLoopObserverRef
是觀察者,每一個Observer都包含了一個回調(函數指針),當RunLoop的狀態發生變化時,觀察者就能經過回調接受到這個變化。主要是用來向外界報告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
kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部事件
};
複製代碼
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver
{
// CoreFoundation 中的 runtime 基礎信息
CFRuntimeBase _base;
// 互斥鎖
pthread_mutex_t _lock;
// observer 對應的 runloop
CFRunLoopRef _runLoop;
// observer 觀察了多少個 runloop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
// observer 優先級
CFIndex _order; /* immutable */
// observer 回調函數
CFRunLoopObserverCallBack _callout; /* immutable */
// observer 上下文
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
複製代碼
CFRunLoopTimerRef
CFRunLoopTimerRef
是基於時間的觸發器,它和NSTimer是toll-free bridged的,能夠混用。其包含一個時間長度和一個回調(函數指針)。
當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。
總結一下關於RunLoop的結構
RunLoop
本質也是一個結構體對象RunloopMode
是指的一個事件循環必須在某種模式下跑,系統會預約義幾個模式。一個Runloop有多個Mode;CFRunloopSource
,CFRunloopTimer
,CFRunloopObserver
這些元素是在Mode裏面的,Mode與這些元素的對應關係也是1對多的。可是必須至少有一個Source
或者Timer
,由於若是Mode爲空,RunLoop運行到空模式不會進行空轉,就會馬上退出。CFRunloopSource
分爲source0
(處理用戶事件)和source1
(處理內核事件)CFRunloopObserver
是監聽和通知Runloop狀態
RunLoop 有五種運行模式,其中常見的有1.2兩種。
1. kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
3. UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,做爲標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並非一種真正的Mode
複製代碼
咱們平時在開發中必定遇到過,當咱們使用NSTimer每一段時間執行一些事情時滑動UIScrollView,NSTimer就會暫停,當咱們中止滑動之後,NSTimer又會從新恢復的狀況,這是因爲RunloopMode
必須在同一個模式下跑。
主線程的 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 裏去。
一個 Mode 能夠將本身標記爲」Common」屬性(經過將其 ModeName 添加到 RunLoop 的 「commonModes」 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部Mode裏。
咱們知道在main函數啓動時,會有Runloop的用DefaultMode默認啓動和使用指定Mode進行啓動,相關的源碼以下,能夠發現,其核心邏輯都是調用了CFRunLoopRunSpecific
函數
/// 用DefaultMode啓動
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
/// 用指定的Mode啓動,並容許設置RunLoop的超時時間
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼
CFRunLoopRunSpecific
接着咱們查看CFRunLoopRunSpecific
函數,根據其代碼,主要總結爲如下幾個步驟:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
{ /* DOES CALLOUT */
CHECK_FOR_FORK();
// 若是 runloop 正在回收中,直接返回 kCFRunLoopRunFinished ,表示 runloop 已經完成
if (__CFRunLoopIsDeallocating(rl))
return kCFRunLoopRunFinished;
// 對 runloop 加鎖
__CFRunLoopLock(rl);
✅// 從 runloop 中查找給定的 mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
✅// 若是找不到 mode,且當前 runloop 的 currentMode 也爲空,進入 if 邏輯
// __CFRunLoopModeIsEmpty 函數結果爲空的話,說明 runloop 已經處理完全部任務
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode))
{
Boolean did = false;
// 若是 currentMode 不爲空
if (currentMode)
// 對 currentMode 解鎖
__CFRunLoopModeUnlock(currentMode);
// 對 runloop 解鎖
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// 暫時取出 runloop 的 per_run_data
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
✅// 取出 runloop 的當前 mode
CFRunLoopModeRef previousMode = rl->_currentMode;
✅// 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
rl->_currentMode = currentMode;
✅// 初始化返回結果 result
int32_t result = kCFRunLoopRunFinished;
✅// 若是註冊了 observer 監聽 kCFRunLoopEntry 狀態(即將進入 loop),則通知 observer
if (currentMode->_observerMask & kCFRunLoopEntry)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
✅✅✅✅// runloop 核心函數 __CFRunLoopRun
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
✅// 若是註冊了 observer 監聽 kCFRunLoopExit 狀態(即將推出 loop),則通知 observer
if (currentMode->_observerMask & kCFRunLoopExit)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
// 對 currentMode 解鎖
__CFRunLoopModeUnlock(currentMode);
// 還原原來的 previousPerRun
__CFRunLoopPopPerRunData(rl, previousPerRun);
// 還原原來的 mode
rl->_currentMode = previousMode;
// 對 runloop 解鎖
__CFRunLoopUnlock(rl);
return result;
}
複製代碼
CFRunLoopRun
CFRunLoopRun
是RunLoop的核心函數,一次運行循環就是一次 CFRunLoopRun
的運行。其5個參數分別表明的意義以下:
CFRunLoopRef rl
: CFRunLoopRef 對象CFRunLoopModeRef rlm
: mode 的名稱CFTimeInterval seconds
: 超時時間Boolean stopAfterHandle
: 處理完 source 後是否直接返回CFRunLoopModeRef previousMode
: 前一次運行循環的 modestatic int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
複製代碼
因爲CFRunLoopRun
的函數過長,邏輯比較複雜,因此咱們精簡了代碼,只講解其中的一些的核心邏輯,主要有如下幾個步驟:
dispatch_source_t
建立一個定時器來處理超時相關的邏輯,若是沒設置會默認一個特別大的數字do...while
循環開始處理事件Observers
: RunLoop 即將觸發 Timer
回調。Observers
: RunLoop 即將觸發 Source0
(非port) 回調。Source1
(基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。Observers
: RunLoop 的線程即將進入休眠(sleep)。mach_msg
等待接受 mach_port
的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
// 聲明一個空的 GCD 定時器
dispatch_source_t timeout_timer = NULL;
// 初始化一個 「超時上下文」 結構體指針對象
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
...
int32_t retVal = 0;
do
{
// 通知 Observers 即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers 即將處理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 Source0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle))
{
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 判斷有無 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
{
// 若是有 Source1,就跳轉到 handle_msg
goto handle_msg;
}
didDispatchPortLastTime = false;
// 通知 Observers 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
do
{
if (kCFUseCollectableAllocator)
{
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
// 等待別的消息來喚醒當前線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort)
{
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue))
;
if (rlm->_timerFired)
{
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
}
else
{
if (msg && msg != (mach_msg_header_t *)msg_buffer)
free(msg);
}
}
else
{
// Go ahead and leave the inner loop.
break;
}
} while (1);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers 結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被 timer 喚醒)
{
// 處理 timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被 GCD 喚醒)
{
// 處理 GCD 主隊列
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
else
{
// 被 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);
return retVal;
}
複製代碼
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
複製代碼
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.
當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。
實際上 RunLoop 底層也會用到 GCD 的東西,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
複製代碼
下面主要介紹下 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。
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 中。
將setImage放到NSDefaultRunLoopMode去作,也就是在滑動的時候並不會去調用這個方法,而是會等到滑動完畢切換到NSDefaultRunLoopMode下面纔會調用。
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
複製代碼
EXC_BAD_ACCESS
是訪問已被釋放的內存致使,野指針錯誤。 由 SIGABRT 引發的Crash 是系統發這個signal給App,程序收到這個signal後,就會把主線程的RunLoop殺死,程序就Crash了 該例只針對 SIGABRT引發的Crash有效。當App發生主線程卡頓時,咱們能夠經過RunLoop來監聽到相對應的堆棧信息,而後進行優化處理。
CFRunLoopObserverContext
觀察者CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
複製代碼
//建立子線程監控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子線程開啓一個持續的 loop 用來進行監控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
//BeforeSources 和 AfterWaiting 這兩個狀態可以檢測到是否卡頓
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
//將堆棧信息上報服務器的代碼放到這裏
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});
複製代碼