RunLoop概念
RunLoop介紹
RunLoop 是什么?RunLoop 還是比較顧名思義的一個(gè)東西,說(shuō)白了就是一種循環(huán),只不過(guò)它這種循環(huán)比較高級(jí)。一般的 while 循環(huán)會(huì)導(dǎo)致 CPU 進(jìn)入忙等待狀態(tài),而 RunLoop 則是一種“閑”等待,這部分可以類(lèi)比 Linux 下的 epoll。當(dāng)沒(méi)有事件時(shí),RunLoop 會(huì)進(jìn)入休眠狀態(tài),有事件發(fā)生時(shí), RunLoop 會(huì)去找對(duì)應(yīng)的 Handler 處理事件。RunLoop 可以讓線(xiàn)程在需要做事的時(shí)候忙起來(lái),不需要的話(huà)就讓線(xiàn)程休眠。
沒(méi)有Runloop的程序
我們通過(guò)Xcode新建一個(gè)命令行項(xiàng)目,main.m
文件里的代碼如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
程序在執(zhí)行完代碼NSLog(@"Hello, World!");
之后,就會(huì)通過(guò) return 0;
推出程序,這是一種線(xiàn)性的執(zhí)行流程。
我們?cè)傩陆ㄒ粋€(gè)iOS項(xiàng)目,你看到的main.m
文件是這個(gè)樣子的
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我們會(huì)進(jìn)入app的界面,然后app就不會(huì)退出了,會(huì)一直運(yùn)行著。
在命令行工程里面的main.m
里面,是沒(méi)有加Runloop的,而iOS工程的main.m
里面,其實(shí)在UIApplicationMain()
這個(gè)方法中,系統(tǒng)加上了Runloop,讓程序可以一直循環(huán)運(yùn)行下去不退出。
iOS
項(xiàng)目,在main
函數(shù)中系統(tǒng)就會(huì)自動(dòng)幫我們創(chuàng)建runloop
對(duì)象:return UIApplicationMain(argc, argv, nil, appDelegateClassName);
.
RunLoop
的基本作用就是:
保證程序的基本運(yùn)行.程序一啟動(dòng)就會(huì)開(kāi)一個(gè)主線(xiàn)程,主線(xiàn)程一開(kāi)起來(lái)就會(huì)跑一個(gè)主線(xiàn)程對(duì)應(yīng)的RunLoop,RunLoop保證主線(xiàn)程不會(huì)被銷(xiāo)毀,也就保證了程序的持續(xù)運(yùn)行
處理App中的各種事件 (比如:觸摸事件,定時(shí)器事件 等等).
節(jié)省 CPU 資源,提高程序性能: 該做事時(shí)做事,沒(méi)有事的時(shí)候就休息.(程序運(yùn)行起來(lái)時(shí),當(dāng)什么操作都沒(méi)有做的時(shí)候,RunLoop就告訴CPU,現(xiàn)在沒(méi)有事情做,我要去休息,這時(shí)CPU就會(huì)將其資源釋放出來(lái)去做其他的事情,當(dāng)有事情做的時(shí)候RunLoop就會(huì)立馬起來(lái)去做事情)
RunLoop
工作原理的偽代碼大概如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//睡眠中等待消息
int message = sleep_and_wait();
//處理消息
retVal = process_message(message);
} while (retVal = 0);
return 0;
}
}
流程:條件成立的時(shí)候一直循環(huán):有事情就處理事情,沒(méi)有事情就休眠睡覺(jué).Runloop其實(shí)就是一個(gè)do-while
循環(huán),每次循環(huán)一圈,都會(huì)判斷一次retVal
,決定是否結(jié)束循環(huán),繼續(xù)執(zhí)行循環(huán)外的代碼。
RunLoop對(duì)象
iOS
中提供了兩套API
來(lái)訪(fǎng)問(wèn)RunLoop
:
-
Foundation : NSRunLoop
: OC 框架 -
Core Foundation : CFRunLoopRef
: C 語(yǔ)言框架
NSRunLoop
和CFRunLoopRef
都代表Runloop對(duì)象,NSRunLoop
是基于CFRunLoopRef
的一層OC包裝,CFRunLoopRef
是開(kāi)源的我們下載好源代碼后新建一個(gè)項(xiàng)目,把源代碼拖到項(xiàng)目中.
Runloop對(duì)象的獲取
-
Foundation
[NSRunloop currentRunLoop];
獲得當(dāng)前線(xiàn)程的RunLoop對(duì)象
[NSRunLoop mainRunLoop];
獲得主線(xiàn)程的Runloop對(duì)象 -
Core Foundation
CFRunLoopGetCurrent();
獲得當(dāng)前線(xiàn)程的RunLoop對(duì)象
CFRunLoopGetMain();
獲得主線(xiàn)程的Runloop對(duì)象
CFRunLoop.c
文件 -> 然后找到CFRunLoopGetCurrent
函數(shù) -> 進(jìn)入_CFRunLoopGet0
函數(shù)
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//??????根據(jù)線(xiàn)程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
????????????
static CFMutableDictionaryRef __CFRunLoops = NULL; //字典
// 獲取 runloop 對(duì)象 參數(shù):傳入一個(gè) 字典 和 key (線(xiàn)程)
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//如果 runloop 不存在 , 就創(chuàng)建,并放到字典中
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
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);
}
????????????
}
RunLoop是在第一次獲取的時(shí)候創(chuàng)建的,并且
RunLoop和 線(xiàn)程 是 一一對(duì)應(yīng)的關(guān)系,
RunLoop是存放在一個(gè)全局字典中:以線(xiàn)程作為
key,
RunLoop作為
value.
Runloop與線(xiàn)程
為什么聊Runloop一定要搭上線(xiàn)程?我們知道,程序里的每一句代碼,都會(huì)在線(xiàn)程(
主線(xiàn)程/子線(xiàn)程
)里面被執(zhí)行,上面四種獲得Runloop對(duì)象的代碼也不例外,一定是跑在線(xiàn)程里面的。之前我們說(shuō)到,Runloop是為了讓程序不退出,其實(shí)更準(zhǔn)確地說(shuō),是為了保持某個(gè)線(xiàn)程不結(jié)束,只要還有未結(jié)束的線(xiàn)程,那么整個(gè)程序就不會(huì)退出,因?yàn)榫€(xiàn)程是程序的運(yùn)行的調(diào)度的基本單元。線(xiàn)程與Runloop的關(guān)系是
一對(duì)一
的,一個(gè)新創(chuàng)建的線(xiàn)程,是沒(méi)有Runloop對(duì)象的,當(dāng)我們?cè)谠摼€(xiàn)程里第一次通過(guò)上面的API獲得Runloop時(shí),Runloop對(duì)象才會(huì)被創(chuàng)建,并且通過(guò)一個(gè)全局字典將Runloop對(duì)象和該線(xiàn)程存儲(chǔ)綁定在一起,形成一對(duì)一關(guān)系。Runloop會(huì)在線(xiàn)程結(jié)束時(shí)銷(xiāo)毀,主線(xiàn)程的Runloop已經(jīng)自動(dòng)獲取過(guò)(創(chuàng)建),子線(xiàn)程默認(rèn)沒(méi)有開(kāi)啟RunLoop(直到你在該線(xiàn)程獲取它)。RunLoop對(duì)象創(chuàng)建后,會(huì)被保存在一個(gè)全局的Dictionary里,線(xiàn)程作為
key
,Runloop對(duì)象作為value
。
Runloop對(duì)象底層結(jié)構(gòu)
我們可以在源碼CFRunloop.c
中找到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;//RunLoop對(duì)應(yīng)的線(xiàn)程
uint32_t _winthread;
CFMutableSetRef _commonModes;//存儲(chǔ)的是字符串,記錄所有標(biāo)記為common的mode
CFMutableSetRef _commonModeItems;//存儲(chǔ)所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode;//當(dāng)前運(yùn)行的mode
CFMutableSetRef _modes;//存儲(chǔ)的是CFRunLoopModeRef
//???????????? 核心組成 ????????????
struct _block_item *_blocks_head;//doblocks的時(shí)候用到
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
RunLoop Mode Mode可以視為事件的管家,一個(gè)Mode管理著各種事件,它的結(jié)構(gòu)如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名稱(chēng)
Boolean _stopped; //mode是否被終止
char _padding[3];
//幾種事件
//???????????? 核心組成 ????????????
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers;//定時(shí)器
//???????????? 核心組成 ????????????
CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet;//保存所有需要監(jiān)聽(tīng)的port,比如_wakeUpPort,_timerPort都保存在這個(gè)數(shù)組中
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 */
};
一個(gè)CFRunLoopMode對(duì)象有一個(gè)name,若干source0、source1、timer、observer和若干port,可見(jiàn)事件都是由Mode在管理,而RunLoop管理Mode。
Runloop相關(guān)的5個(gè)相關(guān)的類(lèi)
- CFRunLoopRef——這個(gè)就是Runloop對(duì)象
-
CFRunLoopModeRef——其內(nèi)部主要包括四個(gè)容器,分別用來(lái)存放
source0
、source1
、observer
以及timer
-
CFRunLoopSourceRef——分為
source0
和source1
source0
:包括 觸摸事件處理、[performSelector: onThread: ]
source1
:包括 基于Port的線(xiàn)程間通信、系統(tǒng)事件捕捉 -
CFRunLoopTimerRef——
timer
事件,包括我們?cè)O(shè)置的定時(shí)器事件、[performSelector: withObject: afterDelay:]
-
CFRunLoopObserverRef——監(jiān)聽(tīng)者,Runloop狀態(tài)變更的時(shí),會(huì)通知監(jiān)聽(tīng)者進(jìn)行函數(shù)回調(diào),UI界面的刷新就是在監(jiān)聽(tīng)到Runloop狀態(tài)為
BeforeWaiting
時(shí)進(jìn)行的。
對(duì)于以上這幾個(gè)類(lèi)相互之間的關(guān)系,可以通過(guò)如下的圖來(lái)描繪
從圖中可看出,一個(gè)RunLoop對(duì)象里面包含了若干個(gè)RunLoopMode
,RunLoop內(nèi)部是通過(guò)一個(gè)集合容器_modes
來(lái)裝這些RunLoopMode
的。
RunLoopMode內(nèi)部核心內(nèi)容是4個(gè)數(shù)組容器,分別用來(lái)裝source0
,source1
,observer
和timer
,RunLoop對(duì)象內(nèi)部有一個(gè)_currentMode
,它指向了該RunLoop對(duì)象的其中一個(gè)RunLoopMode
,它代表的含義是RunLoop當(dāng)前所運(yùn)行的RunLoopMode
,所謂“運(yùn)行”也就是說(shuō),RunLoop當(dāng)前只會(huì)執(zhí)行_currentMode
所指向的RunLoopMode
里面所包括的事件(source0、source1、observer、timer
).RunLoop對(duì)象內(nèi)部還包括一個(gè)線(xiàn)程對(duì)象_pthread
,這就是跟它一一對(duì)應(yīng)的那個(gè)線(xiàn)程對(duì)象。
RunLoop Source
Run Loop Source分為Source、Observer、Timer三種,他們統(tǒng)稱(chēng)為ModeItem。
CFRunLoopSource
根據(jù)官方的描述,CFRunLoopSource是對(duì)input sources的抽象。CFRunLoopSource分source0和source1兩個(gè)版本,它的結(jié)構(gòu)如下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //?????? 用于標(biāo)記Signaled狀態(tài),source0只有在被標(biāo)記為Signaled狀態(tài),才會(huì)被處理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
source0
source0是App內(nèi)部事件,由App自己管理的UIEvent、CFSocket都是source0。當(dāng)一個(gè)source0事件準(zhǔn)備執(zhí)行的時(shí)候,必須要先把它標(biāo)記為signal狀態(tài).
App自己管理的UIEven,包括觸摸事件處理、[performSelector: onThread: ]
,這個(gè)也可以通過(guò)代碼來(lái)驗(yàn)證一下。首先看一下觸摸事件,在ViewController
里面重寫(xiě)方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"點(diǎn)擊屏幕");
}
#9 0x00007fff2039038a in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
可以看出系統(tǒng)是通過(guò)一個(gè)CF的函數(shù)__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
來(lái)調(diào)用UIKit進(jìn)行事件處理的
source0是非基于Port的。只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件。使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來(lái)喚醒 RunLoop,讓其處理這個(gè)事件。
source1由RunLoop和內(nèi)核管理,source1帶有mach_port_t,可以接收內(nèi)核消息并觸發(fā)回調(diào),以下是source1的結(jié)構(gòu)體
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除了包含回調(diào)指針外包含一個(gè)mach port,Source1可以監(jiān)聽(tīng)系統(tǒng)端口和通過(guò)內(nèi)核和其他線(xiàn)程通信,接收、分發(fā)系統(tǒng)事件,它能夠主動(dòng)喚醒RunLoop(由操作系統(tǒng)內(nèi)核進(jìn)行管理,例如CFMessagePort消息)。官方也指出可以自定義Source,因此對(duì)于CFRunLoopSourceRef來(lái)說(shuō)它更像一種協(xié)議,框架已經(jīng)默認(rèn)定義了兩種實(shí)現(xiàn),如果有必要開(kāi)發(fā)人員也可以自定義,詳細(xì)情況可以查看官方文檔。
RunLoop 的狀態(tài):
RunLoop
有以下幾種狀態(tài):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入 休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 以上所有狀態(tài)
};
下面我們寫(xiě)代碼來(lái)驗(yàn)證一下這些狀態(tài)的切換.首先寫(xiě)代碼測(cè)試一下,NStimer
喚醒RunLoop
:
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;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// 創(chuàng)建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
}
RUN> ????????????
image-20210512124423553可以看出,Runloop的狀態(tài)切換時(shí),都會(huì)被
observer
監(jiān)聽(tīng)到。
我們?cè)賱?chuàng)建一個(gè)UITextView
,拖動(dòng)UITextView
看看RunLoop
狀態(tài)的切換情況
- (void)viewDidLoad {
[super viewDidLoad];
// 創(chuàng)建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);
}
RUN> ????????????
image-20210512130047036拖動(dòng)
UITextView
看看2021-05-12 12:46:02.808851+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - kCFRunLoopDefaultMode 2021-05-12 12:46:02.809032+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - UITrackingRunLoopMode 2021-05-12 12:46:04.280133+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - UITrackingRunLoopMode 2021-05-12 12:46:04.280280+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - kCFRunLoopDefaultMode
可以看到
RunLoop
頻繁的在kCFRunLoopDefaultMode
和UITrackingRunLoopMode
之間切換.
特別備注
本系列文章總結(jié)自MJ老師在騰訊課堂iOS底層原理班(下)/OC對(duì)象/關(guān)聯(lián)對(duì)象/多線(xiàn)程/內(nèi)存管理/性能優(yōu)化,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán),請(qǐng)聯(lián)系我刪除,謝謝!