Runloop學習總結

什么是Runloop

· 一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

· Runloop類似于一個while循環,循環執行代碼,保持程序的持續運行。
· RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數后,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。

· 在iOS的工程的main.m文件中我們可以看到這樣的代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain函數內部就啟動了一個Runloop,使App一直運行,這個默認開啟的Runloop默認和主線程關聯起來。

· 新建一個工程,在storyboard上加上按鈕,運行


Paste_Image.png

結果如下:

Paste_Image.png

從Xcode左上角看的出來程序一直在運行


Paste_Image.png

當把代碼改為:

int main(int argc, char * argv[]) {
    @autoreleasepool {
//        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        return 0;
    }
}

main函數直接返回0,AppDelegate里面的方法沒有執行,然后程序就就退出了。

Paste_Image.png

再把代碼修改如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", @"這里會打印");
        int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"%@", @"這里不會打印");
        return result;
    }
}

運行結果如下:

Paste_Image.png

程序執行了UIApplicationMain后開啟了默認的Runloop,一直循環15行,所以16行代碼永遠沒有執行。
Runloop可以看作下面的偽代碼:

int main(int argc, char * argv[]) {
   BOOL AppIsRunning = YES;
   while (AppIsRunning) {
        id whoWakesMe = SleepForWakingUp();
        id event = GetEvent(whoWakesMe);
        HandleEvent(event);
    }
    return 0;
}

Runloop有什么用處

1、使程序一直運行接受用戶輸入
2、決定程序在何時應該處理哪些Event
3、調用解耦(對于編程經驗為0的完全沒搞懂這個意思,解釋為Message Queue)
4、節省CPU時間


<br />

Runloop的機制

(套用sunnnyxx 在視頻中提供的資料)

Paste_Image.png
Paste_Image.png
Paste_Image.png

Runloop事件隊列

Paste_Image.png
Paste_Image.png
Paste_Image.png

RunLoop的掛起與喚醒
從偽代碼可以看出

  • 制定用于喚醒的mach_port端口
  • 調用mach_msg
  • 監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在mach_msg_trap
  • 由另外一個線程(或另一個進程中的某個線程)向內核發送這個端口的msg后,trap狀態被喚醒,RunLoop繼續開始干活

<br />
Runloop對象
1.iOS中有2tAPI來訪問和使用RunLoop
-Foundation 框架
NSRunLoop
-Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表著RunLoop對象
3.NSRunLoop是基于CFRunLoopRef的一層OC包裝, 所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API (Core Foundation 層面)

a 主線程的runloop自動創建,子線程的runloop默認不創建(在子線程中調用NSRunLoop *runloop = [NSRunLoop currentRunLoop];獲取RunLoop對象的時候,就會創建RunLoop);
b runloop退出的條件:app退出;線程關閉;設置最大時間到期;modeItem為空;
c 同一時間一個runloop只能在一個mode,切換mode只能退出runloop,再重進指定mode(隔離modeItems使之互不干擾);
d 一個item可以加到不同mode;一個mode被標記到commonModes里(這樣runloop不用切換mode)。

<br />Source是RunLoop的數據源抽象類(protocol)

RunLoop定義了兩個Version的Source:
1、Source0:處理App內部事件、App自己負責管理(觸發),如UIEvent、CFSocket
2、Source1:由RunLoop和內核管理,Mach port驅動,如CFMachPort、CFMessagePort
如有需要,可從中選擇一種來實現自己的Source
上一條基本不會發生

<br />RunLoopTimer的封裝

// 創建但是不會加入當前 Runloop
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

// 創建但是加入當前 Runloop 的 NSDefaultRunLoopMode 并執行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

<br />CFRunLoopObserver
向外部報告RunLoop當前狀態的更改,框架中很多機制都由RunLoopObserver觸發,如CAAnimation

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

UIKit通過RunLoopObserver在RunLoop兩次Sleep間對AutoreleasePool進行Pop和Push,將這次Loop中產生的Autorelease對象釋放
Runloop的寄生于線程:一個線程只能有唯一對應的runloop;但這個根runloop里可以嵌套子runloops;
自動釋放池寄生于Runloop:程序啟動后,主線程注冊了兩個Observer監聽runloop的進出與睡覺。一個最高優先級OB監測Entry狀態;一個最低優先級OB監聽BeforeWaiting狀態和Exit狀態。
線程(創建)-->runloop將進入-->最高優先級OB創建釋放池-->runloop將睡-->最低優先級OB銷毀舊池創建新池-->runloop將退出-->最低優先級OB銷毀新池-->線程(銷毀)

Paste_Image.png

<br />CFRunLoopMode

  • RunLoop在同一段時間只能且必須在一種特定Mode下Run
  • 更換Mode時,需要停止當前Loop,然后重啟新Loop
  • Mode是iOS App滑動順暢的關鍵
  • 可以定制自己的Mode
// 默認狀態、空閑狀態
NSDefaultRunLoopMode
// 滑動ScrollView時
UITrackingRunLoopMode
// 私有,App啟動時
UIInitializationRunLoopMode
// Mode集合,可以理解為 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的集合
NSRunLoopCommonModes
Runloop與GCD任務:

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調里執行這個 block。Runloop只處理主線程的block,dispatch 到其他線程仍然是由 libDispatch 處理的。

關于網絡請求

iOS 中,關于網絡請求的接口自下至上有如下幾層:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

1.CFSocket 是最底層的接口,只負責 socket 通信。
2.CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。
3.NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向對象的接口,AFNetworking 工作于這一層。
4.NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。

下面主要介紹下 NSURLConnection 的工作過程。

通常使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 后,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source)。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。

當開始網絡傳輸時,我們可以看到 NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate。



NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知。當收到通知后,其會在合適的時機向 CFMultiplexerSource 等 Source0 發送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執行實際的回調。


Runloop實驗

實驗一
- (IBAction)buttonDidClick:(id)sender {
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
     
- (void)timerTest {
    NSLog(@"%s", __func__);
}

輸出結果

Paste_Image.png
實驗二

把代碼改成如下,輸入結果一樣

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

如果把[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];屏蔽,會發現沒有打印東西,因為timerWithTimeInterval這個方法只是創建了并沒有加入Runloop

實驗三 有scrollView的情況下使用Timer

在實驗二的基礎上,在vc中加一個textView,run起來,模擬器界面如下:

Paste_Image.png

點擊按鈕,然后滾動scrollView,在停止滾動,打印結果

Paste_Image.png

可以看的出來滾動的時間段,timer并沒有效果,那是因為滾動的時候主線程Runloop已經切換mode為UITrackingRunLoopMode,Runloop只能指定一個mode,而timer只是加在NSDefaultRunLoopMode,所以發生滾動的時候,Runloop并不會響應timer;當松開手的時候Runloop切換回NSDefaultRunLoopMode,timer就重新起作用。

當我們把timer的mode修改為NSRunLoopCommonModes,此時滾動scrollView的同時也能響應timer:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

<br />

實驗四 CFRunLoopSourseRef的實驗

我們在button的響應注釋,然后打個斷點,run后點擊button會發現如下類似這種UIEvent是屬于Souce0

Paste_Image.png

<br />

實驗五 CFRunLoopObserverRef的實驗

- (void)createObserver {
    // 創建監聽者對象
    // rl: RunLoop
    // observer: 監聽者對象
    // mode: Runloop所在的mode
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"observer--------%lu", activity);
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
}
Paste_Image.png

根據CFRunLoopActivity枚舉,我們可以看出Runloop的狀態變化
1:即將進入Runloop-> 2:即將處理NSTimer-> 4:即將處理Souce0 -> 32:即將進入休眠 -> 64:從休眠仲喚醒

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

實驗更新

代碼:

- (IBAction)buttonDidClick:(id)sender {
    NSLog(@"%s", __func__);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _myThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"%@",  @"+++++");
    });
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(myThreadTest) onThread:_myThread withObject:nil waitUntilDone:NO];
}

- (void)myThreadTest {
    NSLog(@"%s", __FUNCTION__);
}

點擊按鈕后打印出+++++,然后點擊屏幕空白處- (void)myThreadTest并沒有觸發。

Paste_Image.png

這是因為_myThread中的Runloop只run了一次就退出了,從而子線程沒有監聽到屏幕的點擊事件。只run一次的原因首先看這張圖

Paste_Image.png

代碼中只是讓子線程的運行循環run了一次,并沒有加入實質的source、port、Observer或者timer,Runloop直接跑一次直接退出了,導致點擊時間沒有Runloop來響應。

要響應- (void)myThreadTest必須要子線程的Runloop保持駐留狀態,給Runloop添加一個port讓其保持駐留,此時我們點擊button之后再點擊屏幕空白處可以看到打印出來的日志,可以看的出來點擊事件已經起效了,并且+++++也沒有打印出來,那是因為子線程的運行循環已經駐留,循環外面的代碼就執行不到。

- (IBAction)buttonDidClick:(id)sender {
    NSLog(@"%s", __func__);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _myThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"++++");
    });
}
Paste_Image.png

Runloop使用

AFNetworking中RunLoop的創建
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (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;
}
利用Runloop有話UITableView

因為UITableView滾動的時候主線程Runloop的mode切換為UITrackingRunLoopMode,當停止滾動的時候會切回NSDefaultRunLoopMode,從而可以減輕UITableView的卡頓。

    UIImage *downloadedImage = ...;
    [self.avatarImageView performSelector:@selector(setImage:)
                               withObject:downloadedImage
                               afterDelay:0
                                  inModes:@[NSDefaultRunLoopMode]];

參考資料:
http://blog.ibireme.com/2015/05/18/runloop/
http://www.lxweimin.com/p/37ab0397fec7
https://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 通過閱讀YY大神的博客深入理解RunLoop還有觀看了孫源大大@sunnyxx錄制的RunLoop視頻,總算對Ru...
    巫師學徒閱讀 331評論 0 0
  • 什么是RunLoop 從字面上看,就是運行循環,跑圈 其實它內部就是do-while循環,在這個循環內部不斷地處理...
    zhazha閱讀 1,457評論 1 7
  • Runloop是iOS和OSX開發中非常基礎的一個概念,從概念開始學習。 RunLoop的概念 -般說,一個線程一...
    小貓仔閱讀 1,024評論 0 1
  • 隨著年齡的增長,在愛情中,最終和我們在一起的往往并不是當初我們愛的死去活來的人,取而代之的是那位愿意陪伴在我們身邊...
    琛筱閱讀 290評論 0 1
  • 步驟: 一、先打形,畫出大概輪廓。(用筆要輕,盡量用直線畫線,先不要糾結細節。) 二、根據大形畫出花的具體輪廓,運...
    南方小花CX閱讀 1,035評論 0 4