前言
iOS開發(fā)中,RunLoop就是個神秘的領(lǐng)域洒试,很多2~3年的開發(fā)者都不能準確的描述它的具體含義倍奢,甚至可能從來都沒有接觸過該方面的技術(shù),或者項目中隱約用到過(定時器)垒棋,但是不知道是其的作用娱挨,問題一解決就不在深究了,不過也有情可原捕犬,畢竟大部分開發(fā)者還處于基本功能的構(gòu)建,很少涉及性能的優(yōu)化酵镜。
RunLoop之所以神秘碉碉,個人認為,系統(tǒng)能夠利用RunLoop 實現(xiàn)自動釋放池淮韭、延遲回調(diào)垢粮、觸摸事件、屏幕刷新等功能靠粪。實現(xiàn)這種技術(shù)的關(guān)鍵點在于:如何管理事件/消息蜡吧,如何讓線程在沒有處理消息時休眠以避免資源占用毫蚓、在有消息到來時立刻被喚醒。
RunLoop的基本概念
簡介
RunLoop從字面上來看:運行循環(huán)昔善,或者是跑圈元潘。
#基本作用:
1、保持程序的持續(xù)運行(比如主運行循環(huán))
2君仆、處理App中的各種事件(比如觸摸事件翩概、定時器事件、Selector事件)
3返咱、節(jié)省CPU資源钥庇,提高性能。
通常來講咖摹,一個線程一次只能執(zhí)行一個任務(wù)评姨,當線程中的任務(wù)執(zhí)行完成后,線程就會自動退出萤晴。如果我們需要一個長運行機制吐句,讓線程能夠隨時處理事件并且保證不退出,通常我們的做法硫眯,邏輯是這樣的:
function loop {
initialize();
BOOL running = YES;
do{
// 執(zhí)行各種任務(wù)蕴侧,處理各種事件
}while (running);
return 0;
}
上述函數(shù)中,通過一個循環(huán)來執(zhí)行任務(wù)两入,根據(jù)事件的回調(diào)和觀察者的屬性來改更改running的值净宵,有效的使得函數(shù)中的任務(wù)長久的運行。簡單的說裹纳,這就是程序的運行模式择葡。
眾所周知,iOS應(yīng)用啟動后剃氧,不會正常的自動退出敏储。這就是因為iOS系統(tǒng)擁有的RunLoop機制的作用,當應(yīng)用啟動后朋鞍,主線程的RunLoop默認開啟已添,從而應(yīng)用進入一個死循環(huán),阻止了程序在運行完畢之后退出滥酥。
int main(int argc, char * argv[]) {
@autoreleasepool {
//這個函數(shù)永遠不會有返回值
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}
1.在UIApplicationMain函數(shù)內(nèi)部就啟動了一個RunLoop
2.UIApplicationMain函數(shù)一直沒有返回更舞,保持了程序的持續(xù)運行
3.這個默認啟動的RunLoop是主線程關(guān)聯(lián)的
4.一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認已經(jīng)啟動
5.子線程的RunLoop得手動啟動(調(diào)用運行方法)
6.RunLoop只能選擇一個模式啟動坎吻,如果當前模式中沒有任Source缆蝉,Timer,Observer,那么就直接退出RunLoop
RunLoop在循環(huán)過程中監(jiān)聽著port事件和timer事件刊头,當前線程有任務(wù)時黍瞧,喚醒當當線程去執(zhí)行任務(wù),任務(wù)執(zhí)行完成以后原杂,使當前線程進入休眠狀態(tài)印颤。
RunLoop與線程
RunLoop,正如其名污尉,loop表示某種循環(huán)膀哲,和run放在一起就表示一直在運行著的循環(huán)。實際上被碗,RunLoop和線程是緊密相連的某宪,可以這樣說run loop是為了線程而生,沒有線程锐朴,它就沒有存在的必要兴喂。RunLoop是線程的基礎(chǔ)架構(gòu)部分,Cocoa和CoreFundation框架都提供了RunLoop的相關(guān)接口焚志,方便配置和管理線程的RunLoop衣迷。每個線程,包括程序的主線程(main thread)都有與之相應(yīng)的run loop對象酱酬,主線程的RunLoop對象是默認開啟的壶谒,子線程的run loop對象需要程序員手動開啟。
從源碼中可以看出膳沽,線程和 RunLoop 之間是一一對應(yīng)的汗菜,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop挑社,如果你不主動獲取陨界,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時痛阻,RunLoop 的銷毀是發(fā)生在線程結(jié)束時菌瘪。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。
RunLoop與autorelease pool
從程序啟動到加載完成是一個完整的運行循環(huán)阱当,然后會停下來俏扩,等待用戶交互,用戶的每一次交互都會啟動一次運行循環(huán)弊添,來處理用戶所有的點擊事件动猬,觸摸事件。我們都知道: 所有autorelease的對象表箭,在出了作用域之后,會被自動添加到最近創(chuàng)建的自動釋放池中。但是如果每次都放進應(yīng)用程序的main.m中的autoreleasepool中免钻,遲早有被撐滿的一刻彼水。這個過程中必須有一個釋放的動作。在一次完整的運行循環(huán)結(jié)束之前慨蛙,會被銷毀撞反。
Autorelease對象出了作用域之后惹挟,會被添加到最近一次創(chuàng)建的自動釋放池中,并會在當前的runloop迭代結(jié)束時釋放盯桦。
在使用手動的內(nèi)存管理方式的項目中,會經(jīng)常用到很多自動釋放的對象渤刃,如果這些對象不能夠被即時釋放掉拥峦,會造成內(nèi)存占用量急劇增大。Run loop就為我們做了這樣的工作卖子,每當一個運行循環(huán)結(jié)束的時候略号,它都會釋放一次autorelease pool,同時pool中的所有自動釋放類型變量都會被釋放掉洋闽。
RunLoop常用的類
首先要知道iOS里面有兩套API可以訪問和使用RunLoop:
1玄柠、Foundation ---> NSRunLoop
2、Core Foundation ---> CFRunLoopRef
CoreFoundation 里面關(guān)于 RunLoop 有5個類:
1诫舅、CFRunLoopRef
2羽利、CFRunLoopModeRef
3、CFRunLoopSourceRef
4刊懈、CFRunLoopTimerRef
5这弧、CFRunLoopObserverRef
上面兩套都可以使用,但是要知道CFRunLoopRef是用c語言寫的俏讹,是開源的当宴,相比于NSRunLoop更加底層,而NSRunLoop其實是對CFRunLoopRef的一個簡單的封裝泽疆。便于使用而已户矢。這樣說來,顯然CFRunLoopRef的性能要高一點殉疼。
另外梯浪,我們不能在一個線程中去操作另外一個線程的run loop對象,那很可能會造成意想不到的后果瓢娜。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的挂洛,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對應(yīng)的CFRunLoopRef類眠砾,來達到線程安全的目的虏劲。
CFRunLoopSourceRef
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。
事件源(input source)
按照官方文檔的分類
Port-Based Sources (基于端口,跟其他線程交互,通過內(nèi)核發(fā)布的消息)
Custom Input Sources (自定義)
Cocoa Perform Selector Sources (performSelector…方法)
按照函數(shù)調(diào)用棧的分類
Source0:非基于Port的
Source1:基于Port的
? Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件柒巫。使用時励堡,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理堡掏,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop应结,讓其處理這個事件。
? Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針)泉唁,被用于通過內(nèi)核和其他線程相互發(fā)送消息鹅龄。這種 Source 能主動喚醒 RunLoop 的線程。
定時源(timer source)
定時源在預(yù)設(shè)的時間點同步方式傳遞消息亭畜,這些消息都會發(fā)生在特定時間或者重復(fù)的時間間隔扮休。定時源則直接傳遞消息給處理例程,不會立即退出run loop贱案。
需要注意的是肛炮,盡管定時器可以產(chǎn)生基于時間的通知,但它并不是實時機制宝踪。和輸入源一樣侨糟,定時器也和你的run loop的特定模式相關(guān)。如果定時器所在的模式當前未被run loop監(jiān)視瘩燥,那么定時器將不會開始直到run loop運行在相應(yīng)的模式下秕重。類似的,如果定時器在run loop處理某一事件期間開始厉膀,定時器會一直等待直到下次run loop開始相應(yīng)的處理程序溶耘。如果run loop不再運行,那定時器也將永遠不啟動服鹅。
創(chuàng)建定時器源有兩種方法凳兵,
方法一:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
target:self
selector:@selector(backgroundThreadFire:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
方法二:
[NSTimer scheduledTimerWithTimeInterval:10
target:self
selector:@selector(backgroundThreadFire:)
userInfo:nil
repeats:YES];
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于時間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的企软,可以混用庐扫。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當其加入到 RunLoop 時仗哨,RunLoop會注冊對應(yīng)的時間點形庭,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)厌漂。
- NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響
- GCD的定時器不受RunLoop的Mode影響
開發(fā)中萨醒,我們需要一個定時操作,來循環(huán)的執(zhí)行任務(wù)苇倡,如果這個任務(wù)是不耗時的富纸,我們可以選擇NSTimer和GCD囤踩,如果任務(wù)是耗時的,我們通常會在子線程創(chuàng)建一個定時器晓褪,來執(zhí)行耗時任務(wù)高职,當選擇NSTimer時計時,定時器里的任務(wù)不會執(zhí)行辞州,原因是當前線程對相應(yīng)的RunLoop并沒有開啟,創(chuàng)建線程的函數(shù)執(zhí)行完成之后寥粹,這個線程就被銷毀了(可以用繼承CustomThread : NSThread变过,重寫dealloc方法來驗證),此時涝涤,你可能想用一個全局的對象來保存線程媚狰,任然不能解決問題,全局的對象描述指向線程的那個引用地址阔拳,這和線程的實體沒有關(guān)系崭孤,并不能代替一個實在的線程。
解決方案:開啟當前線程的RunLoop糊肠,當不需要任務(wù)的時候關(guān)閉
//創(chuàng)建一個線程
CustomThread *thread = [[CustomThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
while (!_isfinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(@"線程開始了");
}];
//開啟線程
[thread start];
- (void)timerAction:(id) objc
{
while (_isfinished) {
[NSThread exit];//不需要時辨宠,關(guān)閉當前線程
}
NSLog(@"定時器開啟了");
}
GCD執(zhí)行,不存在上述問題货裹,因為嗤形,系統(tǒng)內(nèi)部默認給我們開啟當前線程的RunLoop,性能更加穩(wěn)定弧圆,還不存在內(nèi)存泄露赋兵。
/*timer需要全局變量*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_currentTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_currentTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_currentTimer, ^{
NSLog(@"-----開始前線程:%@----", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
// 在主線程中實現(xiàn)需要的功能
NSLog(@"-----開始后線程:%@----", [NSThread currentThread]);
});
});
dispatch_resume(_currentTimer);
/*
// 掛起定時器(dispatch_suspend 之后的 Timer,是不能被釋放的搔预!會引起崩潰)
dispatch_suspend(_timer);
// 關(guān)閉定時器
dispatch_source_cancel(_timer);
*/
選擇 GCD 還是 NSTimer霹期? 參考文章
CFRunLoopObserverRef
CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)拯田,當 RunLoop 的狀態(tài)發(fā)生變化時历造,觀察者就能通過回調(diào)接受到這個變化。
應(yīng)用
//添加runloop監(jiān)聽者
- (void)addRunloopObserver{
//獲取 當前的Runloop ref - 指針
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
//定義一個RunloopObserver
CFRunLoopObserverRef defaultModeObserver;
//上下文
/*
typedef struct {
CFIndex version; //版本號 long
void * info;//萬能指針勿锅,這里我們要填寫對象(self或者傳進來的對象)
const void *(*retain)(const void *info); //填寫&CFRetain
void (*release)(const void *info); //填寫&CGFRelease
CFStringRef (*copyDescription)(const void *info); //NULL
} CFRunLoopObserverContext;
*/
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
/*
1 NULL空指針 nil空對象 這里填寫NULL
2 模式
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)改變};
3 是否重復(fù) - YES
4 nil 或者 NSIntegerMax - 999
5 回調(diào)
6 上下文
*/
//創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting, YES,
NSIntegerMax - 999,
&Callback,
&context);
//添加當前runloop的觀察著
if(defaultModeObserver){
CFRunLoopAddObserver(currentRunLoop, defaultModeObserver, kCFRunLoopDefaultMode);
//釋放
CFRelease(defaultModeObserver);
}
}
//這里處理耗時操作了
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//通過info橋接為需要的對象
}
CFRunLoopModeRef
系統(tǒng)默認注冊了5個Mode:(前兩個跟最后一個常用)
kCFRunLoopDefaultMode:App的默認Mode帕膜,通常主線程是在這個
Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸
摸滑動溢十,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個
Mode垮刹,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
kCFRunLoopCommonModes: 這是一個占位用的Mode张弛,不是一種真正的Mode
其中 CFRunLoopModeRef 類并沒有對外暴露荒典,只是通過 CFRunLoopRef 的接口進行了封裝酪劫。他們的關(guān)系如下:
一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer寺董。每次調(diào)用 RunLoop 的主函數(shù)時覆糟,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode遮咖。如果需要切換 Mode滩字,只能退出 Loop,再重新指定一個 Mode 進入御吞。這樣做主要是為了分隔開不同組的 Source/Timer/Observer麦箍,讓其互不影響。
RunLoop的事件隊列
從這個事件隊列中可以看出:
①如果是事件到達陶珠,消息會被傳遞給相應(yīng)的處理程序來處理挟裂, runloop處理完當次事件后,run loop會退出揍诽,而不管之前預(yù)定的時間到了沒有诀蓉。你可以重新啟動run loop來等待下一事件。
②如果線程中有需要處理的源暑脆,但是響應(yīng)的事件沒有到來的時候渠啤,線程就會休眠等待相應(yīng)事件的發(fā)生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作饵筑,而沒工作的時候處于休眠狀態(tài)埃篓。
RunLoop 的實際應(yīng)用舉例
AF2.x,創(chuàng)建了一條常駐線程專門處理所有請求的回調(diào)事件根资。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source架专,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了。