什么是Runloop
· 一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
· Runloop類似于一個while循環,循環執行代碼,保持程序的持續運行。
· RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數后,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。
· 在iOS的工程的main.m文件中我們可以看到這樣的代碼:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain
函數內部就啟動了一個Runloop,使App一直運行,這個默認開啟的Runloop默認和主線程關聯起來。
· 新建一個工程,在storyboard上加上按鈕,運行
結果如下:
從Xcode左上角看的出來程序一直在運行
當把代碼改為:
int main(int argc, char * argv[]) {
@autoreleasepool {
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
return 0;
}
}
main
函數直接返回0,AppDelegate里面的方法沒有執行,然后程序就就退出了。
再把代碼修改如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%@", @"這里會打印");
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"%@", @"這里不會打印");
return result;
}
}
運行結果如下:
程序執行了UIApplicationMain
后開啟了默認的Runloop,一直循環15行,所以16行代碼永遠沒有執行。
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、使程序一直運行接受用戶輸入
2、決定程序在何時應該處理哪些Event
3、調用解耦(對于編程經驗為0的完全沒搞懂這個意思,解釋為Message Queue)
4、節省CPU時間
<br />
Runloop的機制
(套用sunnnyxx 在視頻中提供的資料)
Runloop事件隊列
RunLoop的掛起與喚醒
從偽代碼可以看出
- 制定用于喚醒的
mach_port
端口
- 調用
mach_msg
- 監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在
mach_msg_trap
- 由另外一個線程(或另一個進程中的某個線程)向內核發送這個端口的
msg
后,trap
狀態被喚醒,RunLoop繼續開始干活
<br />
Runloop對象
1.iOS中有2tAPI來訪問和使用RunLoop
-Foundation 框架
NSRunLoop
-Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表著RunLoop對象
3.NSRunLoop是基于CFRunLoopRef的一層OC包裝, 所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API (Core Foundation 層面)
a 主線程的runloop自動創建,子線程的runloop默認不創建(在子線程中調用NSRunLoop *runloop = [NSRunLoop currentRunLoop];獲取RunLoop對象的時候,就會創建RunLoop);
b runloop退出的條件:app退出;線程關閉;設置最大時間到期;modeItem為空;
c 同一時間一個runloop只能在一個mode,切換mode只能退出runloop,再重進指定mode(隔離modeItems使之互不干擾);
d 一個item可以加到不同mode;一個mode被標記到commonModes里(這樣runloop不用切換mode)。
<br />Source是RunLoop的數據源抽象類(protocol)
RunLoop定義了兩個Version的Source:
1、Source0:處理App內部事件、App自己負責管理(觸發),如UIEvent、CFSocket
2、Source1:由RunLoop和內核管理,Mach port驅動,如CFMachPort、CFMessagePort
如有需要,可從中選擇一種來實現自己的Source
上一條基本不會發生
<br />RunLoopTimer的封裝
// 創建但是不會加入當前 Runloop
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 創建但是加入當前 Runloop 的 NSDefaultRunLoopMode 并執行
+ (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
向外部報告RunLoop當前狀態的更改,框架中很多機制都由RunLoopObserver觸發,如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通過RunLoopObserver在RunLoop兩次Sleep
間對AutoreleasePool進行Pop和Push,將這次Loop中產生的Autorelease
對象釋放
Runloop的寄生于線程:一個線程只能有唯一對應的runloop;但這個根runloop里可以嵌套子runloops;
自動釋放池寄生于Runloop:程序啟動后,主線程注冊了兩個Observer監聽runloop的進出與睡覺。一個最高優先級OB監測Entry狀態;一個最低優先級OB監聽BeforeWaiting狀態和Exit狀態。
線程(創建)-->runloop將進入-->最高優先級OB創建釋放池-->runloop將睡-->最低優先級OB銷毀舊池創建新池-->runloop將退出-->最低優先級OB銷毀新池-->線程(銷毀)
<br />CFRunLoopMode
- RunLoop在同一段時間只能且必須在一種特定Mode下Run
- 更換Mode時,需要停止當前Loop,然后重啟新Loop
- Mode是iOS App滑動順暢的關鍵
- 可以定制自己的Mode
// 默認狀態、空閑狀態
NSDefaultRunLoopMode
// 滑動ScrollView時
UITrackingRunLoopMode
// 私有,App啟動時
UIInitializationRunLoopMode
// Mode集合,可以理解為 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的集合
NSRunLoopCommonModes
Runloop與GCD任務:
當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調里執行這個 block。Runloop只處理主線程的block,dispatch 到其他線程仍然是由 libDispatch 處理的。
關于網絡請求
iOS 中,關于網絡請求的接口自下至上有如下幾層:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
1.CFSocket 是最底層的接口,只負責 socket 通信。
2.CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。
3.NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向對象的接口,AFNetworking 工作于這一層。
4.NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。
下面主要介紹下 NSURLConnection 的工作過程。
通常使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 后,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source)。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。
當開始網絡傳輸時,我們可以看到 NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知。當收到通知后,其會在合適的時機向 CFMultiplexerSource 等 Source0 發送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執行實際的回調。
Runloop實驗
實驗一
- (IBAction)buttonDidClick:(id)sender {
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
輸出結果
實驗二
把代碼改成如下,輸入結果一樣
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];
屏蔽,會發現沒有打印東西,因為timerWithTimeInterval
這個方法只是創建了并沒有加入Runloop
實驗三 有scrollView的情況下使用Timer
在實驗二的基礎上,在vc中加一個textView,run起來,模擬器界面如下:
點擊按鈕,然后滾動scrollView,在停止滾動,打印結果
可以看的出來滾動的時間段,timer并沒有效果,那是因為滾動的時候主線程Runloop已經切換mode為UITrackingRunLoopMode,Runloop只能指定一個mode,而timer只是加在NSDefaultRunLoopMode,所以發生滾動的時候,Runloop并不會響應timer;當松開手的時候Runloop切換回NSDefaultRunLoopMode,timer就重新起作用。
當我們把timer的mode修改為NSRunLoopCommonModes,此時滾動scrollView的同時也能響應timer:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
<br />
實驗四 CFRunLoopSourseRef的實驗
我們在button的響應注釋,然后打個斷點,run后點擊button會發現如下類似這種UIEvent是屬于Souce0
<br />
實驗五 CFRunLoopObserverRef的實驗
- (void)createObserver {
// 創建監聽者對象
// rl: RunLoop
// observer: 監聽者對象
// mode: Runloop所在的mode
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"observer--------%lu", activity);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
根據CFRunLoopActivity枚舉,我們可以看出Runloop的狀態變化
1:即將進入Runloop-> 2:即將處理NSTimer-> 4:即將處理Souce0 -> 32:即將進入休眠 -> 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
};
實驗更新
代碼:
- (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__);
}
點擊按鈕后打印出+++++
,然后點擊屏幕空白處- (void)myThreadTest
并沒有觸發。
這是因為_myThread中的Runloop只run了一次就退出了,從而子線程沒有監聽到屏幕的點擊事件。只run一次的原因首先看這張圖
代碼中只是讓子線程的運行循環run了一次,并沒有加入實質的source、port、Observer或者timer,Runloop直接跑一次直接退出了,導致點擊時間沒有Runloop來響應。
要響應- (void)myThreadTest
必須要子線程的Runloop保持駐留狀態,給Runloop添加一個port讓其保持駐留,此時我們點擊button之后再點擊屏幕空白處可以看到打印出來的日志,可以看的出來點擊事件已經起效了,并且+++++
也沒有打印出來,那是因為子線程的運行循環已經駐留,循環外面的代碼就執行不到。
- (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的創建
+ (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
因為UITableView滾動的時候主線程Runloop的mode切換為UITrackingRunLoopMode
,當停止滾動的時候會切回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.lxweimin.com/p/37ab0397fec7
https://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690