一文看完 Runloop
Runloop 是和執行緒緊密相關的一個基礎元件,是很多執行緒有關功能的幕後功臣。 本文將從以下幾個方面來總結runloop:
- 什麼是runloop
- runloop的作用
- runloop和執行緒的關係
- runloop詳細介紹及原始碼分析
- runloop原理分析
- runloop應用
什麼是runloop
- Runloop 還是比較顧名思義的一個東西,說白了就是一種迴圈,只不過它這種迴圈比較高階。一般的do..while 迴圈會導致 CPU 進入忙等待狀態,而 Runloop 則是一種“閒”等待。
runloop
的run
方法原始碼如下所示,是一個do..while
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode,1.0e10,false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製程式碼
- 當沒有事件時,Runloop 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可以讓執行緒在需要做事的時候忙起來,不需要的話就讓執行緒休眠。
-
runloop
實際上是一個物件,這個物件提供了一個入口函式。
runloop的作用
- 保持程式的持續執行,迴圈避免執行緒銷燬
- 處理APP的各種事件(觸控、定時器、performSelector)
- 節省cpu資源、提供程式的效能(該做事就做事,該休息就休息)
runloop在系統裡的使用
在iOS系統裡,下面的這些都有使用runloop,通過斷點檢視堆疊可以看到呼叫的方法名:
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資訊
在timer
的block
裡新增斷點,然後左邊箭頭指示的按鈕不選中(預設是選中的),可以看到runloop的呼叫資訊__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
原始碼如下:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func,CFRunLoopTimerRef timer,void *info) {
if (func) {
func(timer,info);
}
getpid(); // thwart tail-call optimization
}
複製程式碼
關於上面總結的其他幾種呼叫的runloop方法名,都可以用上面的這種除錯方式檢視一下。
runloop和執行緒的關係
- runloop和執行緒是一一對應的
- runloop在首次被執行緒獲取時建立,線上程結束時被銷燬
- 主執行緒預設啟動runloop,子執行緒手動啟動(程式啟動時,啟動主執行緒runloop,
[[NSRunLoop currentRunLoop] run]
)
圖中展現了 Runloop 線上程中的作用:從 input source
和 timer source
接受事件,然後線上程中處理事件。
獲取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;
}
複製程式碼
原始碼裡呼叫了_CFRunLoopGet0()
,這裡是傳一個主執行緒pthread_main_thread_np()
進去,如下定義了它是主執行緒
#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR
CF_EXPORT pthread_t _CF_pthread_main_thread_np(void);
#define pthread_main_thread_np() _CF_pthread_main_thread_np()
複製程式碼
還有一個獲取當前執行緒runloop的方法:同樣是呼叫了_CFRunLoopGet0
,只不過傳進去的是當前執行緒pthread_self()
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
複製程式碼
接下來看獲取執行緒runloop的函式_CFRunLoopGet0
(包括主執行緒和子執行緒)的原始碼
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t,kNilPthreadT)) {
//根據執行緒獲取runloop
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
//如果儲存RunLoop的字典不存在
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault,NULL,&kCFTypeDictionaryValueCallBacks);
//建立主執行緒的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict,pthreadPointer(pthread_main_thread_np()),mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL,dict,(void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//字典裡找runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops,pthreadPointer(t),newLoop);
loop = newLoop;
}
__CFSpinUnlock(&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);
}
}
複製程式碼
如果當前儲存的字典容器不存在,首先就建立了一個容器
CFMutableDictionaryRef
可變字典第二步使用主執行緒建立了一個主執行緒runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
第三步
CFDictionarySetValue(dict,mainLoop);
把主執行緒和它的runloop用key-value形式儲存在這個CFMutableDictionaryRef
字典容器裡以上說明,第一次進來的時候,不管是getMainRunloop還是get子執行緒的runloop,主執行緒的runloop總是會被建立
再看到
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,pthreadPointer(t));
,可以用執行緒
把儲存在字典裡的runloop
取出來如果字典裡沒有找到
runloop
,就根據當前的子執行緒建立一個新的runloop
物件並儲存到字典裡最後一步
if (pthread_equal(t,pthread_self())) {...}
判斷當前的執行緒是不是傳遞進來的執行緒,如果是則建立一個回撥,如果執行緒銷燬,就銷燬當前的runloop這裡驗證了上面的結論1和2: runloop和執行緒是一一對應的(字典儲存)。 runloop在首次被執行緒獲取時建立(並且: 不管獲取的是主執行緒runloop還是子執行緒runloop,總是會建立主執行緒的runloop),線上程結束時被銷燬(通過回撥銷燬)
runloop程式碼驗證
在AppDelegate
打斷點,可以看到主執行緒是有呼叫__CFRunloopRun
方法的,所以證明了上面的結論三: 主執行緒是預設開啟runloop
的 [圖片上傳失敗...(image-ad314f-1571322546197)]) 測試runloop
程式碼如下
- (vod)viewDidLoad {
super viewDidLoad];
DLThread *thread = [[DLThread alloc]initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1repeats:YES block:^(NSTimer * _Nonnul) {
NSLog(@"timer");
}];
}];
thread.name = @"Test";
[thread start];
複製程式碼
DLThread.m
裡只寫瞭如下程式碼
-(void)dealloc{
NSLog(@"執行緒銷燬了");
}
複製程式碼
執行上面的程式碼,發現timer
並沒有列印,說明子執行緒裡開啟timer
沒成功,然後添加了程式碼運行當前執行緒的runloop,如下所示:
DLThread *thread = [[DLThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"Test";
[thread start];
複製程式碼
發現timer
一直在列印了,這裡證明了兩個結論: timer
的執行是和runloop
有關的,子執行緒的runloop
是需要手動開啟的
那麼如何停止timer
呢?新增了一個標記值isStopping
用來退出執行緒
DLThread *thread = [[DLThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
if(self.isStopping){
[NSThread exit];
}
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"Test";
[thread start];
複製程式碼
執行發現,線上程銷燬後,timer
也停止了,這裡側面證明了上面的結論二: runloop
是線上程結束時銷燬的
runloop原始碼分析
在runloop原始碼裡需要探索的:
- CFRunLoop
- CFRunLoopMode
- CFRunLoopSource
- CFRunLoopObserver
- CFRunLoopTimer
CFRunLoop
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 核心向該埠傳送訊息可以喚醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop對應的執行緒
uint32_t _winthread; //
CFMutableSetRef _commonModes; //儲存的是字串,記錄所有標記為common的mode
CFMutableSetRef _commonModeItems;//儲存所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //當前執行的mode
CFMutableSetRef _modes; //儲存的是CFRunLoopModeRef
struct _block_item *_blocks_head; //doblocks的時候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
複製程式碼
可以看到,其實runloop就是一個結構體物件,裡面包含了一個執行緒,一個當前正在執行的mode,N個mode,N個commonMode。
- runloop和執行緒一一對應
- runloop包含多個mode,mode包含多個 mode item(sources,timers,observers)
- runloop一次只能執行在一個model下:
- 切換mode:停止loop -> 設定mode -> 重啟runloop
- runloop通過切換mode來篩選要處理的事件,讓其互不影響
- iOS執行流暢的關鍵
CFRunLoopMode
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode的名稱
Boolean _stopped; //mode是否被終止
char _padding[3];
CFMutableSetRef _sources0; //sources0
CFMutableSetRef _sources1; //sources1
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers; //定時器
CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet; //儲存所有需要監聽的port,比如_wakeUpPort,_timerPort都儲存在這個陣列中
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,N個source0、N個source1、timer、observer和port,可見事件都是由Mode
在管理,而RunLoop
管理Mode
。
它們之間的關係如下圖:
mode
是允許定製的,不過至少要包含一個mode item
(source/timer/observer)。 同一個mode item
可以被多個mode持有
蘋果公開的三種 RunLoop Mode:
- NSDefaultRunLoopMode(kCFRunloopDefaultMode):預設狀態,app通常在這個mode下執行
- UITrackingRunLoopMode:介面跟蹤mode(例如滑動scrollview時不被其他mode影響)
- NSRunLoopCommonModes(kCFRunLoopCommonModes):是前兩個mode的集合,可以把自定義mode用CFRunLoopAddCommonMode函式加入到集合中
還有兩種mode,只需做了解即可:
- GSEventReceiveRunLoopMode:接收系統內部mode,通常用不到
- UIInitializationRunLoopMode:私有,只在app啟動時使用,使用完就不在集合中了
CFRunLoopSource
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; ////用於標記Signaled狀態,source0只有在被標記為Signaled狀態,才會被處理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable,except invalidation */
CFRunLoopSourceContext1 version1; /* immutable,except invalidation */
} _context;
};
複製程式碼
CFRunloopSourceRef
是runloop的資料來源抽象類物件(protocol),由原始碼可以看到共用體(union:在相同的記憶體位置儲存不同的資料型別),可見Source分為兩類:
Source0
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,CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
複製程式碼
source0: 處理App內部事件、APP自己負責管理(觸發)例如:UIEvent CFSocket。 打斷點基本都會看到它。
source0
是非基於Port的。只包含了一個回撥(函式指標),它並不能主動觸發事件。CFRunLoopSourceSignal
(source)將這個事件標記為待處理CFRunLoopWakeUp
來喚醒runloop,讓他處理事件
自定義source實現步驟:
- 建立一個底層source0 源
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(),&context);
- 把我們的建立的source0新增到runloop
CFRunLoopAddSource(rlp,source0,kCFRunLoopDefaultMode)
- 執行訊號,標記待處理
CFRunLoopSourceSignal
- 喚醒runloop去處理
CFRunLoopWakeUp
- 取消移除源
CFRunLoopRemoveSource
- 釋放runloop
CFRelease(rlp)
Source1
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;
複製程式碼
source1:
由runloop和 Mach port管理,Mach port驅動,包含一個 mach_port和一個回撥(函式指標),被用於通過核心和其他執行緒相互發送訊息。
它能夠主動喚醒RunLoop(由作業系統核心進行管理,例如: CFMachPort,CFMessagePort)
還允許實現自己的Source,但一般不會這麼做
CFRunLoopObserver
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 */
};
複製程式碼
它是一個觀察者,能夠監聽Runloop的狀態改變,可以向外部報告runloop狀態的更改,框架中很多機制都由它觸發(如CAAnimation)
在CFRunloop.h
檔案裡可以看到observer監聽的狀態如下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),kCFRunLoopBeforeTimers = (1UL << 1),kCFRunLoopBeforeSources = (1UL << 2),kCFRunLoopBeforeWaiting = (1UL << 5),kCFRunLoopAfterWaiting = (1UL << 6),kCFRunLoopExit = (1UL << 7),kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼
正好和下圖runloop流程裡的observer所對應:
CFRunLoopTimer
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 */
};
複製程式碼
-
CFRunLoopTimer
是定時器,可以在設定的時間點丟擲回撥 -
CFRunLoopTimer
和NSTimer
是toll-free bridged的,可以相互轉換 -
CFRunLoopTimer
的封裝有三種: NSTimer,performSelector和CADisplayLink
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
複製程式碼
簡單總結了這三種timer,如下圖:
runloop原理分析
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName,CFTimeInterval seconds,Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(),modeName,seconds,returnAfterSourceHandled);
}
複製程式碼
CFRunLoopRun
和CFRunLoopRunInMode
都呼叫了CFRunLoopRunSpecific
函式
CFRunLoopRunSpecific
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl,CFStringRef modeName,Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
/// 首先根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl,false);
/// 通知 Observers: RunLoop 即將進入 loop。
__CFRunLoopDoObservers(rl,currentMode,kCFRunLoopEntry);
/// 內部函式,進入loop
result = __CFRunLoopRun(rl,returnAfterSourceHandled,previousMode);
/// 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl,kCFRunLoopExit);
return result;
}
複製程式碼
上面的原始碼是簡化程式碼後的原始碼,實際原始碼複雜一些,根據原始碼可得出如下結論:
- 在進入run loop之前通知observer,狀態為kCFRunLoopEntry
- 在退出run loop之後通知observer,狀態為kCFRunLoopExit
- 進入runloop的時候呼叫了
__CFRunLoopRun
函式
__CFRunLoopRun(核心重點)
/// 核心函式
static int32_t __CFRunLoopRun(CFRunLoopRef rl,CFRunLoopModeRef rlm,Boolean stopAfterHandle,CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
/// 通知 Observers: 即將處理timer事件
__CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeTimers);
/// 通知 Observers: 即將處理Source事件
__CFRunLoopDoObservers(rl,kCFRunLoopBeforeSources)
/// 處理Blocks
__CFRunLoopDoBlocks(rl,rlm);
/// 處理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl,stopAfterHandle);
/// 處理sources0返回為YES
if (sourceHandledThisLoop) {
/// 處理Blocks
__CFRunLoopDoBlocks(rl,rlm);
}
/// 判斷有無埠訊息(Source1)
if (__CFRunLoopServiceMachPort(dispatchPort,&msg,sizeof(msg_buffer),&livePort,&voucherState,NULL)) {
/// 處理訊息
goto handle_msg;
}
/// 通知 Observers: 即將進入休眠
__CFRunLoopDoObservers(rl,kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
/// 等待被喚醒
__CFRunLoopServiceMachPort(waitSet,poll ? 0 : TIMEOUT_INFINITY,&voucherCopy);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
/// 通知 Observers: 被喚醒,結束休眠
__CFRunLoopDoObservers(rl,kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer喚醒) {
/// 處理Timers
__CFRunLoopDoTimers(rl,mach_absolute_time());
} else if (被GCD喚醒) {
/// 處理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1喚醒) {
/// 被Source1喚醒,處理Source1
__CFRunLoopDoSource1(rl,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,previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
複製程式碼
以上是runloop
核心函式的簡寫原始碼(比較清晰易懂)點選下載runloop原始碼:密碼 3kww 還有一個監聽喚醒埠訊息的函式__CFRunLoopServiceMachPort
比較重要,系統核心將這個執行緒掛起,停留在mach_msg_trap狀態,等待接受 mach_port(用於喚醒的埠) 的訊息。執行緒將進入休眠,直到被其他執行緒或另一個程序的某個執行緒向該埠傳送mach_msg訊息喚醒
__CFRunLoopServiceMachPort
/**
* 接收指定核心埠的訊息
*
* @param port 接收訊息的埠
* @param buffer 訊息緩衝區
* @param buffer_size 訊息緩衝區大小
* @param livePort 暫且理解為活動的埠,接收訊息成功時候值為msg->msgh_local_port,超時時為MACH_PORT_NULL
* @param timeout 超時時間,單位是ms,如果超時,則RunLoop進入休眠狀態
*
* @return 接收訊息成功時返回true 其他情況返回false
*/
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) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0; //訊息頭的標誌位
msg->msgh_local_port = port; //源(發出的訊息)或者目標(接收的訊息)
msg->msgh_remote_port = MACH_PORT_NULL; //目標(發出的訊息)或者源(接收的訊息)
msg->msgh_size = buffer_size;//訊息緩衝區大小,單位是位元組
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
//通過mach_msg傳送或者接收的訊息都是指標,
//如果直接傳送或者接收訊息體,會頻繁進行記憶體複製,損耗效能
//所以XNU使用了單一核心的方式來解決該問題,所有核心元件都共享同一個地址空間,因此傳遞訊息時候只需要傳遞訊息的指標
ret = mach_msg(msg,MACH_RCV_MSG|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),port,timeout,MACH_PORT_NULL);
CFRUNLOOP_WAKEUP(ret);
//接收/傳送訊息成功,給livePort賦值為msgh_local_port
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
//MACH_RCV_TIMEOUT
//超出timeout時間沒有收到訊息,返回MACH_RCV_TIMED_OUT
//此時釋放緩衝區,把livePort賦值為MACH_PORT_NULL
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
//MACH_RCV_LARGE
//如果接收緩衝區太小,則將過大的訊息放在佇列中,並且出錯返回MACH_RCV_TOO_LARGE,
//這種情況下,只返回訊息頭,呼叫者可以分配更多的記憶體
if (MACH_RCV_TOO_LARGE != ret) break;
//此處給buffer分配更大記憶體
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer,buffer_size);
}
HALT;
return false;
}
複製程式碼
runloop應用
事件響應
當一個硬體事件(觸控/鎖屏/搖晃/加速)發生後,首先有
IOKit.framework
生成一個IOHIDEvent
事件並由SpringBoard
接受,之後由mach port
轉發給需要的App程序。蘋果註冊了一個 Source1 來接受系統事件,通過回撥函式觸發Source0(所以Event實際上是基於Source0)的,呼叫
_UIApplicationHandleEventQueue()
進行應用內部的分發。_UIApplicationHandleEventQueue()
會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理螢幕旋轉/傳送給 UIWindow 等。
手勢識別
當上面的
_UIApplicationHandleEventQueue()
識別了一個手勢時,其首先會呼叫 Cancel 將當前的touchesBegin/Move/End
系列回撥打斷。隨後系統將對應的UIGestureRecognizer
標記為待處理。蘋果註冊了一個 Observer 監測
BeforeWaiting
(Loop即將進入休眠) 事件,這個Observer的回撥函式是_UIGestureRecognizerUpdateObserver()
,其內部會獲取所有剛被標記為待處理的GestureRecognizer
,並執行GestureRecognizer
的回撥。當有
UIGestureRecognizer
的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。
介面重新整理
當UI發生改變時(Frame變化,UIView/CALayer的結構變化)時,或手動呼叫了
UIView/CALayer的setNeedsLayout/setNeedsDisplay
方法後,這個UIView/CALayer就被標記為待處理。蘋果註冊了一個用來監聽
BeforeWaiting
和Exit
的Observer,在他的回撥函式裡會遍歷所有待處理的UIView/CALayer
來執行實際的繪製和調整,並更新UI介面。
AutoreleasePool
主執行緒Runloop註冊了兩個Observers,其回撥都是
_wrapRunloopWithAutoreleasePoolHandler
Observers1
監聽Entry
事件: 優先順序最高,確保在所有的回撥前建立釋放池,回撥內呼叫_objc_autoreleasePoolPush()
建立自動釋放池Observers2
監聽BeforeWaiting
和Exit
事件: 優先順序最低,保證在所有回撥後釋放釋放池。BeforeWaiting
事件:呼叫_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊池並建立新池,Exit
事件: 呼叫_objc_autoreleasePoolPop()
,釋放自動釋放池
tableView延遲載入圖片,保證流暢
給ImageView
載入圖片的方法用PerformSelector
設定當前執行緒的RunLoop的執行模式kCFRunLoopDefaultMode
,這樣滑動時候就不會執行載入圖片的方法 [self.imgView performSelector:@selector(setImage:) withObject:cellImg afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Timer不被ScrollView的滑動影響
-
+timerWihtTimerInterval...
建立timer -
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]
把timer加到當前runloop,使用佔位模式 -
runloop run/runUntilData
手動開啟子執行緒runloop - 使用GCD建立定時器,GCD建立的定時器不會受RunLoop的影響
// 獲得佇列
dispatch_queue_t queue = dispatch_get_main_queue();
// 建立一個定時器(dispatch_source_t本質還是個OC物件)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,queue);
// 設定定時器的各種屬性(幾時開始任務,每隔多長時間執行一次)
// GCD的時間引數,一般是納秒(1秒 == 10的9次方納秒)
// 比當前時間晚1秒開始執行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒執行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer,start,interval,0);
// 設定回撥
dispatch_source_set_event_handler(self.timer,^{
NSLog(@"------------%@",[NSThread currentThread]);
});
// 啟動定時器
dispatch_resume(self.timer);
複製程式碼
GCD
dispatch_async(dispatch_get_main_queue)
使用到了RunLooplibDispatch
向主執行緒的Runloop
傳送訊息將其喚醒,並從訊息中取得block,並在回撥__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
裡執行這個block
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。
AFNetworking
- 使用runloop開啟常駐執行緒
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
複製程式碼
- 給 runloop 新增
[NSMachPort port](source1)
使runloop不退出,實際並沒有給這個port發訊息
AsyncDisplayKit
仿照 QuartzCore/UIKit
框架的模式,實現了一套類似的介面更新的機制:即在主執行緒的 RunLoop 中新增一個 Observer,監聽了 kCFRunLoopBeforeWaiting
和 kCFRunLoopExit
事件,在收到回撥時,遍歷所有之前放入佇列的待處理的任務,然後一一執行。
卡頓檢測
dispatch_semaphore_t 是一個訊號量機制,訊號量到達、或者 超時會繼續向下進行,否則等待,如果超時則返回的結果必定不為0,訊號量到達結果為0。GCD訊號量-dispatch_semaphore_t
通過監聽mainRunloop的狀態和訊號量阻塞執行緒的特點來檢測卡頓,通過
kCFRunLoopBeforeSource
和kCFRunLoopBeforeWaiting
的間隔時長超過自定義閥值則記錄堆疊資訊。推薦文章: RunLoop實戰:實時卡頓監控
FPS檢測
建立CADisplayLink物件的時候會指定一個selector,把建立的CADisplayLink物件加入runloop,所以就實現了以螢幕重新整理的頻率呼叫某個方法。
在呼叫的方法中計算執行的次數,用次數除以時間,就算出了FPS。
注:iOS正常重新整理率為每秒60次。
@implementation ViewController {
UILabel *_fpsLbe;
CADisplayLink *_link;
NSTimeInterval _lastTime;
float _fps;
}
- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
NSLog(@"count = %d,delta = %f,_lastTime = %f,_fps = %.0f",_count,delta,_lastTime,_fps);
self.count = 0;
_fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}
複製程式碼
防崩潰處理
NSSetUncaughtExceptionHandler(&HandleException);
監聽異常訊號SIGILL
,SIGTRAP
,SIGABRT
,SIGBUS
,SIGSEGV
,SIGFPE
回撥方法內建立一個Runloop,將主執行緒的所有Runmode都拿過來跑,作為應用程式主Runloop的替代。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);
while (captor.needKeepAlive) {
for (NSString *mode in (__bridge NSArray *)allModesRef) {
if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
continue;
}
CFStringRef modeRef = (__bridge CFStringRef)mode;
CFRunLoopRunInMode(modeRef,keepAliveReloadRenderingInterval,false);
}
}
複製程式碼
- 可以記錄堆疊資訊,上傳伺服器或者彈出友好提示頁面等一系列操作。
常駐執行緒
可以把自己建立的執行緒新增到Runloop中,做一些頻繁處理的任務,例如:檢測網路狀態,定時上傳一些資訊等。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@",[NSThread currentThread]);
@autoreleasepool{
/*如果不加這句,會發現runloop創建出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
下面的方法給runloop新增一個NSport,就是新增一個事件源,也可以新增一個定時器,或者observer,讓runloop不會掛掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1,2,3實現的效果相同,讓runloop無限期執行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
複製程式碼
以上均為個人對runloop的資料收集及部分理解,如有錯誤請指正,歡迎討論。
歡迎加入iOS開發交流學習群(密碼123),我們一起共同學習,共同成長
收錄:原文地址