從字面上來講是運行循環,也能夠翻譯爲跑圈.安全
RunLoop和線程是息息相關的,咱們都知道線程的做用就是用來執行特定的一個或多個任務,正常狀況下,線程執行完當前任務後就會退出,以後若線程又有任務須要執行也沒法繼續執行了.這時咱們就須要一種方式讓線程能不斷執行任務,即便當前線程沒有任務執行,線程也不會退出,而是等待下一個任務的到來.因此咱們就有了RunLoop.markdown
每一條線程都有惟一一個與之對應的RunLoop對象.app
主線程的RunLoop對象系統已經自動幫咱們建立好了,而且只有主線程結束時即程序結束時纔會銷燬.框架
子線程的Runloop對象須要咱們主動建立並維護,子線程的Runloop對象在第一次獲取時就會建立,銷燬則是在子線程結束時. 而且建立出來的runLoop對象默認是不開啓的,必須手動開啓RunLoop.ide
Runloop並不保證線程安全,咱們只能在當前線程內部操做當前線程的Runloop對象,而不能在當前線程中去操做其餘線程的RunLoop對象.函數
相關代碼以下:oop
NSRunLoop *currentRunLoop = [NSRunloop currentRunloop] //獲取當前線程的RunLoop對象,在子線程中調用時若是是第一次獲取內部會幫咱們建立RunLoop對象
[currentRunLoop run];
[NSRunLooop mainRunLoop] //獲取主線程的RunLoop對象
複製代碼
咱們在啓動一個程序時,系統會自動調用建立項目時自動建立的main.m 的文件.main.m文件以下所示:性能
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
複製代碼
其中UIApplicationMain
函數中內部幫咱們開啓了主線程的RunLoop,這個RunLoop使得程序只要不退出或者崩潰,UIApplicationMain
函數就一直不會返回,保持了程序的持續運行.上邊的代碼中主線程開啓RunLoop的過程能夠簡單理解爲如下代碼:spa
int main(int argc, char * argv[]) {
BOOL isRunning = YES;
do {
//執行各類任務,處理各類事件
} while(isRunning);
return 0;
}
複製代碼
下圖是蘋果官方的RunLoop模型圖線程
從上圖能夠看出RunLoop就是線程中的一個循環,RunLoop會在循環中經過 Input sources(輸入源) 和 Timer sources(定時源)不斷檢測是否有事件須要執行.而後對接收到的事件通知線程去處理,而且在沒有事件的時候讓線程去休息.
iOS爲咱們提供了兩套API來訪問RunLoop, 一套是Foundation框架的NSRunLoop, 一套是Core Foundation框架的CFRunLoop. NSRunloop本質是基於CFRunLoop的oc對象封裝,因此咱們在這裏就講解Core Foundation框架下有關RunLoop的五個類.
5 CFRunLoopObserverRef: 觀察者,可以監聽RunLoop的狀態改變
下面詳細講解幾種類的具體含義相互關係. 先來看看一張能表示五個類關係的圖:
接着來說解這五個類的相互關係:
一個RunLoop對象(CFRunLoopRef)包含若干個運行模式(CFRunLoopModeRef)。而每一個運行模式下又有若干個輸入源(CFRunLoopSourceRef),定時源(CFRunLoopTimerRef),觀察者(CFRunLoopObserverRef)
下面咱們來詳細講解一下這五個類:
CFRunLoop類是Core Foundation框架下的RunLoop對象類.咱們能夠經過如下方式獲取RunLoop對象
CFRunLoopGetCurrent(); //獲取當前線程的RunLoop對象,在子線程中調用時若是是第一次獲取內部會幫咱們建立RunLoop對象
CFRunLoopGetMain(); //獲取主線程的RunLoop對象
系統默認定義了多種運行模式, 以下:
其中kCFRunLoopDefaultMode, UITrackingRunLoopMode,kCFRunLoopCommonModes是咱們開發中須要用到的模式.具體使用方法咱們在2.3 CFRunLoopTimerRef中結合CFRunLoopTimerRef來演示說明
CFRunLoopTimerRef是定時源, 理解爲基於時間的觸發器, 基本上就是NSTimer. 下面咱們來演示一下CFRunLoopModeRef和CFRunLoopTimerRef結合的使用方法.
在Main.Storyboard中拖入一個textView. 而後嘗試執行如下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
[self timer1];
}
- (void)timer1 {
//1.建立定時器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//2.將定時器添加到當前的RunLoop,指定RunLoop的運行模式爲默認運行模式
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run {
NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
複製代碼
當程序運行時, run方法每隔兩秒就會執行一次, 可是若拖動textView,run方法就不會執行.這是由於什麼呢?
咱們建立的timer是加入到RunLoop的NSDefaultRunLoopMode運行模式中, 可是當咱們拖動textView,當前RunLoop會退出當前運行模式,並進入到UITrackingRunLoopMode運行模式,咱們建立的timer並無添加到併到UITrackingRunLoopMode運行模式中,因此run方法就不會執行.
那麼有什麼解決方法呢?
解決方法一:
把timer也添加到UITrackingRunLoopMode運行模式中.這樣就能夠在兩種運行模式下都執行run方法.
增長代碼以下:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
複製代碼
解決方法二:
把timer加入到kCFRunLoopCommonMode運行模式中.前面2.2中已經提到這種模式其實知識一種佔位模式,並非真正的運行模式.如果將timer添加到這個模式中,那麼timer會被添加到打上common標籤的運行模式中.
那麼那些運行模式會被打上common標籤呢?
NSDefaultRunLoopMode 和 UITrackingRunLoopMode
因此只要添加到kCFRunLoopCommonMode運行模式也就等價於把timer加入到NSDefaultRunLoopMode和UITrackingRunLoopMode這兩種運行模式中.
將代碼替換成以下代碼:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼
除了上面代碼中使用的timer的建立方法,還有一種經常使用的timer建立方法
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
複製代碼
這種方法建立出來的timer會被默認添加到NSDefaultRunLoopMode運行模式,若想添加到UITrackingRunLoopMode中,只要拿到timer對象而後選擇上面的其中一種解決方法便可.
剛纔提到了例子都是在主線程中建立timer並加入到RunLoop中特定的運行模式中,那麼要是在子線程中建立timer有什麼區別呢?
請嘗試執行下面的代碼:
- (void)viewDidLoad {
[super viewDidLoad];
[NSThread detachNewThreadSelector:@selector(timer2) toTarget:self withObject:nil];
}
- (void)timer2 {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
- (void)run {
NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
複製代碼
你會發現run方法根本不會調用,這是爲何呢?
這其實就要和上面提到的runLoop的的建立和管理有關了.
子線程的Runloop對象須要咱們主動建立並維護,子線程的Runloop對象在第一次獲取時就會建立,銷燬則是在子線程結束時. 而且建立出來的runLoop對象默認是不開啓的,必須手動開啓RunLoop.
因此咱們應該修改代碼爲以下:
- (void)timer2 {
//1.獲取RunLoop並建立
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
//2.建立timer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//3.啓動子線程的RunLoop
[currentRunLoop run];
}
複製代碼
CFRunLoopSourceRef是事件源(RunLoop模型圖中提到過的)
第一種是經過官方理論來分的, 第二種是在實際應用中經過調用函數來分的.
下面咱們舉個例子經過函數調用棧中的source
1.首先咱們在main.storyboard中拖入一個按鈕,並添加動做
2.而後在點擊動做中的代碼中加入一個輸出語句,並打上一個斷點
步驟以下:
當咱們運行程序後點擊按鈕後就會來到此斷點,而後咱們就能夠查看當前的函數調用棧.
以下圖所示:
因此點擊事件是這樣來的:
CFRunLoopObserver是監聽者, 可以監聽RunLoop的狀態改變.
能夠監聽的時間點有如下:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中被喚醒
kCFRunLoopExit = (1UL << 7), //即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU //監聽全部事件
};
複製代碼
具體使用方法以下:
- (void)viewDidLoad {
[super viewDidLoad];
[self observer];
}
- (void)observer {
/**
@param1:怎麼分配空間(通常傳入默認分配方式)
@param2:要監聽的RunLoop的什麼狀態
@param3:是否要持續監聽
@param4:優先級 老是傳0
@param5:當狀態改變時的回調
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity 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;
}
});
/**
@param1:要監聽的RunLoop對象
@param2:觀察者
@param3:運行模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
複製代碼
打印臺信息以下:
能夠看到RunLoop在程序運行後就會處理大量的Source和Timer事件,當沒有事情須要作的時候就會進入休眠狀態,即讓線程休眠,當有事件須要處理時就會喚醒RunLoop再次處理事件,
五個類都理解完以後咱們就來具體說明RunLoop的運行原理.
其中咱們藉助下面這張網友的邏輯圖進行說明
結合上面這個邏輯圖咱們來講明一個蘋果官方文檔給出的RunLoop運行邏輯
具體順序以下:
首先RunLoop會去檢查Mode裏是否有source/timer, 沒有直接退出
前面都是一些理論知識的講解,接下來咱們咱們就講講在實戰中如何使用RunLoop.
剛剛在前面的2.3中咱們已經講解了把Timer加入到RunLoop的不一樣運行模式的做用和區別.你們若是忘了能夠回去再看看如何使用.
咱們可能有時會遇到一種狀況,就是咱們的界面有tableView,每一個tableView的cell中都有許多圖片.而後當咱們滾動tableView,須要顯示不少圖片,這時候可能就會出現卡頓現象.
那麼這時咱們就可使用RunLoop來解決這個問題.具體方法爲利用performSelector
方法調用UIImageView的setImage:
方法,而後指定在RunLoop下的NSDefaultRunLoopMode運行模式.代碼以下:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"男孩"] afterDelay:5.0 inModes:@[NSDefaultRunLoopMode]];
複製代碼
咱們設置顯示圖片的時間爲五秒以後,可是程序運行後咱們拖動textView,發現五秒後圖片並無出現,而是當咱們拖動結束時候才顯示出來.
這是由於咱們設置顯示圖片的操做是在RunLoop的NSDefaultRunLoopMode模式中,當咱們拖動textView時,RunLoop會切換到UITrackingRunLoopMode模式,這時即便設定的操做執行時間也不會執行,而是要等到咱們結束完拖動後纔會切換回NSDefaultRunLoopMode模式執行設置圖片的操做.
在上面推遲顯示圖片的程序中,咱們能夠發現當咱們切換到UITrackingRunLoopMode中,設定的執行操做的時間並無中止計時,因此當咱們一中止拖動時就會立刻執行操做.
那麼咱們要是在RunLoop的NSDefaultRunLoopMode模式下添加了一個timer,拖動textView一段時間後,許多本該執行的操做在中止拖動以後會怎樣執行呢.讓咱們運行下面代碼來看看效果吧.
- (void)viewDidLoad {
[super viewDidLoad];
//[self observer];
NSLog(@"%s", __func__);
[self test2];
}
- (void)test2 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"test2");
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
複製代碼
效果以下圖:
我是在13:02:50進行拖動textView,而後13:03:14結束拖動,能夠發現若是當時期間24秒間隔本應該要執行12次打印,最後只執行了兩次,並且這兩次執行是基本緊接着執行的,期間沒有間隔.而後又開始了正常的兩秒種執行一次打印.
因此咱們能夠得出RunLoop的邏輯,當timer添加到RunLoop的NSDefaultRunLoopMode模式時,在切換到UITrackingRunLoopMode模式後,RunLoop會最多暫存兩次操做,而後等到RunLoop切換回NSDefaultRunLoopMode模式下,再緊挨着執行兩次操做.
結論:
因此當NSTimer添加到NSDefaultRunLoopMode模式並非絕對精準的,當咱們滾動一些視圖時,執行操做就會變得不按時.解決方法就是把timer也添加到UITrackingRunLoopMode模式中,或者使用其餘定時器如GCD定時器.
[NSThread detachNewThreadSelector:@selector(run1) toTarget:self withObject:nil]
會建立並自動開啓一條線程執行任務,不須要手動啓動
咱們以前建立線程都是爲了執行特定任務,執行問特定任務後,線程會自動進入死亡狀態.線程進入死亡狀態後,是沒法再次啓動線程,讓線程繼續執行任務的.
若線程進入死亡狀態再次調用start方法會報錯
咱們在作項目時可能會在後臺執行頻繁操做,在子線程中執行耗時操做(以下載文件,後臺播放音樂,後臺記錄用戶信息),那麼我最好能讓線程不進入死亡狀態所以能夠持續的執行任務,而不是頻繁的建立和銷燬線程.
那麼咱們應該怎麼作呢?
添加一條指向常駐內存的線程強引用,而後在這條線程中建立一個RunLoop,並添加一個Sources,而後開啓RunLoop.緣由是RunLoop只要沒有超時,任務就會一直執行不完,那麼線程就不會進入死亡狀態.
具體實現過程:
實現代碼以下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%s", __func__);
[self residentThread];
}
- (void)residentThread {
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
self.thread = thread;
[self.thread start];
}
- (void)run1 {
//這裏寫須要執行的代碼
NSLog(@"run1 -- %@", [NSThread currentThread]);
//一個RunLoop至少須要一個Source或者Timer,在這裏添加一個Source1
[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
NSLog(@"未開啓RunLoop -- %@", [NSThread currentThread]);
}
複製代碼
3.運行後會發現 未開啓RunLoop 並不打印,由於RunLoop循環一直沒有返回.
爲了線程是否還能夠繼續執行其餘任務即沒有進入死亡狀態,咱們在touchesBegan中調用PerformSelector方法,看看是否會打印.
代碼以下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}
- (void)run2 {
NSLog(@"run2 -- %@", [NSThread currentThread]);
}
複製代碼
運行代碼後點擊屏幕,發現能夠打印,即線程可以繼續執行任務.這樣常駐線程就完成了.
1 只有子線程的RunLoop設置退出時間纔有用,主線程的RunLoop是沒法退出的.即下面這句代碼是不會起到使RunLoop退出的做用.
[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
複製代碼
2 RunLoop何時建立和銷燬自動釋放池
首先咱們要知道RunLoop爲何要建立自動釋放池?
由於在一個RunLoop運行循環過程當中會產生大量變量和對象,並且大多數變量是不會再使用的.那麼若不清理掉這些不用的變量,內存就可能會被堆滿.因此RunLoop會按期建立一個自動釋放池,而且在特意時間釋放掉釋放池,並從新再建立一個.
第一次建立: 啓動RunLoop的時候 最後一次銷燬: 退出RunLoop以前 其餘時候的建立和銷燬: 在RunLoop進入休眠狀態前會釋放掉舊的釋放池,釋放池中的變量也一塊兒被銷燬了.而後建立出一個新的釋放池,用來存放新產生的不用的變量.