你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS RunLoop詳解

iOS RunLoop詳解

編輯:IOS開發綜合

一、簡介

[CFRunLoopRef源碼](http://opensource.apple.com/tarballs/CF/)

RunLoop是一個對象,這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件),從而保持程序的持續運行;而且在沒有事件處理的時候,會進入睡眠模式,從而節省CPU資源,提高程序性能。

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);
}
這種模型通常被稱作 Event Loop。 Event Loop 在很多系統和框架裡都有實現,比如 Node.js 的事件處理,比如 Windows 程序的消息循環,再比如 OSX/iOS 裡的 RunLoop。實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。 RunLoop管理了其需要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 “接受消息->等待->處理” 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。

二、RunLoop的深入分析

1. 從程序入口main函數開始

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

程序主線程一開始,就會一直跑,那麼猜想其內部一定是開啟了一個和主線程對應的RunLoop
並且可以看出函數返回的是一個int返回值的 UIApplicationMain()函數

2. 我們繼續深入UIApplicationMain函數

UIKIT_EXTERN int UIApplicationMain
(int argc, 
char *argv[], 
NSString * __nullable principalClassName,
 NSString * __nullable delegateClassName
);

我們發現它返回的是一個int類型的值,那麼我們對main函數做一些修改:
““
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@”開始”);
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@”結束”);
return re;
}
}

運行程序,我們發現只會打印`開始`,並不會打印``結束``,這再次說明在`UIApplicationMain`函數中,開啟了一個和主線程相關的`RunLoop`,導致`UIApplicationMain`不會返回,一直在運行中,也就保證了程序的持續運行。

##3. 繼續學習CFRunLoopRef
>RunLoop對象包括Fundation中的NSRunLoop對象和CoreFoundation中的CFRunLoopRef對象。因為Fundation框架是基於CoreFoundation的封裝,因此我們學習RunLoop還是要研究CFRunLoopRef 源碼。


###獲取RunLoop對象

//Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象


**1. 主線程獲取CFRunLoopRef源碼**
  ```
   // 創建字典
 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創建主線程 根據傳入的主線程創建主線程對應的RunLoop
 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

2. 創建與子線程相關聯的CFRunLoopRe源碼
蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,並先為主線程創建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    // 直接從 Dictionary 裡獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    if (!loop) {
        // 取不到時,創建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        // 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

3. CFRunloopRef與線程之間的關系

首先,iOS 開發中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經失效了,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果並沒有提供這兩個對象相互轉換的接口,但不管怎麼樣,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如,你可以通過 pthread_main_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基於 pthread 來管理的。 從CFRunLoopRef源碼可以看出,線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 裡。線程剛創建時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。
[NSRunLoop currentRunLoop];方法調用時,會先看一下字典裡有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop,如果沒有則會創建一個,並將與之對應的子線程存入字典中。

. 總結來說. CFRunloopRef與線程之間的關系

線程在處理完自己的任務後一般會退出,為了實現線程不退出能夠隨時處理任務的機制被稱為EventLoop,node.js 的事件處理,windows程序的消息循環,iOS、OSX的RunLoop都是這種機制。 線程和RunLoop是一一對應的,關系保存在全局的字典裡。
在主線程中,程序啟動時,系統默認添加了有kCFRunLoopDefaultMode 和 UITrackingRunLoopMode兩個預置Mode的RunLoop,保證程序處於等待狀態,如果接收到來自觸摸事件等,就會執行任務,否則處於休眠中。 線程創建時並沒有RunLoop,(主線程除外),RunLoop不能創建,只能主動獲取才會有。RunLoop的創建是在第一次獲取時,RunLoop的銷毀是發生在線程結束時。只能在一個線程中獲取自己和主線程的RunLoop。

Core Foundation中關於RunLoop的5個類

CFRunLoopRef  //獲得當前RunLoop和主RunLoop
CFRunLoopModeRef  //運行模式,只能選擇一種,在不同模式中做不同的操作
CFRunLoopSourceRef  //事件源,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者

* CFRunLoopModeRef*
1.簡介:

每個CFRunLoopRef 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

CFRunLoopModeRef 類並沒有對外暴露,只是通過 CFRunLoopRef 的接口進行了封裝。他們的關系如下:

CFRunLoopRef獲取Mode的接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

2.CFRunLoopMode的類型

kCFRunLoopDefaultMode
App的默認Mode,通常主線程是在這個Mode下運行

UITrackingRunLoopMode:
界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響

UIInitializationRunLoopMode:
在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用 GSEventReceiveRunLoopMode:
接受系統事件的內部 Mode,通常用不到 kCFRunLoopCommonModes:
這是一個占位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode

3.CFRunLoopMode 和 CFRunLoop 的結構

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set 
    CFMutableSetRef _sources1;    // Set 
    CFMutableArrayRef _observers; // Array 
    CFMutableArrayRef _timers;    // Array 
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set 
    CFMutableSetRef _commonModeItems; // Set 
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set 
    ...
};

CFRunLoopSourceRef

是事件產生的地方。Source有兩個版本:Source0 和 Source1。 Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。 Source1 包含了一個 mach_port 和一個回調(函數指針),被用於通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程.

* CFRunLoopObserverRef*

CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變。

我們直接來看代碼,給RunLoop添加監聽者,監聽其運行狀態:

 //創建監聽者
     /*
     第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
     第二個參數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態
     第三個參數 Boolean repeats:YES:持續監聽 NO:不持續
     第四個參數 CFIndex order:優先級,一般填0即可
     第五個參數 :回調 兩個參數observer:監聽者 activity:監聽的事件
     */
     /*
     所有事件
     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
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop進入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要處理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要處理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒來了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;

            default:
                break;
        }
    });

    // 給RunLoop添加監聽者
    /*
     第一個參數 CFRunLoopRef rl:要監聽哪個RunLoop,這裡監聽的是主線程的RunLoop
     第二個參數 CFRunLoopObserverRef observer 監聽者
     第三個參數 CFStringRef mode 要監聽RunLoop在哪種運行模式下的狀態
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的內存管理(Core Foundation)
     凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最後做一次release
     GCD本來在iOS6.0之前也是需要我們釋放的,6.0之後GCD已經納入到了ARC中,所以我們不需要管了
     */
    CFRelease(observer);
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved