iOS Runloop & AutoReleasePool

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內部是怎么實現的?
    1. RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。
    1. 每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。
    1. 如果需要切換 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?什么應用場景?
  1. 常駐線程:給子線程開啟一個RunLoop,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出

  2. 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相關面試題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379