RunLoop概念
RunLoop介紹
RunLoop 是什么奶段?RunLoop 還是比較顧名思義的一個東西饥瓷,說白了就是一種循環(huán),只不過它這種循環(huán)比較高級痹籍。一般的 while 循環(huán)會導(dǎo)致 CPU 進入忙等待狀態(tài)呢铆,而 RunLoop 則是一種“閑”等待,這部分可以類比 Linux 下的 epoll蹲缠。當沒有事件時棺克,RunLoop 會進入休眠狀態(tài),有事件發(fā)生時线定, RunLoop 會去找對應(yīng)的 Handler 處理事件娜谊。RunLoop 可以讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠斤讥。
沒有Runloop的程序
我們通過Xcode新建一個命令行項目纱皆,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!");
之后,就會通過 return 0;
推出程序周偎,這是一種線性的執(zhí)行流程抹剩。
我們再新建一個iOS項目,你看到的main.m
文件是這個樣子的
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我們會進入app的界面蓉坎,然后app就不會退出了澳眷,會一直運行著。
在命令行工程里面的main.m
里面蛉艾,是沒有加Runloop的钳踊,而iOS工程的main.m
里面,其實在UIApplicationMain()
這個方法中勿侯,系統(tǒng)加上了Runloop拓瞪,讓程序可以一直循環(huán)運行下去不退出。
iOS
項目,在main
函數(shù)中系統(tǒng)就會自動幫我們創(chuàng)建runloop
對象:return UIApplicationMain(argc, argv, nil, appDelegateClassName);
.
RunLoop
的基本作用就是:
保證程序的基本運行.程序一啟動就會開一個主線程助琐,主線程一開起來就會跑一個主線程對應(yīng)的RunLoop,RunLoop保證主線程不會被銷毀祭埂,也就保證了程序的持續(xù)運行
處理App中的各種事件 (比如:觸摸事件,定時器事件 等等).
節(jié)省 CPU 資源,提高程序性能: 該做事時做事,沒有事的時候就休息.(程序運行起來時,當什么操作都沒有做的時候,RunLoop就告訴CPU蛆橡,現(xiàn)在沒有事情做舌界,我要去休息,這時CPU就會將其資源釋放出來去做其他的事情泰演,當有事情做的時候RunLoop就會立馬起來去做事情)
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;
}
}
流程:條件成立的時候一直循環(huán):有事情就處理事情,沒有事情就休眠睡覺.Runloop其實就是一個do-while
循環(huán)呻拌,每次循環(huán)一圈,都會判斷一次retVal
睦焕,決定是否結(jié)束循環(huán)藐握,繼續(xù)執(zhí)行循環(huán)外的代碼。
RunLoop對象
iOS
中提供了兩套API
來訪問RunLoop
:
-
Foundation : NSRunLoop
: OC 框架 -
Core Foundation : CFRunLoopRef
: C 語言框架
NSRunLoop
和CFRunLoopRef
都代表Runloop對象垃喊,NSRunLoop
是基于CFRunLoopRef
的一層OC包裝猾普,CFRunLoopRef
是開源的我們下載好源代碼后新建一個項目,把源代碼拖到項目中.
Runloop對象的獲取
-
Foundation
[NSRunloop currentRunLoop];
獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop];
獲得主線程的Runloop對象 -
Core Foundation
CFRunLoopGetCurrent();
獲得當前線程的RunLoop對象
CFRunLoopGetMain();
獲得主線程的Runloop對象
CFRunLoop.c
文件 -> 然后找到CFRunLoopGetCurrent
函數(shù) -> 進入_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ù)線程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
????????????
static CFMutableDictionaryRef __CFRunLoops = NULL; //字典
// 獲取 runloop 對象 參數(shù):傳入一個 字典 和 key (線程)
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是在第一次獲取的時候創(chuàng)建的,并且
RunLoop和 線程 是 一一對應(yīng)的關(guān)系,
RunLoop是存放在一個全局字典中:以線程作為
key,
RunLoop作為
value.
Runloop與線程
為什么聊Runloop一定要搭上線程?我們知道缔御,程序里的每一句代碼抬闷,都會在線程(
主線程/子線程
)里面被執(zhí)行,上面四種獲得Runloop對象的代碼也不例外耕突,一定是跑在線程里面的笤成。之前我們說到,Runloop是為了讓程序不退出眷茁,其實更準確地說炕泳,是為了保持某個線程不結(jié)束,只要還有未結(jié)束的線程上祈,那么整個程序就不會退出培遵,因為線程是程序的運行的調(diào)度的基本單元。線程與Runloop的關(guān)系是
一對一
的登刺,一個新創(chuàng)建的線程籽腕,是沒有Runloop對象的,當我們在該線程里第一次通過上面的API獲得Runloop時纸俭,Runloop對象才會被創(chuàng)建皇耗,并且通過一個全局字典將Runloop對象和該線程存儲綁定在一起,形成一對一關(guān)系揍很。Runloop會在線程結(jié)束時銷毀郎楼,主線程的Runloop已經(jīng)自動獲取過(創(chuàng)建),子線程默認沒有開啟RunLoop(直到你在該線程獲取它)窒悔。RunLoop對象創(chuàng)建后呜袁,會被保存在一個全局的Dictionary里,線程作為
key
简珠,Runloop對象作為value
阶界。
Runloop對象底層結(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對應(yīng)的線程
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;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
RunLoop Mode Mode可以視為事件的管家芜抒,一個Mode管理著各種事件,它的結(jié)構(gòu)如下:
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;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers;//定時器
//???????????? 核心組成 ????????????
CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_t托启,value是CFRunLoopSourceRef
__CFPortSet _portSet;//保存所有需要監(jiān)聽的port,比如_wakeUpPort攘宙,_timerPort都保存在這個數(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 */
};
一個CFRunLoopMode對象有一個name屯耸,若干source0、source1蹭劈、timer疗绣、observer和若干port,可見事件都是由Mode在管理铺韧,而RunLoop管理Mode多矮。
Runloop相關(guān)的5個相關(guān)的類
- CFRunLoopRef——這個就是Runloop對象
-
CFRunLoopModeRef——其內(nèi)部主要包括四個容器,分別用來存放
source0
哈打、source1
塔逃、observer
以及timer
-
CFRunLoopSourceRef——分為
source0
和source1
source0
:包括 觸摸事件處理、[performSelector: onThread: ]
source1
:包括 基于Port的線程間通信料仗、系統(tǒng)事件捕捉 -
CFRunLoopTimerRef——
timer
事件湾盗,包括我們設(shè)置的定時器事件、[performSelector: withObject: afterDelay:]
-
CFRunLoopObserverRef——監(jiān)聽者立轧,Runloop狀態(tài)變更的時格粪,會通知監(jiān)聽者進行函數(shù)回調(diào),UI界面的刷新就是在監(jiān)聽到Runloop狀態(tài)為
BeforeWaiting
時進行的氛改。
對于以上這幾個類相互之間的關(guān)系帐萎,可以通過如下的圖來描繪
從圖中可看出,一個RunLoop對象里面包含了若干個RunLoopMode
胜卤,RunLoop內(nèi)部是通過一個集合容器_modes
來裝這些RunLoopMode
的疆导。
RunLoopMode內(nèi)部核心內(nèi)容是4個數(shù)組容器,分別用來裝source0
瑰艘,source1
是鬼,observer
和timer
,RunLoop對象內(nèi)部有一個_currentMode
紫新,它指向了該RunLoop對象的其中一個RunLoopMode
均蜜,它代表的含義是RunLoop當前所運行的RunLoopMode
,所謂“運行”也就是說芒率,RunLoop當前只會執(zhí)行_currentMode
所指向的RunLoopMode
里面所包括的事件(source0囤耳、source1、observer、timer
).RunLoop對象內(nèi)部還包括一個線程對象_pthread
充择,這就是跟它一一對應(yīng)的那個線程對象德玫。
RunLoop Source
Run Loop Source分為Source、Observer椎麦、Timer三種宰僧,他們統(tǒng)稱為ModeItem。
CFRunLoopSource
根據(jù)官方的描述观挎,CFRunLoopSource是對input sources的抽象琴儿。CFRunLoopSource分source0和source1兩個版本,它的結(jié)構(gòu)如下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //?????? 用于標記Signaled狀態(tài)嘁捷,source0只有在被標記為Signaled狀態(tài)惩坑,才會被處理
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棵介。當一個source0事件準備執(zhí)行的時候,必須要先把它標記為signal狀態(tài).
App自己管理的UIEven,包括觸摸事件處理缓升、[performSelector: onThread: ]
鼓鲁,這個也可以通過代碼來驗證一下。首先看一下觸摸事件仔沿,在ViewController
里面重寫方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"點擊屏幕");
}
#9 0x00007fff2039038a in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
可以看出系統(tǒng)是通過一個CF的函數(shù)__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
來調(diào)用UIKit進行事件處理的
source0是非基于Port的坐桩。只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件封锉。使用時绵跷,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理成福,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop碾局,讓其處理這個事件。
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)指針外包含一個mach port蕴潦,Source1可以監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程通信像啼,接收、分發(fā)系統(tǒng)事件潭苞,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進行管理忽冻,例如CFMessagePort消息)。官方也指出可以自定義Source此疹,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議僧诚,框架已經(jīng)默認定義了兩種實現(xiàn)遮婶,如果有必要開發(fā)人員也可以自定義,詳細情況可以查看官方文檔湖笨。
RunLoop 的狀態(tài):
RunLoop
有以下幾種狀態(tài):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入 休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 以上所有狀態(tài)
};
下面我們寫代碼來驗證一下這些狀態(tài)的切換.首先寫代碼測試一下,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)切換時,都會被
observer
監(jiān)聽到慈省。
我們再創(chuàng)建一個UITextView
,拖動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拖動
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對象/關(guān)聯(lián)對象/多線程/內(nèi)存管理/性能優(yōu)化臀防,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán)边败,請聯(lián)系我刪除清钥,謝謝!