基本概念
進(jìn)程
進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,而且每個(gè)進(jìn)程之間是獨(dú)立的,它們都運(yùn)行在其專用且受保護(hù)的內(nèi)存空間內(nèi),比如同時(shí)打開迅雷、Xcode,系統(tǒng)就會(huì)分別啟動(dòng)兩個(gè)進(jìn)程。
線程
一個(gè)人進(jìn)程如果想要執(zhí)行任務(wù),必須得有至少一條線程,進(jìn)程的所有任務(wù)都會(huì)在線程中執(zhí)行,比如使用網(wǎng)易云音樂(lè)播放音樂(lè),使用迅雷下載電影,都需要在線程中執(zhí)行。
主線程
iOS 程序運(yùn)行后,系統(tǒng)會(huì)默認(rèn)開啟一條線程,稱為“主線程”或者“UI 線程”,主線程是用來(lái)顯示/刷新 UI 界面,處理 UI 事件的。
簡(jiǎn)介
運(yùn)行循環(huán)、跑圈
總結(jié)下來(lái),RunLoop 的作用主要體現(xiàn)在三方面:
保持程序的持續(xù)運(yùn)行
處理App中的各種事件(比如觸摸事件、定時(shí)器事件、Selector事件)
節(jié)省CPU資源,提高程序性能:該做事的時(shí)候做事,該休息的時(shí)候休息
就是說(shuō),如果沒(méi)有 RunLoop 程序一運(yùn)行就結(jié)束了,你根本不可能看到持續(xù)運(yùn)行的 app。
iOS中有2套API訪問(wèn)和使用RunLoop
Foundation:NSRunLoop
Core Foundation: CFRunLoopRef
NSRunLoop是基于CFRunLoopRef的一層OC包裝,因此我們需要研究CFRunLoopRef層面的API(Core Foundation層面)
關(guān)于 RunLoop 的源碼請(qǐng)看這里
RunLoop與線程
源碼中,關(guān)于創(chuàng)建線程的核心代碼如下:
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { // 如果沒(méi)有線程,則要?jiǎng)?chuàng)建線程
__CFUnlock(&loopsLock);
// 創(chuàng)建一個(gè)可變字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 將主線程放進(jìn)去,創(chuàng)建 RunLoop(也就是說(shuō),創(chuàng)建哪個(gè)線程的 RunLoop 需要將線程作為參數(shù)傳入)
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 將主線程的 RunLoop 和主線程以 key/value 的形式保存。
// 因此由此可以看出,一條線程和一個(gè) RunLoop 是一一對(duì)應(yīng)的
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 當(dāng)你輸入 cunrrentRunLoop 時(shí),會(huì)通過(guò)當(dāng)前線程這個(gè) key,在字典中尋找對(duì)應(yīng)的 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果沒(méi)有在字典中找到
if (!loop) {
// 則重新創(chuàng)建一個(gè) RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
// 然后將 RunLoop 和線程以 key/value 的形式保存
// 再一次驗(yàn)證了 RunLoop 和 key 是一一對(duì)應(yīng)的
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);
}
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;
}
程序啟動(dòng)時(shí),系統(tǒng)會(huì)自動(dòng)創(chuàng)建主線程的 RunLoop
每一條線程都有唯一的一個(gè)與之對(duì)應(yīng)的RunLoop對(duì)象
主線程的RunLoop已經(jīng)自動(dòng)創(chuàng)建好了,子線程的RunLoop需要手動(dòng)創(chuàng)建
RunLoop在第一次獲取時(shí)創(chuàng)建,在線程結(jié)束時(shí)銷毀
代碼:
// 獲取當(dāng)前的線程的RunLoop對(duì)象,注意RunLoop是懶加載,currentRunLoop時(shí)會(huì)自動(dòng)創(chuàng)建對(duì)象
[NSRunLoop currentRunLoop];
// 獲取主線程的RunLoop對(duì)象
[NSRunLoop mainRunLoop];
// 如果是 CF 層面
CFRunLoopGetCurrent();
CFRunLoopGetMain();
RunLoop相關(guān)類
通過(guò):
NSLog(@"%@", [NSRunLoop mainRunLoop]);
可以對(duì) RunLoop 內(nèi)部一覽無(wú)余
Core Foundation中關(guān)于RunLoop的5個(gè)類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
RunLoop 想要跑起來(lái),必須有 Mode 對(duì)象支持,而 Mode 里面必須有
(NSSet *)Source、(NSArray *)Timer,源和定時(shí)器。
至于另外一個(gè)類(NSArray *)observer是用于監(jiān)聽(tīng) RunLoop 的狀態(tài),因此不會(huì)激活RunLoop。
CFRunLoopModeRef
CFRunLoopModeRef 代表 RunLoop 的運(yùn)行模式
每個(gè) RunLoop 都包含若干個(gè) Mode ,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer,每次
RunLoop 啟動(dòng)時(shí),只能指定其中一個(gè) Mode,這個(gè) Mode 被稱作CurrentMode,如果需要切換 Mode,只能退出
Loop,再重新指定一個(gè) Mode 進(jìn)入,這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響(可以通過(guò)切換
Mode,完成不同的 timer/source/observer)。
[NSRunLoop currentRunLoop].currentMode; // 獲取當(dāng)前運(yùn)行模式
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode:
NSDefaultRunLoopMode:App 的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行(默認(rèn)情況下運(yùn)行)
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響(操作 UI 界面的情況下運(yùn)行)
UIInitializationRunLoopMode:在剛啟動(dòng) App 時(shí)進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用
GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到(繪圖服務(wù))
NSRunLoopCommonModes:這是一個(gè)占位用的 Mode,不是一種真正的 Mode (RunLoop無(wú)法啟動(dòng)該模式,設(shè)置這種模式下,默認(rèn)和操作 UI 界面時(shí)線程都可以運(yùn)行,但無(wú)法改變 RunLoop 同時(shí)只能在一種模式下運(yùn)行的本質(zhì))
下面主要區(qū)別 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 以及 NSRunLoopCommonModes。請(qǐng)看以下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 在默認(rèn)模式下添加的 timer 當(dāng)我們拖拽 textView 的時(shí)候,不會(huì)運(yùn)行 run 方法
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 在 UI 跟蹤模式下添加 timer 當(dāng)我們拖拽 textView 的時(shí)候,run 方法才會(huì)運(yùn)行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// timer 可以運(yùn)行在兩種模式下,相當(dāng)于上面兩句代碼寫在一起
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)run
{
NSLog(@"--------run");
}
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[UITrackingRunLoopMode]];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[NSRunLoopCommonModes]];
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于事件的觸發(fā)器
CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop的Mode 影響
創(chuàng)建 Timer 有兩種方式,下面的這種方式必須手動(dòng)添加到 RunLoop 中去才會(huì)被調(diào)用
// 這種方式創(chuàng)建的timer 必須手動(dòng)添加到RunLoop中去才會(huì)被調(diào)用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];
// 同時(shí)讓RunLoop跑起來(lái)
[[NSRunLoop currentRunLoop] run];
而通過(guò)scheduledTimer創(chuàng)建 Timer 一開始就會(huì)自動(dòng)被添加到當(dāng)前線程并且以
NSDefaultRunLoopMode模式運(yùn)行起來(lái),代碼如下:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
/*
注意:調(diào)用了 scheduledTimer 返回的定時(shí)器,已經(jīng)自動(dòng)被添加到當(dāng)前
runLoop 中,而且是 NSDefaultRunLoopMode ,想讓上述方法起作用,
必須先讓添加了上述 timer的RunLoop 對(duì)象 run 起來(lái),通過(guò)
scheduledTimerWithTimeInterval 創(chuàng)建的 timer 可以通過(guò)以下方法修改 mode
*/
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
注意: GCD的定時(shí)器不受RunLoop的Mode影響
CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(run)];
/*
注意:CADisplayLink ,也是在 Runloop 下運(yùn)行的,
有一個(gè)方法可以將CADisplayLink 對(duì)象添加到一個(gè) Runloop 對(duì)象中去
*/
[display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
CFRunLoopSourceRef
CFRunLoopSourceRef 其實(shí)是事件源(輸入源)
按照官方文檔,Source的分類
Port-Based Sources:基于端口的:跟其他線程進(jìn)行交互的,Mac內(nèi)核發(fā)過(guò)來(lái)一些消息
Custom Input Sources:自定義輸入源
Cocoa Perform Selector Sources(self performSelector:...)
按照函數(shù)調(diào)用棧,Source的分類
Source0:非基于Port的(觸摸事件、按鈕點(diǎn)擊事件)
Source1:基于Port的,通過(guò)內(nèi)核和其他線程通信,接收分發(fā)系統(tǒng)事件
(觸摸硬件,通過(guò) Source1 接收和分發(fā)系統(tǒng)事件到 Source0 處理)
為了搞清楚,Source 是如何通過(guò)函數(shù)調(diào)用棧來(lái)傳遞事件的,我們做如下實(shí)驗(yàn):
我們可以看到,從程序啟動(dòng) start 開始,函數(shù)調(diào)用棧在監(jiān)聽(tīng)到事件點(diǎn)擊后,會(huì)一路往下,一直到-buttonClick:方法,中間會(huì)經(jīng)過(guò)CFRunLoopSource0,這說(shuō)明我們的按鈕點(diǎn)擊事件是屬于 Source0 的。
而 Source1 是基于 Port 的,就是說(shuō),Source1 是和硬件交互的,觸摸首先在屏幕上被包裝成一個(gè) event 事件,再通過(guò) Source1 進(jìn)行分發(fā)到 Source0,最后通過(guò) Source0 進(jìn)行處理。
CFRunLoopObserverRef
CFRunLoopObserverRef 是觀察者,能夠監(jiān)聽(tīng) RunLoop 的狀態(tài)改變,主要監(jiān)聽(tīng)以下幾個(gè)時(shí)間節(jié)點(diǎn):
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
{
kCFRunLoopEntry = (1UL << 0), // 1 即將進(jìn)入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 128 即將退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 監(jiān)聽(tīng)所有事件
};
// 1.創(chuàng)建觀察者 監(jiān)聽(tīng) RunLoop
// 參1: 有個(gè)默認(rèn)值 CFAllocatorRef :CFAllocatorGetDefault()
// 參2: CFOptionFlags activities 監(jiān)聽(tīng)RunLoop的活動(dòng) 枚舉 見(jiàn)上面
// 參3: 重復(fù)監(jiān)聽(tīng) Boolean repeats YES
// 參4: CFIndex order 傳0
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 該方法可以在添加timer之前做一些事情,? 在添加source之前做一些事情
NSLog(@"%zd", activity);
});
// 2.添加觀察者,監(jiān)聽(tīng)當(dāng)前的RunLoop對(duì)象
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// CF層面的東西 凡是帶有create、copy、retain等字眼的函數(shù)在CF中要進(jìn)行內(nèi)存管理
CFRelease(observer);
通過(guò)打印可以觀察的 RunLoop 的狀態(tài)
補(bǔ)充:在進(jìn)入第一個(gè)階段前,會(huì)先判斷當(dāng)前 RunLoop 空不空, 如果是空的 直接來(lái)到10階段!
RunLoop的應(yīng)用
NSTimer
需求 讓定時(shí)器 在其他線程開啟
NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
// 這種方式創(chuàng)建的timer 必須手動(dòng)添加到Runloop中去才會(huì)被調(diào)用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 同時(shí)讓RunLoop跑起來(lái)
[[NSRunLoop currentRunLoop] run];
}];
[[[NSOperationQueue alloc] init] addOperation:block];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
ImageView:顯示performSelector
需求
有時(shí)候,用戶拖拽scrollView的時(shí)候,mode:UITrackingRunLoopMode,顯示圖片,如果圖片很大,會(huì)渲染比較耗時(shí),造成不好的體驗(yàn),因此,設(shè)置當(dāng)用戶停止拖拽的時(shí)候再顯示圖片,進(jìn)行延遲操作
方法1:設(shè)置scrollView的delegate? 當(dāng)停止拖拽的時(shí)候做一些事情
方法2:使用performSelector 設(shè)置模式為default模式 ,則顯示圖片這段代碼只能在RunLoop切換模式之后執(zhí)行
// 加載比較大的圖片時(shí),
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// inModes 傳入一個(gè) mode 數(shù)組,這句話的意思是
// 只有在 NSDefaultRunLoopMode 模式下才會(huì)執(zhí)行 seletor 的方法顯示圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
}
效果為:當(dāng)用戶點(diǎn)擊之后,下載圖片,但是圖片太大,不能及時(shí)下載。這時(shí)用戶可能會(huì)做些其他 UI 操作,比如拖拽,但是如果用戶正在拖拽瀏覽其他的東西時(shí),圖片下載完畢了,此時(shí)如果要渲染顯示,會(huì)造成不好的用戶體驗(yàn),所以當(dāng)用戶拖拽完畢后,顯示圖片。
這是因?yàn)椋脩敉献В幱?UITrackingRunLoopMode 模式下,所以圖片不會(huì)顯示。
常駐線程
需求:
搞一個(gè)線程一直存在,一直在后臺(tái)做一些操作 比如監(jiān)聽(tīng)某個(gè)狀態(tài), 比如監(jiān)聽(tīng)是否聯(lián)網(wǎng)。
- (void)viewDidLoad {
[super viewDidLoad];
// 需求:搞一個(gè)線程一直不死,一直在后臺(tái)做一些操作 比如監(jiān)聽(tīng)某個(gè)狀態(tài), 比如監(jiān)聽(tīng)是否聯(lián)網(wǎng)。
// 需要在線程中開啟一個(gè)RunLoop 一個(gè)線程對(duì)應(yīng)一個(gè)RunLoop 所以獲得當(dāng)前RunLoop就會(huì)自己創(chuàng)建RunLoop
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run2) object:nil];
self.thread = thread;
[thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2
{
NSLog(@"----------");
/*
* 創(chuàng)建RunLoop,如果RunLoop內(nèi)部沒(méi)有添加任何Source Timer
* 會(huì)直接退出循環(huán),因此需要自己添加一些source才能保持RunLoop運(yùn)轉(zhuǎn)
*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] run];
NSLog(@"-----------22222222");
}
從 RunLoop 的源碼看來(lái),如果一個(gè) RunLoop 中沒(méi)有添加任何的 Source Timer,會(huì)直接退出循環(huán)。
自動(dòng)釋放池
RunLoop循環(huán)時(shí),在進(jìn)入睡眠之前會(huì)清掉自動(dòng)釋放池,并且創(chuàng)建一個(gè)新的釋放池,用于內(nèi)部變量的銷毀。
在子線程開RunLoop的時(shí)候一定要自己寫一個(gè)@autoreleasepool,一個(gè)RunLoop對(duì)應(yīng)一條線程,自動(dòng)釋放吃是針對(duì)當(dāng)前線程里面的對(duì)象。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(excute) object:nil];
self.thread = thread;
[thread start];
}
- (void)excute
{
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(text) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
這樣保證了內(nèi)存安全。
文本代碼:RunLoop