什么是Runloop
· 一般來(lái)講住闯,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù)瓜浸,執(zhí)行完成后線程就會(huì)退出澳淑。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出插佛,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
· Runloop類似于一個(gè)while循環(huán)杠巡,循環(huán)執(zhí)行代碼,保持程序的持續(xù)運(yùn)行雇寇。
· RunLoop 實(shí)際上就是一個(gè)對(duì)象氢拥,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面 Event Loop 的邏輯谢床。線程執(zhí)行了這個(gè)函數(shù)后兄一,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息)识腿,函數(shù)返回出革。
· 在iOS的工程的main.m文件中我們可以看到這樣的代碼:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain
函數(shù)內(nèi)部就啟動(dòng)了一個(gè)Runloop,使App一直運(yùn)行渡讼,這個(gè)默認(rèn)開(kāi)啟的Runloop默認(rèn)和主線程關(guān)聯(lián)起來(lái)骂束。
· 新建一個(gè)工程,在storyboard上加上按鈕成箫,運(yùn)行
結(jié)果如下:
從Xcode左上角看的出來(lái)程序一直在運(yùn)行
當(dāng)把代碼改為:
int main(int argc, char * argv[]) {
@autoreleasepool {
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
return 0;
}
}
main
函數(shù)直接返回0展箱,AppDelegate里面的方法沒(méi)有執(zhí)行,然后程序就就退出了蹬昌。
再把代碼修改如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%@", @"這里會(huì)打印");
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"%@", @"這里不會(huì)打印");
return result;
}
}
運(yùn)行結(jié)果如下:
程序執(zhí)行了UIApplicationMain
后開(kāi)啟了默認(rèn)的Runloop混驰,一直循環(huán)15行,所以16行代碼永遠(yuǎn)沒(méi)有執(zhí)行皂贩。
Runloop可以看作下面的偽代碼:
int main(int argc, char * argv[]) {
BOOL AppIsRunning = YES;
while (AppIsRunning) {
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
return 0;
}
Runloop有什么用處
1栖榨、使程序一直運(yùn)行接受用戶輸入
2、決定程序在何時(shí)應(yīng)該處理哪些Event
3明刷、調(diào)用解耦(對(duì)于編程經(jīng)驗(yàn)為0的完全沒(méi)搞懂這個(gè)意思,解釋為Message Queue)
4婴栽、節(jié)省CPU時(shí)間
<br />
Runloop的機(jī)制
(套用sunnnyxx 在視頻中提供的資料)
Runloop事件隊(duì)列
RunLoop的掛起與喚醒
從偽代碼可以看出
- 制定用于喚醒的
mach_port
端口
- 調(diào)用
mach_msg
- 監(jiān)聽(tīng)喚醒端口,被喚醒前,系統(tǒng)內(nèi)核將這個(gè)線程掛起,停留在
mach_msg_trap
- 由另外一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)端口的
msg
后,trap
狀態(tài)被喚醒,RunLoop繼續(xù)開(kāi)始干活
<br />
Runloop對(duì)象
1.iOS中有2tAPI來(lái)訪問(wèn)和使用RunLoop
-Foundation 框架
NSRunLoop
-Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表著RunLoop對(duì)象
3.NSRunLoop是基于CFRunLoopRef的一層OC包裝, 所以要了解RunLoop內(nèi)部結(jié)構(gòu),需要多研究CFRunLoopRef層面的API (Core Foundation 層面)
a 主線程的runloop自動(dòng)創(chuàng)建,子線程的runloop默認(rèn)不創(chuàng)建(在子線程中調(diào)用NSRunLoop *runloop = [NSRunLoop currentRunLoop];獲取RunLoop對(duì)象的時(shí)候辈末,就會(huì)創(chuàng)建RunLoop)愚争;
b runloop退出的條件:app退出;線程關(guān)閉挤聘;設(shè)置最大時(shí)間到期轰枝;modeItem為空;
c 同一時(shí)間一個(gè)runloop只能在一個(gè)mode组去,切換mode只能退出runloop狸膏,再重進(jìn)指定mode(隔離modeItems使之互不干擾);
d 一個(gè)item可以加到不同mode添怔;一個(gè)mode被標(biāo)記到commonModes里(這樣runloop不用切換mode)湾戳。
<br />Source是RunLoop的數(shù)據(jù)源抽象類(protocol)
RunLoop定義了兩個(gè)Version的Source:
1、Source0:處理App內(nèi)部事件广料、App自己負(fù)責(zé)管理(觸發(fā))砾脑,如UIEvent、CFSocket
2艾杏、Source1:由RunLoop和內(nèi)核管理韧衣,Mach port驅(qū)動(dòng),如CFMachPort购桑、CFMessagePort
如有需要畅铭,可從中選擇一種來(lái)實(shí)現(xiàn)自己的Source
上一條基本不會(huì)發(fā)生
<br />RunLoopTimer的封裝
// 創(chuàng)建但是不會(huì)加入當(dāng)前 Runloop
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 創(chuàng)建但是加入當(dāng)前 Runloop 的 NSDefaultRunLoopMode 并執(zhí)行
+ (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;
<br />CFRunLoopObserver
向外部報(bào)告RunLoop當(dāng)前狀態(tài)的更改,框架中很多機(jī)制都由RunLoopObserver觸發(fā)勃蜘,如CAAnimation
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
};
UIKit通過(guò)RunLoopObserver在RunLoop兩次Sleep
間對(duì)AutoreleasePool進(jìn)行Pop和Push硕噩,將這次Loop中產(chǎn)生的Autorelease
對(duì)象釋放
Runloop的寄生于線程:一個(gè)線程只能有唯一對(duì)應(yīng)的runloop;但這個(gè)根runloop里可以嵌套子runloops缭贡;
自動(dòng)釋放池寄生于Runloop:程序啟動(dòng)后炉擅,主線程注冊(cè)了兩個(gè)Observer監(jiān)聽(tīng)runloop的進(jìn)出與睡覺(jué)。一個(gè)最高優(yōu)先級(jí)OB監(jiān)測(cè)Entry狀態(tài)阳惹;一個(gè)最低優(yōu)先級(jí)OB監(jiān)聽(tīng)BeforeWaiting狀態(tài)和Exit狀態(tài)谍失。
線程(創(chuàng)建)-->runloop將進(jìn)入-->最高優(yōu)先級(jí)OB創(chuàng)建釋放池-->runloop將睡-->最低優(yōu)先級(jí)OB銷毀舊池創(chuàng)建新池-->runloop將退出-->最低優(yōu)先級(jí)OB銷毀新池-->線程(銷毀)
<br />CFRunLoopMode
- RunLoop在同一段時(shí)間只能且必須在一種特定Mode下Run
- 更換Mode時(shí),需要停止當(dāng)前Loop莹汤,然后重啟新Loop
- Mode是iOS App滑動(dòng)順暢的關(guān)鍵
- 可以定制自己的Mode
// 默認(rèn)狀態(tài)快鱼、空閑狀態(tài)
NSDefaultRunLoopMode
// 滑動(dòng)ScrollView時(shí)
UITrackingRunLoopMode
// 私有,App啟動(dòng)時(shí)
UIInitializationRunLoopMode
// Mode集合纲岭,可以理解為 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的集合
NSRunLoopCommonModes
Runloop與GCD任務(wù):
當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí),libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息抹竹,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block荒勇,并在回調(diào)里執(zhí)行這個(gè) block柒莉。Runloop只處理主線程的block,dispatch 到其他線程仍然是由 libDispatch 處理的沽翔。
關(guān)于網(wǎng)絡(luò)請(qǐng)求
iOS 中兢孝,關(guān)于網(wǎng)絡(luò)請(qǐng)求的接口自下至上有如下幾層:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
1.CFSocket 是最底層的接口,只負(fù)責(zé) socket 通信仅偎。
2.CFNetwork 是基于 CFSocket 等接口的上層封裝跨蟹,ASIHttpRequest 工作于這一層。
3.NSURLConnection 是基于 CFNetwork 的更高層的封裝橘沥,提供面向?qū)ο蟮慕涌诖靶珹FNetworking 工作于這一層。
4.NSURLSession 是 iOS7 中新增的接口座咆,表面上是和 NSURLConnection 并列的痢艺,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程)仓洼,AFNetworking2 和 Alamofire 工作于這一層。
下面主要介紹下 NSURLConnection 的工作過(guò)程堤舒。
通常使用 NSURLConnection 時(shí)色建,你會(huì)傳入一個(gè) Delegate,當(dāng)調(diào)用了 [connection start] 后舌缤,這個(gè) Delegate 就會(huì)不停收到事件回調(diào)箕戳。實(shí)際上,start 這個(gè)函數(shù)的內(nèi)部會(huì)會(huì)獲取 CurrentRunLoop国撵,然后在其中的 DefaultMode 添加了4個(gè) Source0 (即需要手動(dòng)觸發(fā)的Source)陵吸。CFMultiplexerSource 是負(fù)責(zé)各種 Delegate 回調(diào)的,CFHTTPCookieStorage 是處理各種 Cookie 的介牙。
當(dāng)開(kāi)始網(wǎng)絡(luò)傳輸時(shí)壮虫,我們可以看到 NSURLConnection 創(chuàng)建了兩個(gè)新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的耻瑟。NSURLConnectionLoader 這個(gè)線程內(nèi)部會(huì)使用 RunLoop 來(lái)接收底層 socket 的事件旨指,并通過(guò)之前添加的 Source0 通知到上層的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通過(guò)一些基于 mach port 的 Source 接收來(lái)自底層 CFSocket 的通知喳整。當(dāng)收到通知后谆构,其會(huì)在合適的時(shí)機(jī)向 CFMultiplexerSource 等 Source0 發(fā)送通知,同時(shí)喚醒 Delegate 線程的 RunLoop 來(lái)讓其處理這些通知框都。CFMultiplexerSource 會(huì)在 Delegate 線程的 RunLoop 對(duì) Delegate 執(zhí)行實(shí)際的回調(diào)搬素。
Runloop實(shí)驗(yàn)
實(shí)驗(yàn)一
- (IBAction)buttonDidClick:(id)sender {
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
輸出結(jié)果
實(shí)驗(yàn)二
把代碼改成如下,輸入結(jié)果一樣
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
如果把[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
屏蔽魏保,會(huì)發(fā)現(xiàn)沒(méi)有打印東西熬尺,因?yàn)?code>timerWithTimeInterval這個(gè)方法只是創(chuàng)建了并沒(méi)有加入Runloop
實(shí)驗(yàn)三 有scrollView的情況下使用Timer
在實(shí)驗(yàn)二的基礎(chǔ)上,在vc中加一個(gè)textView谓罗,run起來(lái)粱哼,模擬器界面如下:
點(diǎn)擊按鈕,然后滾動(dòng)scrollView檩咱,在停止?jié)L動(dòng)揭措,打印結(jié)果
可以看的出來(lái)滾動(dòng)的時(shí)間段,timer并沒(méi)有效果刻蚯,那是因?yàn)闈L動(dòng)的時(shí)候主線程Runloop已經(jīng)切換mode為UITrackingRunLoopMode绊含,Runloop只能指定一個(gè)mode,而timer只是加在NSDefaultRunLoopMode炊汹,所以發(fā)生滾動(dòng)的時(shí)候躬充,Runloop并不會(huì)響應(yīng)timer;當(dāng)松開(kāi)手的時(shí)候Runloop切換回NSDefaultRunLoopMode,timer就重新起作用充甚。
當(dāng)我們把timer的mode修改為NSRunLoopCommonModes以政,此時(shí)滾動(dòng)scrollView的同時(shí)也能響應(yīng)timer:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
<br />
實(shí)驗(yàn)四 CFRunLoopSourseRef的實(shí)驗(yàn)
我們?cè)赽utton的響應(yīng)注釋,然后打個(gè)斷點(diǎn)津坑,run后點(diǎn)擊button會(huì)發(fā)現(xiàn)如下類似這種UIEvent是屬于Souce0
<br />
實(shí)驗(yàn)五 CFRunLoopObserverRef的實(shí)驗(yàn)
- (void)createObserver {
// 創(chuàng)建監(jiān)聽(tīng)者對(duì)象
// rl: RunLoop
// observer: 監(jiān)聽(tīng)者對(duì)象
// mode: Runloop所在的mode
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"observer--------%lu", activity);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
根據(jù)CFRunLoopActivity枚舉妙蔗,我們可以看出Runloop的狀態(tài)變化
1:即將進(jìn)入Runloop-> 2:即將處理NSTimer-> 4:即將處理Souce0 -> 32:即將進(jìn)入休眠 -> 64:從休眠仲喚醒
/* 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
};
實(shí)驗(yàn)更新
代碼:
- (IBAction)buttonDidClick:(id)sender {
NSLog(@"%s", __func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_myThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%@", @"+++++");
});
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(myThreadTest) onThread:_myThread withObject:nil waitUntilDone:NO];
}
- (void)myThreadTest {
NSLog(@"%s", __FUNCTION__);
}
點(diǎn)擊按鈕后打印出+++++
,然后點(diǎn)擊屏幕空白處- (void)myThreadTest
并沒(méi)有觸發(fā)疆瑰。
這是因?yàn)開(kāi)myThread中的Runloop只run了一次就退出了,從而子線程沒(méi)有監(jiān)聽(tīng)到屏幕的點(diǎn)擊事件昙啄。只run一次的原因首先看這張圖
代碼中只是讓子線程的運(yùn)行循環(huán)run了一次穆役,并沒(méi)有加入實(shí)質(zhì)的source、port梳凛、Observer或者timer耿币,Runloop直接跑一次直接退出了,導(dǎo)致點(diǎn)擊時(shí)間沒(méi)有Runloop來(lái)響應(yīng)韧拒。
要響應(yīng)- (void)myThreadTest
必須要子線程的Runloop保持駐留狀態(tài)淹接,給Runloop添加一個(gè)port讓其保持駐留,此時(shí)我們點(diǎn)擊button之后再點(diǎn)擊屏幕空白處可以看到打印出來(lái)的日志叛溢,可以看的出來(lái)點(diǎn)擊事件已經(jīng)起效了塑悼,并且+++++
也沒(méi)有打印出來(lái),那是因?yàn)樽泳€程的運(yùn)行循環(huán)已經(jīng)駐留楷掉,循環(huán)外面的代碼就執(zhí)行不到厢蒜。
- (IBAction)buttonDidClick:(id)sender {
NSLog(@"%s", __func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_myThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"++++");
});
}
Runloop使用
AFNetworking中RunLoop的創(chuàng)建
+ (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有話UITableView
因?yàn)閁ITableView滾動(dòng)的時(shí)候主線程Runloop的mode切換為UITrackingRunLoopMode
,當(dāng)停止?jié)L動(dòng)的時(shí)候會(huì)切回NSDefaultRunLoopMode
烹植,從而可以減輕UITableView的卡頓。
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
參考資料:
http://blog.ibireme.com/2015/05/18/runloop/
http://www.reibang.com/p/37ab0397fec7
https://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690