RunLoop的基本概念
- RunLoop運行循環,其本質是一個 do-while循環,與普通的while循環是有區別的,普通的while循環會導致CPU進入
忙等待狀態,即一直消耗CPU
,而RunLoop則不會,RunLoop是一種閑等待,無事件處理時會進入休眠狀態,不會消耗CPU資源
;
RunLoop的基本作用
- 保證應用程序能持續穩定的運行而不退出,例如我們的在app啟動之后,默認會開啟一個主運行循環RunLoop,其能讓app持續穩定運行,不會中途退出,除非用戶手動殺掉app進程;
- 處理App中的各種事件(觸摸、定時器、performSelector),,也就是說RunLoop是一個事件處理循環;
- 節省CPU資源,提升應用程序的性能,即當沒有事件處理時會進入休眠狀態,不會消耗CPU資源;
RunLoop的源碼分析
- OSX/iOS 系統中,提供了兩個這樣的對象:
NSRunLoop
和CFRunLoopRef
CFRunLoopRef
是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的,NSRunLoop
是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的; - CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/ 下載到整個 CoreFoundation 的源碼來查看
- RunLoop不能手動創建,只能獲取,被動的創建,下面提供了獲取主線程RunLoop與獲取當前正在運行RunLoop函數的源碼:
//獲取主線程RunLoop
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;
}
//獲取當前正在運行的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
- 最終都會進入
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
函數,參數是當前線程;
//全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//pthread_t為空 默認設置成main線程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
//保證線程安全
__CFLock(&loopsLock);
if (!__CFRunLoops) {
//第一次進入時,初始化全局Dic,并先為主線程創建一個RunLoop。
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//創建主運行循環
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//將線程和RunLoop 以鍵值對的形式 存儲在全局字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//獲取其他線程的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//RunLoop不存在,創建一個新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//線程與RunLoop 鍵值對存在在全局字典中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
- 易知
線程與RunLoop是以鍵值對
的形式存儲在一個全局字典(__CFRunLoops)
中的,所以線程與RunLoop之間是一一對應的關系; - 當
全局字典(__CFRunLoops)
為空,即第一次初始化時,會創建主線程循環
,并將主線程和主RunLoop 以鍵值對的形式 存儲在__CFRunLoops中; - 接下來,進入獲取其他線程的RunLoop,若RunLoop不存在,會創建RunLoop,執行
__CFRunLoopCreate(t)
函數;
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
//RunLoop的屬性設置
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}
- 創建RunLoop對象,并進行相關屬性的設置;
RunLoop的底層結構
- RunLoop的結構體定義如下:
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;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
-
_currentMode
:表示RunLoop當前的模式,是CFRunLoopModeRef
結構體類型; -
_modes
:表示RunLoop的模式集合,說明RunLoop可以有多種不同的模式; -
_commonModeItems
:是<Source/Observer/Timer>組的Items集合;
RunLoop的模式CFRunLoopModeRef
- 其底層結構體如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0; //事件源0
CFMutableSetRef _sources1; //事件源1
CFMutableArrayRef _observers; //觀察者
CFMutableArrayRef _timers; //定時器
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中包含以下主要部分:
- 事件源:
_sources0
,_sources1
- 觀察者源:
_observers
- 定時器源:
_timers
- RunLoop擁有多種不同的Mode,每個Mode中包含Source/Timer/Observer; Source/Timer/Observer又稱為一組Item,
子線程的RunLoop中必須要有Item的存在,也就是說必須要有源,有任務事件處理,否則RunLoop會無事可做,就會自動退出
;
RunLoop_Mode.png
- RunLoopMode的類型有如下:
- RunLoopMode在蘋果文檔中提及的有五個,而在iOS中公開暴露出來的只有
NSDefaultRunLoopMode
和NSRunLoopCommonModes
,NSRunLoopCommonModes 實際上是一個Mode 的集合
,默認包括NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
; -
NSDefaultRunLoopMode
:App的默認 Mode,通常主線程是在這個 Mode 下運行的; -
NSConnectionReplyMode
; -
NSModalPanelRunLoopMode
; -
NSEventTrackingRunLoopMode
:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響; -
NSRunLoopCommonModes
:偽模式,靈活性更好;
- RunLoopMode在蘋果文檔中提及的有五個,而在iOS中公開暴露出來的只有
RunLoop的事件源_sources0與_sources1 -- CFRunLoopSourceRef
-
_sources0
,_sources1
屬于CFRunLoopSourceRef
類型,其底層結構體如下所示:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
- CFRunLoopSourceRef 是事件產生的地方,可以看到Source源有兩種類型:
Source0
和Source1
-
Source0 屬于CFRunLoopSourceContext類型
:其只包含了一個回調(函數指針),它并不能主動觸發事件,使用時你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件,主要包括觸摸事件
,PerformSelectors事件
; - CFRunLoopSourceContext結構體如下:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
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);
void (*perform)(void *info);
} CFRunLoopSourceContext;
- 代碼驗證如下:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"點擊");
}
@end
- 點擊屏幕,控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_45.png
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"test");
}
@end
- 控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_47.png
-
Source1 屬于CFRunLoopSourceContext1 類型
:其包含了一個 mach_port 和一個回調(函數指針),被用于基于Port的線程間通信
; - CFRunLoopSourceContext1結構體如下:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
RunLoop的定時器源timers -- CFRunLoopTimerRef
- RunLoop的定時器timers的類型為
CFRunLoopTimerRef
,其底層結構體如下所示:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
其是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用,其包含一個
時間長度
和一個回調(函數指針)
,當其加入到 RunLoop 時,RunLoop會注冊對應的時間點
,當到指定時間點時,RunLoop會被喚醒以執行那個回調(函數指針);代碼測試:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer調用了");
}];
}
@end
- 控制臺輸入bt,打印函數調用堆棧如下:
Snip20210528_48.png
RunLoop的觀察者源Observers -- CFRunLoopObserverRef
- RunLoop的觀察者Observers屬于
CFRunLoopObserverRef類型
,其底層結構體如下:
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
- 每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接收到這個變化,可以觀測的時間點狀態有以下幾個:
/* Run Loop Observer Activities */
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
};
- 測試代碼:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//創建監聽者
/*
第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
第二個參數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態
第三個參數 Boolean repeats:YES:持續監聽 NO:不持續
第四個參數 CFIndex order:優先級,一般填0即可
第五個參數 :回調 兩個參數observer:監聽者 activity:監聽的事件
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監聽者
/*
第一個參數 CFRunLoopRef rl:要監聽哪個RunLoop,這里監聽的是主線程的RunLoop
第二個參數 CFRunLoopObserverRef observer 監聽者
第三個參數 CFStringRef mode 要監聽RunLoop在哪種運行模式下的狀態
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的內存管理(Core Foundation)
凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最后做一次release
GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經納入到了ARC中,所以我們不需要管了
*/
CFRelease(observer);
}
@end
- 點擊屏幕,控制臺打印結果如下:
Snip20210528_51.png
- 可以看到Observer確實能監聽到RunLoop的運行狀態,包括喚醒,休息,以及處理各種事件;
Items源所觸發的事件回調類型
- 上面已經驗證了Source0,定時器Timer與observer源的事件回調;
Snip20210528_49.png
- block調用:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
- 調用timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- 響應source0:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
- 響應source1:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
- GCD主隊列:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
- observer源:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION_
_
RunLoop的運行
- RunLoop執行,底層調用如下:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
- 內部調用
CFRunLoopRunSpecific
函數:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
////首先根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
////通知 Observers: RunLoop 即將進入loop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
////進入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
////通知 Observers: RunLoop 即將退出loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
-
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)
:通知Observers,RunLoop即將進入循環,此處有Observer會創建AutoreleasePool: _objc_autoreleasePoolPush()
-
__CFRunLoopRun
:核心函數,進入RunLoop運行循環; -
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit)
:通知Observers,RunLoop即將退出循環,此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
- 進入__CFRunLoopRun源碼,由于這部分代碼較多,于是這里用偽代碼代替。其主要邏輯是根據不同的事件源進行不同的處理,當RunLoop休眠時,可以通過相應的事件喚醒RunLoop;
//核心函數
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){
//通過GCD開啟一個定時器,然后開始跑圈
dispatch_source_t timeout_timer = NULL;
...
dispatch_resume(timeout_timer);
int32_t retVal = 0;
//處理事務,即處理items
do {
// 通知 Observers: 即將處理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即將處理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 處理sources0返回為YES
if (sourceHandledThisLoop) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無端口消息(Source1)
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 處理消息
goto handle_msg;
}
// 通知 Observers: 即將進入休眠 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 等待被喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 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 if (被source1喚醒){
// 被Source1喚醒,處理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
// 處理block
__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;//結束
}
}while (0 == retVal);
return retVal;
}
- 可以看到,實際上 RunLoop 就是這樣一個函數,其內部是一個
do-while 循環
,當調用 CFRunLoopRun()函數時,線程就會一直停留在這個循環里,直到超時或被手動停止,該函數才會返回;
image.png
RunLoop與AutoreleasePool之間的關系
- App啟動后,蘋果在主線程 RunLoop 里注冊了
兩個Observer
,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()
; - 第一個Observer源
監聽RunLoop的進入狀態
,其回調會調用_objc_autoreleasePoolPush() 創建自動釋放池
,此回調用執行的優先級是最高的,保證自動釋放池的創建 在其他所有回調執行之前; - 第二個Observer源監聽
RunLoop進入休眠狀態
和RunLoop的退出狀態
,RunLoop進入休眠狀態時 回調執行 釋放舊的自動釋放池并創建新的自動釋放池,RunLoop的退出狀態時 回調執行釋放自動釋放池,此時回調執行的優先級是最低的,保證自動釋放池的釋放,在其他所有回調執行之后; - 在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的,這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了;
- 調試,在工程中加入符號斷點
_wrapRunLoopWithAutoreleasePoolHandler
,然后運行工程,我運行的Xcode版本為11.3.1;
Snip20210528_55.png
Snip20210528_56.png
面試題一:ScrollView發生滾動的時候,NSTimer為什么會停止工作?
- 當ScrollView發生滾動的時候,主線程的RunLoop會切換到UITrackingRunLoopModel模式下,而NSTimer默認是添加到UIDefaultRunLoopModel下的,所以NSTimer不會執行回調,當ScrollView停止滾動是主線程的RunLoop又會切換到UIDefaultRunLoopModel模式下,此時NSTimer會恢復工作,執行自己的回調方法,為了保證NSTimer在UITrackingRunLoopModel模式下也能工作,可將主線程RunLoop的model模式設置成組合模式NSRunLoopCommonModes;
面試題二:RunLoop的實際應用
-
應用一:線程保活
,一般情況下,當線程執行完任務之后就會自動退出銷毀,如下所示:
#import "ViewController.h"
#import "YYThread.h"
@interface ViewController ()
@property(nonatomic,strong)YYThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[YYThread alloc]initWithTarget:self selector:@selector(callBack) object:nil];
[self.thread start];
}
- (void)callBack {
NSLog(@"%s %@ -- 開始",__func__,[NSThread currentThread]);
NSLog(@"%s -- 結束",__func__);
}
@end
- 自定義線程YYThread在執行完callBack方法之后,就會退出銷毀,下面使用RunLoop保活YYThread線程,并能執行其他任務,實現如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[YYThread alloc]initWithTarget:self selector:@selector(callBack) object:nil];
[self.thread start];
}
- (void)callBack {
NSLog(@"%s %@ -- 開始",__func__,[NSThread currentThread]);
//實現線程保活
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s -- 結束",__func__);
}
- (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]);
}
點擊屏幕,讓YYThread線程繼續執行test函數,說明RunLoop確實能讓YYThread線程存活,繼續執行其他任務;
應用二:監聽App的卡頓
,原理是子線程向主線程發送消息,若主線程未在指定的時間內作出應答,說明主線程在執行耗時任務,存在App的卡頓,代碼實現見iOS性能優化01 -- 卡頓優化應用三:優化tableView的滾動
,利用RunLoop的Model特性,可將更新UI的操作放到
UIDefaultRunLoopModel模式中,這樣tableView在滾動時不會因為更新UI而影響到平滑的滾動效果,網絡圖片加載完成,更新UI到ImageView時,使用UIDefaultRunLoopModel模式,代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *networkImage = nil;
[self.icon performSelector:@selector(setImage:) withObject:networkImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
}