Runloop概述
runloop是來做什么的?runloop和線程有什么關系?主線程默認開啟了runloop么?子線程呢?
- runloop: 從字面意思看:運行循環、跑圈,其實它內部就是do-while循環,在這個循環內部不斷地處理各種任務(比如Source、Timer、Observer)事件。
- runloop和線程的關系:一個線程對應一個RunLoop,主線程的RunLoop默認創建并啟動,子線程的RunLoop需手動創建且手動啟動(調用run方法)。RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
RunLoop的作用?
- 1.保持程序運行
- 2.處理app的各種事件(比如觸摸,定時器等等)
- 3.節省CPU資源,提高性能。
RunLoop內部是怎么實現的?
- RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。
- 每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。
- 如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。
runloop的mode是用來做什么的?有幾種mode?
- model:是runloop里面的運行模式,不同的模式下的runloop處理的事件和消息有一定的差別。系統默認注冊了5個Mode:
(1)kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
(2)UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
(3)UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
(4)GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。
(5)kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。注意iOS 對以上5中model進行了封裝 NSDefaultRunLoopMode、NSRunLoopCommonModes
Runloop啟動過程
1.通知觀察者 run loop 已經啟動
2.通知觀察者將要開始處理Timer事件
3.通知觀察者將要處理非基于端口的Source0
4.啟動準備好的Souecr0
5.如果基于端口的源Source1準備好并處于等待狀態,立即啟動:并進入步驟9
6.通知觀察者線程進入休眠
7.將線程置于休眠直到任一下面的事件發生
(1)某一事件到達基于端口的源
(2)定時器啟動
(3)Run loop 設置的時間已經超時
(4)run loop 被顯式喚醒
8.通知觀察者線程將被喚醒
9.處理未處理的事件,跳回2
(1)如果用戶定義的定時器啟動,處理定時器事件并重啟 run loop。進入步驟 2
(2)如果輸入源啟動,傳遞相應的消息
(3)如果 run loop 被顯式喚醒而且時間還沒超時,重啟 run loop。進入步驟 2
10.通知觀察者run loop 結束
為什么把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環以后,滑動scrollview的時候NSTimer卻不動了?
- NSTimer對象是在 NSDefaultRunLoopMode下面調用消息的,但是當我們滑動scrollview的時候,NSDefaultRunLoopMode模式就自動切換到UITrackingRunLoopMode模式下面,卻不可以繼續響應nstime發送的消息。所以如果想在滑動scrollview的情況下面還調用nstime的消息,我們可以把nsrunloop的模式更改為NSRunLoopCommonModes.
與FPS的關系
使用實例
在開發中如何使用RunLoop?什么應用場景?
常駐線程:給子線程開啟一個RunLoop,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出
-
AutoreleasePool: iOS應用啟動后會注冊兩個 Observer 管理和維護 AutoreleasePool
第一個 Observer 會監聽 RunLoop 的進入,它會回調objc_autoreleasePoolPush() 向當前的 AutoreleasePoolPage 增加一個哨兵對象標志創建自動釋放池。這個 Observer 的 order 是 -2147483647 優先級最高,確保發生在所有回調操作之前。
第二個 Observer 會監聽 RunLoop 的進入休眠和即將退出 RunLoop 兩種狀態,在即將進入休眠時會調用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據情況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出 RunLoop 時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象。這個Observer 的 order 是 2147483647 ,優先級最低,確保發生在所有回調操作之后。
3.UITableView 與 NSTimer 沖突
4.AFNetworking內部創建了一個空的線程并啟動了RunLoop,當需要使用這個后臺線程執行任務時AFNetworking通過performSelector: onThread: 將這個任務放到后臺線程的RunLoop中。
5.防止UITableView滾動卡頓([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])
6.sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閑狀態下計算出UITableViewCell的高度并進行緩存。
7.老譚的PerformanceMonitor關于iOS實時卡頓監控,同樣是利用Observer對RunLoop進行監視。
[beforeSource, beforeWaiting], [AfterWaitng, ...]
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyClass *object = (__bridge MyClass*)info;
// 記錄狀態值
object->activity = activity;
// 發送信號
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 創建信號
semaphore = dispatch_semaphore_create(0);
// 在子線程監控時長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定連續5次超時50ms認為卡頓(當然也包含了單次超時250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
NSLog(@"好像有點兒卡哦");
}
}
timeoutCount = 0;
}
});
}
AutoReleasePool(深入解析 Autoreleasepool)
蘋果是如何實現Autorelease Pool的?
- Autorelease Pool作用:緩存池,可以避免我們經常寫relase的一種方式。其實就是延遲release,將創建的對象,添加到最近的autoreleasePool中,等到autoreleasePool作用域結束的時候,會將里面所有的對象的引用計數器 - autorelease.
每一個自動釋放池都是由一系列的 AutoreleasePoolPage 組成的,以雙向鏈表的形式連接起來的,并且每一個 AutoreleasePoolPage 的大小都是 4096 字節
class AutoreleasePoolPage {
magic_t const magic; //對當前 AutoreleasePoolPage 完整性的校驗
id *next;
pthread_t const thread; // 保存了當前頁所在的線程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
POOL_SENTINEL(哨兵對象)
- POOL_SENTINEL 只是 nil 的別名。
- 在每個自動釋放池初始化調用 objc_autoreleasePoolPush 的時候,都會把一個 POOL_SENTINEL push 到自動釋放池的棧頂,并且返回這個 POOL_SENTINEL 哨兵對象
- 當方法 objc_autoreleasePoolPop 調用時,就會向自動釋放池中的對象發送 release 消息,直到第一個 POOL_SENTINEL
objc_autoreleasePoolPush
- 有 hotPage 并且當前 page 不滿 , 調用 page->add(obj) 方法將對象添加
- 有 hotPage 并且當前 page 已滿 , 調用 autoreleaseFullPage 初始化一個新的頁 , 調用 page->add(obj) 方法將對象添加
- 無 hotPage , 調用 autoreleaseNoPage 創建一個 hotPage , 調用 page->add(obj) 方法將對象添加
- page->add 添加對象 , 對next指針的一個壓入操作
objc_autoreleasePoolPop
- objc_autoreleasePoolPop 傳入哨兵對象
總結
自動釋放池是由 AutoreleasePoolPage 以雙向鏈表的方式實現的
當對象調用 autorelease 方法時,會將對象加入 AutoreleasePoolPage 的棧中
調用 AutoreleasePoolPage::pop 方法會向棧中的對象發送 release 消息
使用場景
1.寫基于命令行的的程序時,就是沒有UI框架,如AppKit等Cocoa框架時。
2.寫循環,循環里面包含了大量臨時創建的對象。
3.創建了新的線程。(非Cocoa程序創建線程時才需要)
4.長時間在后臺運行的任務。
第三方庫使用舉例
SDWebImage(SDWebImageDecoder.m文件中,3.7.0版本)
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
return nil;
}
...
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
if (anyAlpha) {
return image;
}
...
return imageWithoutAlpha;
}
}
此處操作是生成解壓縮圖片,該方法會在SDWebImageDownloaderOperation connection:(NSURLConnection *)connection didReceiveData:(NSData *)data方法中不停回調,該方法調用會發生在子線程,同時方法內繪制bitmap會生成大量臨時對象,符合第二、第三兩種情況
CocoaLumberjack gcd操作大量使用
- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level {
dispatch_async(_loggingQueue, ^{ @autoreleasepool {
[self lt_addLogger:logger level:level];
} });
}
- (void)removeLogger:(id <DDLogger>)logger {
dispatch_async(_loggingQueue, ^{ @autoreleasepool {
[self lt_removeLogger:logger];
} });
}
dispatch_async使用自定義并發隊列,系統并不會內部線程中添加autoreleasepool,所以手動添加autoreleasepool更好,此處如果使用globalQueue是不需要添加的,符合第三種使用條件
自動釋放池什么時候釋放?
- 當RunLoop開啟時,就會自動創建一個自動釋放池,當RunLoop在休息之前會釋放掉自動釋放池的東西,然后重新創建一個新的空的自動釋放池
Runloop其他博客
掘金總結 https://juejin.im/entry/599c13bc6fb9a0248926a77d
iOS-RunLoop充滿靈性的死循環 http://www.lxweimin.com/p/b9426458fcf6
RunLoop入門 看我就夠了 http://www.lxweimin.com/p/2d3c8e084205
RunLoop總結:RunLoop的應用場景(三) https://blog.csdn.net/u011619283/article/details/53483965
iOS刨根問底-深入理解RunLoop https://www.cnblogs.com/kenshincui/p/6823841.html
iOS實時卡頓監控 http://www.cocoachina.com/ios/20161101/17903.html
iOS Runloop相關面試題