RunLoop做用html
模擬RunLoop內部實現面試
iOS中有兩套API能夠建立獲取RunLoop對象。分別是Foundation框架的NSRunLoop和C語言的CFRunLoopRef微信
NSRunLoop和CFRunLoopRef都表明着RunLoop對象
NSRunLoop是基於CFRunLoopRef的一層OC包裝,因此要了解RunLoop內部結構,須要多研究CFRunLoopRef層面的API(Core Foundation層面)app
void message(int num) { printf("執行第%i個任務", num); } int main(int argc, const char * argv[]) { do { printf("有事嗎? 沒事我睡了"); int number; scanf("%i", &number); message(number); } while (1); return 0; }
一個線程對應一個RunLoop,主線程的RunLoop默認程序啓動就已經建立好了。框架
子線程默認沒有RunLoop,不過子線程能夠有RunLoop,子線程的RunLoop得手動建立而且手動啓動(調用run方法)函數
RunLoop在第一次獲取時建立,在線程結束時銷燬oop
能夠理解爲,子線程的RunLoop是懶加載的(主線程除外)。只有用到的時候纔會建立( 調用currentRunLoop方法)。性能
若是是在子線程中調用currentRunLoop,那麼系統會先查看當前子線程是否有與之對應的NSRunLoop,若是沒有就建立一個RunLoop對象學習
注意:若是想給子線程添加一個與之對應的RunLoop,不能經過alloc、init方法,只能經過currentRunLoop,若是用alloc、init建立出來的RunLoop不能添加到子線程。ui
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
// 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) { __CFUnlock(&loopsLock); // 建立字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); // 建立主線程 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // 保存主線程 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } // 從字典中獲取子線程 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { // 若是不存在建立一個新的子線程 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 保存子線程 if (!loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; }
以上是從CF-1151.16的CFRunLoop.c文件中拷貝的RunLoop的源碼:
當咱們經過[NSRunLoop currentRunLoop]調用NSRunLoop的currentRunLoop方法的時候,底層就會調用NSRunLoopRef的get方法。
1.程序啓動,底層會先建立一個字典。
2.而後立刻會建立一個主線程的RunLoop,並把主線程做爲key,把主線程的RunLoop做爲value添加到字典中。
注意:這也就是爲何一個線程對應一個RunLoop的緣由,由於RunLoop是經過key-value的形式和線程以一一對應的方式保存在字典中的。
3.若是從子線程經過[NSRunLoop currentRunLoop]調用NSRunLoop的currentRunLoop方法的時候,系統會以子線程做爲key,去字典中取對應的RunLoop對象。
4.若是取出來的RunLoop對象爲空,則系統會建立一個RunLoop對象並以子線程做爲key把該RunLoop對象存儲到字典中去。
Core Foundation中關於RunLoop的5個類:
CFRunLoopRef :RunLoop對象
CFRunLoopModeRef :RunLoop的模式,能夠把RunLoop理解爲空調,對應着許多模式,可是一個RunLoop同時只能執行一種模式
CFRunLoopSourceRef : 事件來源,用來處理RunLoop的事件
CFRunLoopTimerRef :定時器,處理和定時器相關的事情
CFRunLoopObserverRef :經過observer監聽事件
一個RunLoop有多個模式:每一個模式都有各自的source、timer和observer。
注意:一個RunLoop有多個模式,可是在同一時刻只能執行一種模式。
CFRunLoopModeRef:
CFRunLoopModeRef表明RunLoop的運行模式
一個 RunLoop 包含若干個 Mode,每一個Mode又包含若干個Source/Timer/Observer
每次RunLoop啓動時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode
若是須要切換Mode,只能退出Loop,再從新指定一個Mode進入
這樣作主要是爲了分隔開不一樣組的Source/Timer/Observer,讓其互不影響
NSDefaultRunLoopMode:App的默認Mode,一般主線程是在這個Mode下運行。程序啓動,若是用戶什麼都沒作,默認就在這個模式
UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。程序啓動,若是用戶滑動了scrollView,就會從默認模式切換到這個模式
UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用
GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到
NSRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode
PS:前四種模式是真正的模式,最後一種模式不是真正的模式。主要學習前兩種模式和最後一種模式。
runLoop默認是個死循環,源碼以下:
// 用DefaultMode啓動 void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }
CFRunLoopSourceRef:
CFRunLoopSourceRef是事件源(輸入源)
按照官方文檔,Source的分類
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
按照函數調用棧,Source的分類
Source0:非基於Port的, 用於用戶主動觸發事件
Source1:基於Port的,經過內核和其餘線程相互發送消息
CFRunLoopTimerRef:
CFRunLoopTimerRef是基於時間的觸發器
CFRunLoopTimerRef基本上說的就是NSTimer,它受RunLoop的Mode影響
GCD的定時器不受RunLoop的Mode影響
建立出來NSTimer對象,咱們須要把NSTimer對象添加到runLoop中
// 建立一個NSTimer以後, 必須將NSTimer添加到RunLoop中, 才能執行 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES]; // 添加到runLoop中(下面這就話就是把timer添加到當前線程的默認模式下) [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
runLoop同一時間只能執行一個模式,因此若是把timer添加到默認模式,那麼timer只在默認模式下生效。例如,切換到追蹤模式,默認模式下的timer是無效的。
那麼怎麼讓timer在默認模式和追蹤模式下都有效呢?
/* common modes = { 0 : <CFString 0x105b56e50 [0x104e83180]>{contents = "UITrackingRunLoopMode"} 2 : <CFString 0x104e5f080 [0x104e83180]>{contents = "kCFRunLoopDefaultMode"} } */
// 這是一個佔位用的Mode,不是一種真正的Mode // 其實Common是一個標識, 它是將NSDefaultRunLoopMode和UITrackingRunLoopMode標記爲了Common // 因此, 只要將timer添加到Common佔位模式下,timer就能夠在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下都能運行
// 至關於timer添加到了這兩個模式中,在這兩個模式中都有效
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
GCD的timer不受NSRunLoop定時器的影響
// 1.建立tiemr // queue: 表明定時器未來回調的方法在哪一個線程中執行 // dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); self.timer = timer; // 2.設置timer /* 第一個參數: 須要設置哪一個timer 第二個參數: 指定定時器開始的時間 第三個參數: 指定間隔時間 第四個參數: 定時器的精準度, 若是傳0表明要求很是精準(系統會讓定時器執行的時間變得更加準確) 若是傳入一個大於0的值, 就表明咱們容許的偏差 // 例如傳入60, 就表明容許偏差有60秒 */ // 定時器開始時間 // dispatch_time_t startTime = DISPATCH_TIME_NOW; // 調用這個函數,就能夠指定兩秒以後開始/而不是當即開始 dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); // 定時器間隔的時間 uint64_t timerInterval = 2.0 * NSEC_PER_SEC; dispatch_source_set_timer(timer, startTime, timerInterval, 0 * NSEC_PER_SEC); // 3.設置timer的回調 dispatch_source_set_event_handler(timer, ^{ NSLog(@"我被調用了 %@", [NSThread currentThread]); }); // 4.開始執行定時器 dispatch_resume(timer); }
CFRunLoopObserverRef:
CFRunLoopObserverRef是觀察者,可以監聽RunLoop的狀態改變
能夠監聽的時間點有如下幾個:
自定義Observer來監聽指定線程的狀態的改變:
// 0.建立一個監聽對象 /* 第一個參數: 告訴系統如何給Observer對象分配存儲空間 第二個參數: 須要監聽的類型 第三個參數: 是否須要重複監聽 第四個參數: 優先級 第五個參數: 監聽到對應的狀態以後的回調 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // NSLog(@"%lu", activity); switch (activity) { case kCFRunLoopEntry: NSLog(@"進入RunLoop"); break; case kCFRunLoopBeforeTimers: NSLog(@"即將處理timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即將處理source"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即將進入睡眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"剛剛從睡眠中醒來"); break; case kCFRunLoopExit: NSLog(@"退出RunLoop"); break; default: break; } }); // 1.給主線程的RunLoop添加監聽 /* 第一個參數:須要監聽的RunLoop對象 第二個參數:給指定的RunLoop對象添加的監聽對象 第三個參數:在那種模式下監聽 */ CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 若是經過scheduled方法建立NSTimer, 系統會默認添加到當前線程的默認模式下 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
一條線程對應一條RunLoop,程序一啓動,主線程的RunLoop就已經建立而且和主線程綁定好。經過查看RunLoop源代碼,系統內部是經過字典的形式把線程和RunLoop進行了綁定。
子線程的RunLoop默認是沒有的,若是想使用子線程的RunLoop,只須要在子線程調用NSRunLoop的currentRunLoop方法便可。
咱們能夠把RunLoop理解爲懶加載的,只有在用到的時候纔會建立。ru若是子線程中調用了currentRunLoop方法,那麼系統會先根據子線程去字典中取對應的RunLoop,若是沒有,則系統會建立一個RunLoop而且和該子線程進行綁定而且保存到字典中。
每一個RunLoop中又有不少的mode,每一個mode中又能夠有不少的source、timer和observer。須要注意的是,RunLoop在同一時刻只能執行一種模式,也就是同一時刻,只有一個模式中的source、timer和observer有效,其餘模式的source、timer和observer無效。蘋果這樣作的目的是防止不一樣模式中的source、timer和observer相互影響,很差控制。
能夠經過timer的形式來監聽RunLoop的執行流程:
進入RunLoop,首先會處理一些系統的事件(也就是首先執行timer、source0、source1)當處理完後,RunLoop就會睡覺。當用戶觸發一些事件後,RunLoop就會從睡眠中醒來,處理timer、source0和source1.處理完事件後又繼續睡覺。
RunLoop是有生命週期的,RunLoop掛掉有兩種狀況:
1.生命週期到了,默認RunLoop的生命週期是很大的,不過咱們能夠本身設置runLoop的生命週期
2.線程掛了,RunLoop也會掛掉
runLoop主要有5個應用場景:NSTimer、ImageView顯示圖片、performSelecter、常駐線程、自動釋放池
默認程序啓動會進入runLoop的default模式。performSelecter: withObject:afterDelay:inMode:方法默認就是在default模式下有效。而在track追蹤模式下無效,因此能夠經過設置模式來控制imageView圖片的顯示。
// 只有在追蹤模式下才會給imageView設置圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"def"] afterDelay:2.0 inModes:@[UITrackingRunLoopMode]];
開發中通常在默認狀況下設置圖片而在追蹤模式下是不設置圖片的,這樣一來能夠提升咱們應用程序的流暢性,爲何呢?若是在track模式下,不只處理屏幕的拖拽事件,還要給imageView設置圖片,很容易出現程序卡頓的現象。
常駐線程應用場景:
舉個例子,某個應用須要頻繁的下載或者上傳大容量的音頻或者視頻,默認主線程就是一個常駐線程,可是這種耗時操做確定要轉移到子線程中取完成。好比說微信\陌陌,用戶有時候須要一直髮送語音,若是每發送一條語音就開啓一個自子線程,那麼頻繁的開啓、銷燬線極大的消耗手機性能,因此常駐線程就應運而生。
如何建立常駐線程?
嘗試一:再次調用[self.thread start];答案固然是否認的。緣由以下:
注意點:默認狀況下,只要一個線程的任務執行完畢,那麼這個線程就不能使用了。因此不能經過start方法來從新啓動一個已經執行完任務的線程。不然會報如下錯誤: -[WSThread start]: attempt to start the thread again'
嘗試二:給這個子線程一個執行不完的任務while(1);答案依然是否認的,緣由以下:
把while(1)添加到子線程執行,而子線程的任務中有一個while死循環,那麼其餘任務永遠也執行不到。
因此,經過死循環雖然保證了子線程永遠不死,可是不能讓子線程處理任務,由於子線程一直在處理while死循環的任務。
嘗試三:聯想主線程爲何不死,由於主線程默認一啓動就會綁定一個runLoop,因此嘗試給子線程綁定一個runLoop
[NSRunLoop currentRunLoop];
[runLoop run];
可是僅僅建立一個runLoop而後run依然無效。緣由以下:
注意:
(1). currentRunLoop僅僅表明建立了一個NSRunLoop對象, 並無運行RunLoop
(2). 一個NSRunLoop中, 若是沒有source或者timer, 那麼NSRunLoop就會退出死循環(面試極可能問到)。由於若是runLoop沒有source和timer,那麼這個runLoop就沒有source和timer事件處理,這個runLoop也就變得沒有意義,因此runLoop會自動退出。(runLoop是否退出和observer沒有關係,只和source和timer有關係)
因此,給runLoop添加一個source或者timer
最終的解決方案:
NSRunLoop *runLoop =[NSRunLoop currentRunLoop]; // 如下代碼的目的是爲了保證runloop不死
/*
// 給runLoop添加一個timer
// NSTimer *timer = [NSTimer timerWithTimeInterval:99999 target:self selector:@selector(demo) userInfo:nil repeats:NO];
// [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
*/
// 或者給runLoop添加一個source,通常都是添加source,不添加timer,寫三方框架的大牛都這麼寫 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];
至此,一個常駐子線程就已經建立好了,而且能夠接受並處理事件。而且只要是在這個常駐子線程中執行的任務,都是在同一個線程中。
以下是建立常駐子線程的代碼:
#import "ViewController.h" #import "WSThread.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView; @property (nonatomic, strong) WSThread *thread; /**< 子線程 */ @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.thread = [[WSThread alloc] initWithTarget:self selector:@selector(demo) object:nil]; [self.thread start]; } - (void)demo { // 在子線程執行 NSLog(@"%s", __func__); // 注意點: 默認狀況下只要一個線程的任務執行完畢, 那麼這個線程就不能使用了 // 在self.thread線程中執行test方法 // [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES]; // while(1); // 給子線程添加一個RunLoop // 注意: // 1. currentRunLoop僅僅表明建立了一個NSRunLoop對象, 並無運行RunLoop // 2. 一個NSRunLoop中, 若是沒有source或者timer, 那麼NSRunLoop就會退出死循環 NSRunLoop *runLoop =[NSRunLoop currentRunLoop]; // 如下代碼的目的是爲了保證runloop不死 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; NSLog(@"-----------"); } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 主線程 NSLog(@"%s", __func__); // [self.thread start]; [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES]; } - (void)test { NSLog(@"%s %@", __func__, [NSThread currentThread]); } @end
打印結果:
2015-10-31 17:46:29.780 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:29.780 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:29.959 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:29.959 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.121 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.122 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.266 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.267 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)} 2015-10-31 17:46:30.431 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:] 2015-10-31 17:46:30.432 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
5.自動釋放池
程序「即將進入runLoop」會建立自動釋放池,「即將退出runLoop」會銷燬自動釋放池。
即將進入休眠狀態會銷燬以前的自動釋放池,再建立一個新的自動釋放池。
/* _wrapRunLoopWithAutoreleasePoolHandler + activities = 0x1 = 1 = 即將進入RunLoop + 建立一個自動釋放池 _wrapRunLoopWithAutoreleasePoolHandler + activities = 0xa0 = 160 = 128 + 32 + 32 即將進入休眠 1.銷燬一個自動釋放池 2.再建立一個新的自動釋放池 + 128 即將退出RunLoop 銷燬一個自動釋放池 */ NSLog(@"%@", [NSRunLoop currentRunLoop]); NSLog(@"%d", 1 << 0); // 1 NSLog(@"%d", 1 << 1); // 2 NSLog(@"%d", 1 << 2); // 4 NSLog(@"%d", 1 << 5); // 32 NSLog(@"%d", 1 << 6); // 64 NSLog(@"%d", 1 << 7); // 128
蘋果官方文檔https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.htmlCFRunLoopRef是開源的http://opensource.apple.com/source/CF/CF-1151.16/