博客連接RunLoop面試題分析面試
更新於2019-07-29 完善AFNetworking常駐線程的做用bash
在重拾RunLoop原理中RunLoop的源碼進行了分析,本該作一個總結方便之後查看,可是RunLoop中的知識點相對來講比較多,總結的東西就比較多。在面試中,又常常愛問一些RunLoop的知識點,接着就以我以前能回憶起來的面試題來對RunLoop作一個總結。網絡
RunLoop就是一種循環,它將運行的程序包裹起來。當沒有事件時,RunLoop會進入休眠狀態,有事件發生時,RunLoop會去找對應的Handler處理事件。RunLoop可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠,一種讓線程能隨時處理事件但並不退出的機制。多線程
NSTimer默認運行在RunLoop的kCFRunLoopDefaultMode
下,在列表滑動的時候,RunLoop會切換UITrackingRunLoopMode
,由於RunLoop只能運行在一種模式下,因此NSTimer不會執行回調。併發
使用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
將timer添加到CommonModes中。它可讓timer
運行在全部被標記爲Common
屬性的Mode中,kCFRunLoopDefaultMode
和UITrackingRunLoopMode
默認都已經被標爲」Common」屬性的。async
NSTimer定時器的觸發是基於RunLoop運行的,因此使用NSTimer以前必須註冊到RunLoop,可是RunLoop爲了節省資源並不會在很是準確的時間點調用定時器。當RunLoop在處理比較複雜的任務時,可能會錯過一個時間點,那麼定時器只能等到下一個時間點執行,並不會延後執行。ide
Mode是RunLoop的運行模式,每一個RunLoop須要運行在一個特定的Mode中。若是須要切換Mode,只能退出Loop,再從新指定一個Mode進入。不一樣組的source0/source1/observer/timer能夠相互隔離,互不影響,從而提升執行效率。函數
經常使用的RunLoop的Modeoop
kCFRunLoopDefaultMode
:App的默認Mode,一般主線程是在這個Mode下運行;UITrackingRunLoopMode
:界面跟蹤Mode,用於滾動視圖追蹤觸摸滑動,保證界面滑動時不受其餘 Mode影響;kCFRunLoopCommonModes
:這是一個佔位用的Mode,並非一種真正的Mode;Common mode並非一個具體的Mode,它只是用來將Mode標記爲Common
屬性。當source0/source1/observer/timer被添加到Common mode下的時候,意味着他們能夠運行在全部被標記爲Common
屬性的Mode中。post
kCFRunLoopDefaultMode
和UITrackingRunLoopMode
默認就被標記了Common
屬性。
以按鈕點擊觸發事件爲例,點擊屏幕的時候,首先系統內部捕獲到這個點擊事件,這是在Source1
中處理的,Source1
會包裝成事件丟到事件隊列中,交給Source0
處理。
當UI須要更新的時候,好比改變了frame
、更新了UIView
/CALayer
的層次時,或者手動調用了setNeedsLayout
/setNeedsDisplay
方法後,這個UIView
/CALayer
就被標記爲待處理,並被提交到一個全局的容器去。
蘋果註冊了一個Observer
監聽BeforeWaiting
(即將進入休眠) 和 Exit
(即將退出Loop) 事件,回調去執行一個很長的函數:CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)()
這個函數裏會遍歷全部待處理的UIView
/CAlayer
以執行實際的繪製和調整,並更新界面。
在程序啓動以後,主線程會建立一個Runloop,也會建立兩個Observer,回調工做都是在_wrapRunLoopWithAutoreleasePoolHandler
函數中。
第一個Observer監聽的是Entry(即將進入Loop),回調是在_objc_autoreleasePoolPush()
中建立自動釋放池的,優先級是最高的,保證建立釋放池是在全部回調以前。
第二個Observer監聽有兩個事件:BeforeWaiting(進入休眠)時調用_objc_autoreleasePoolPop
和_objc_autoreleasePoolPush
釋放舊的釋放池以及建立新的釋放池;Exit(退出Loop)調用_objc_autoreleasePoolPop
來釋放自動釋放池。這個優先級是最低的,保證釋放池發生在全部回調以後調用。
CFRunLoopStop
函數。在AFNetworking2.x中即是利用runloop實現線程保活的。代碼以下:
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
複製代碼
接着本身控制線程的生命週期
@interface TestThreadViewController ()
@property (nonatomic, strong) TestThread *thread;
@end
@implementation TestThreadViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.whiteColor;
__weak __typeof(self) weakSelf = self;
// 這裏有個問題要注意: 線程內部會對target形成一個強引用
// 使用Block形式的API
self.thread = [[TestThread alloc] initWithBlock:^{
NSLog(@"%s -----beigin-----", __func__);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 添加一個Source1
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 調用runloop run, 則runloop就不能被中止,其內部是在不斷調用runMode:beforeDate:
// 不能調用[runLoop run];
while (weakSelf) {
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%s -----end-----", __func__);
}];
[self.thread start];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(100, 100, 30, 20);
button.backgroundColor = UIColor.redColor;
[self.view addSubview:button];
[button addTarget:self action:@selector(_didButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)dealloc {
NSLog(@"%@ dealloc", self.class);
// 下面的代碼就是說在子線程中執行threadStop方法
// 這裏要注意waitUntilDone的用法
// 若是傳入NO,該方法會直接返回執行後續的代碼,這樣會出現問題
// 若是傳入YES,表明必須等到子線程執行完threadStop方法才能執行後面的代碼
[self performSelector:@selector(threadStop)
onThread:self.thread
withObject:nil
waitUntilDone:YES];
}
- (void)_didButtonPressed:(id)sender {
NSLog(@"開始銷燬頁面");
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - 線程保活
- (void)threadPerformingTasks {
NSLog(@"%@ %s", [NSThread currentThread], __func__);
}
- (void)threadStop {
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s", __func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"觸發點擊事件");
[self performSelector:@selector(threadPerformingTasks)
onThread:self.thread
withObject:nil
waitUntilDone:NO];
}
@end
複製代碼
執行結果:
NSURLConnection
目前來講應該不多會被問到了,其底層會使用CFSocket
去發送和接收請求,在發送和接收的一些事件發生後通知原來線程的Runloop
去回調事件。
這道題能夠說是在RunLoop的範疇,可是它更像是AFNetworking2.x爲何使用線程保活的解答,只是內部使用RunLoop技術去解決問題。
首先須要在子線程去開始鏈接,請求發送後,所在的子線程須要保活以保證正常接收到 NSURLConnectionDelegate回調方法。若是每來一個請求就開一條線程,而且保活線程,這樣開銷太大了。因此只須要保活一條固定的線程,在這個線程裏發起請求、接收回調。
這裏要注意一點:雖然使用了一條固定的線程,可是網絡請求並非單線程的,網絡請求會放在一個併發隊列裏,是多線程的。請求返回後通知常駐線程,再經過常駐線程通知主線程。
如下代碼的輸出結果
- (void)viewDidLoad {
[super viewDidLoad];
NSInteger number = 1;
NSLog(@"%zd", number);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performSelector:@selector(printString) withObject:nil afterDelay:0];
});
number = 3;
NSLog(@"%zd", number);
}
- (void)printString {
NSLog(@"sdasdas");
}
複製代碼
結果:1 3
NSObject的performSelecter:afterDelay:
或者performSelector:onThread:
這樣相似的方法被調用時,都依賴於RunLoop。子線程默認不啓用RunLoop的,因此不會執行Selector
中傳入的方法。除非說API內部啓用了RunLoop。好比調用performSelector:onThread:withObject:waitUntilDone:
方法,waitUntilDone:
傳YES的時候,內部會本身啓動RunLoop。
解決方法:
dispatch_async(queue, ^{
[self performSelector:@selector(printString) withObject:nil afterDelay:0];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
});
複製代碼
將下面這張圖深深地刻在本身的骨子裏
當RunLoop一旦休眠意味着CPU不會分配任何資源,那線程也進入休眠。RunLoop休眠內部是調用了mach_msg()
函數。操做系統中有內核層面的API和應用層面的API。mach_msg()
能夠理解爲是應用層面的API,告訴內核休眠該線程休眠。一旦接受到系統事件,也會轉化成內核API,告訴內核須要喚醒該線程,那麼又能夠執行應用層API了。因此RunLoop的休眠能夠當作是用戶狀態到內核狀態的切換,而喚醒RunLoop就是內核狀態到用戶狀態的切換。