iOS開發-RunLoop總結 – iPhone手機開發 iPhone軟體開發教學課程

序言

我們知道當應用啟動後,系統會自動創建一個線程來執行任務,該線程被稱為主線程或者UI線程。其實在主線程創建的時候,系統還會為主線程創建並啟動一種機制(其實就是一個對象,該對象和應用的生命周期有關),叫做RunLoop,被稱為運行循環機制。本文主要將介紹iOS應用中的RunLoop機制。

RunLoop簡介

RunLoop概念

提到RunLoop,我們一般都會提到線程,這是為什麼呢?先來看下 官方對 RunLoop 的定義 : RunLoop是系統中和線程相關的基礎架構的組成部分( 和線程相關 ),一個RunLoop是一個事件處理環,系統利用這個事件處理環來安排事務,協調輸入的各種事件。當一個iOS應用啟動後,如果我們不做任何交互操作,那麼該應用默認不會做任何響應,一旦我們觸摸瞭屏幕,或者點擊瞭某個按鈕,程序就會立即做出相應的響應,給我們的操作一個反饋。就好像這個應用處於一個時刻準備著的狀態,有事要做的時候,它就會馬上做。沒有事要做的時候,它就等待一樣。應用的這一點全是靠RunLoop機制來實現的。RunLoop從字面理解可以把它看做一個運行循環,而且它會事件相關聯,所有這裡我們也暫時把它當做一個事件運行循環。

在系統中,所有的事件響應都由這個事件運行循環來派發和調度的。當系統沒有接收到事件,運行循環就會處於休眠狀態,來節約CPU的資源。當系統接收到事件,運行循環就會被喚醒,來分發和處理事件(這裡涉及到事件的傳遞和響應者鏈條)。所以有瞭這個運行循環的存在,應用就不需要一直處於活躍狀態,一切都由RunLoop來監管,這樣大大的節約瞭系統資源。

蘋果官方為我們提供瞭兩個這樣的運行循環對象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架內的,它提供瞭純 C 函數的 API,所有這些 API 都是線程安全的。NSRunLoop 是基於 CFRunLoopRef 的封裝,提供瞭面向對象的 API,但是這些 API 不是線程安全的。下面結合這兩個對象來介紹運行循環

RunLoop淺析

當應用調用main函數中的UIApplicationMain()方法啟動應用的時候,系統一方面加載視圖文件(storyboard或者是xib文件)和info.plist文件,創建必要的視圖對象;另一方面創建主線程,並在主線程中創建一個RunLoop對象(稱為主運行循環,MainRunLoop),並把該對象存放在一個全局的靜態字典中,以線程對象作為key。官方源碼:

 

/// 全局的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());
}

 

由此可以看出,一個線程對象就對應一個RunLoop對象。創建後,默認啟動該MainRunLoop對象。其內部是一個do-while循環。由此保證瞭應用程序的持續運行。其官方源碼如下:

 

/// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode啟動,允許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode裡沒有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內部函數,進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 觸發 Source0 (非port) 回調。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
            /// ? 一個基於 port 的Source 的事件。
            /// ? 一個 Timer 到時間瞭
            /// ? RunLoop 自身的超時時間到瞭
            /// ? 被其他什麼調用者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            /// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒瞭。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,處理消息。
            handle_msg:

            /// 9.1 如果一個 Timer 到時間瞭,觸發這個Timer的回調。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            /// 9.2 如果有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            /// 9.3 如果一個 Source1 (基於port) 發出事件瞭,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            

            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時參數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入參數標記的超時時間瞭
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調用者強制停止瞭
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有瞭
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

 

既然知道RunLoop機制是用來分發和處理事件的,那麼我們來看看RunLoop能處理的事件類型:輸入源(input source)和定時源(timer source)

輸入源(input source):傳遞異步事件,消息通常來自於其他線程,處理其他線程的消息,如下載操作執行完畢,要回到主線程中更新UI,這些異步事件就是由RunLoop來監聽和管理的。定時源(timer source):傳遞同步事件,發生在特定時間,或者時間間隔,如定時檢查UI界面上有沒有刷新事件、點擊事件等等,也就是處理本線程上的事件。

除瞭處理輸入源,RunLoop也會生成關於RunLoop行為的notification。註冊的RunLoop 觀察者可以收到這些notification,並做相應的處理。可以使用Core Foundation在你的線程註冊RunLoop觀察者。相應的通知如下:

 

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

 

輸入源、定時源、觀察者解析:

輸入源對應Core Foundation框架中的CFRunLoopSourceRef類:CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。Source0 隻包含瞭一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。Source1 包含瞭一個 mach_port 和一個回調(函數指針),被用於通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程。定時源對應Core Foundation框架中的CFRunLoopTimerRef類:CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。觀察者對應Core Foundation框架中的CFRunLoopTimerRef類:CFRunLoopObserverRef 是觀察者,每個 Observer 都包含瞭一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。

RunLoop隻有在一定的模式下才會運行,就是說要想啟動RunLoop就要指定其運行的模式。系統已經提供瞭RunLoop運行的5種模式,如下:

NSDefaultRunLoopMode:是RunLoop默認的模式,表示程序空閑。如果我們用NSTimer來每秒打印輸出的時候,一旦有手勢操作(如滑動、滾動等操作),那麼NSTimer就會停止執行。UITrackingRunLoopMode:跟蹤模式(是UIScrollView專用模式)。上面提到的NSTimer例子,一旦有滾動等操作,RunLoop就會自動從NSDefaultRunLoopMode模式切換到UITrackingRunLoopMode模式,目的就是為瞭保證滾動的流暢性,給用戶提供流暢的體驗。NSRunLoopCommonMode:這種模式會包含以上兩種模式。我們執行滑動操作的同時,NSTimer會始終調用方法來執行打印,但是,一旦所調用的方法中有耗時的操作時,效果就會卡頓。所有在實際開發中,不建議使用該模式。但是,有的需求就是又要有耗時操作又要保證流暢。解決辦法就是將耗時操作放到子線程中(子線程RunLoop需要手動啟動:CFRunLoopRun(),CFRunLoopStop(CFRunLoopCurrent())停止循環)。UIInitializationRunLoopMode:在程序剛啟動的時進入該模式,啟動完就不在使用。GSEventReceiveRunLoopMode:接受系統事件的內部mode,通常用不到。

RunLoop構成

從上面可以看出RunLoop的結構。不過這裡有一個重要的概念就是上面提到的模式:mode。RunLoop模式是所有要監視的輸入源和定時源以及要通知的註冊觀察者的集合。每次運行RunLoop都會指定其運行在哪個模式下。以後,隻有相應的源會被監視並允許接收他們傳遞的消息。(類似的,隻有相應的觀察者會收到通知)。其他模式關聯的源隻有在RunLoop運行在其模式下才會運行,否則處於暫停狀態。

通常代碼中通過指定名字來確定模式。Cocoa和Core Foundation定義瞭默認的以及一系列常用的模式,都是用字符串來標識。當然你也可以指定字符串來自定義模式。雖然你可以給模式指定任何名字,但是所有的模式內容都是相同的。你必須添加輸入源,定時器或者RunLoop觀察者到你定義的模式中。

所有由RunLoop的構成就清楚瞭,RunLoop必須指定一個mode,在mode中必須添加輸入源,定時器或者RunLoop觀察者。如果在啟動RunLoop的時候,沒有指定其模式,該RunLoop是沒有任何效果的。如果沒有添加任何源事件或Timer事件,線程會一直在無限循環的空轉中,會一直占用CPU時間片,沒有實現資源的合理分配。如果沒有while循環驅動且沒有添加任何源事件或Timer事件的線程,線程會直接完成,被系統回收。

RunLoop功能

分發和處理事件

在應用程序運行的時候,系統創建一個主線程,並創建一個主運行循環來管理該主線程。那運行循環是如何對事件進行分發和處理的呢?

蘋果註冊瞭一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收http://iphonedevwiki.net/index.php/IOHIDFamily 。SpringBoard 隻接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給需要的App進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理並包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

 

AutoreleasePool

 

在學習OC的內存管理的時候就一直接觸AutoreleasePool這個概念,對它也是一知半解的。學習這篇文字後,我就會知道AutoreleasePool是在什麼時候創建的,又是在什麼時候被銷毀的瞭。這裡我們再簡單的回顧一下AutoreleasePool的作用。

AutoreleasePool被稱為自動釋放池,在釋放池中的調用瞭autorelease方法的對象都會被壓在該池的頂部(以棧的形式管理對象)。當自動釋放池被銷毀的時候,在該池中的對象會自動調用release方法來釋放資源,銷毀對象。以此來達到自動管理內存的目的。

其實我們在開發中很少主動去創建AutoreleasePool對象,這是為什麼呢?不是說用它來自動管理內存嗎?其實系統在運行的時候就幫我們創建瞭AutoreleasePool對象,隻是我們不知道而已。那麼它是在什麼時候被創建的呢?來看看官方源碼:

 

CFRunLoop {
    current mode = kCFRunLoopDefaultMode
    common modes = {
        UITrackingRunLoopMode
        kCFRunLoopDefaultMode
    }

    common mode items = {

        // source0 (manual)
        CFRunLoopSource {order =-1, {
            callout = _UIApplicationHandleEventQueue}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventSignalCallback }}
        CFRunLoopSource {order = 0, {
            callout = FBSSerialQueueRunLoopSourceHandler}}

        // source1 (mach port)
        CFRunLoopSource {order = 0,  {port = 17923}}
        CFRunLoopSource {order = 0,  {port = 12039}}
        CFRunLoopSource {order = 0,  {port = 16647}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventCallback}}
        CFRunLoopSource {order = 0, {port = 2407,
            callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
        CFRunLoopSource {order = 0, {port = 1c03,
            callout = __IOHIDEventSystemClientAvailabilityCallback}}
        CFRunLoopSource {order = 0, {port = 1b03,
            callout = __IOHIDEventSystemClientQueueCallback}}
        CFRunLoopSource {order = 1, {port = 1903,
            callout = __IOMIGMachPortPortCallback}}

        // Ovserver
        CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
        CFRunLoopObserver {order = 0, activities = 0x20,          // BeforeWaiting
            callout = _UIGestureRecognizerUpdateObserver}
        CFRunLoopObserver {order = 1999000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _afterCACommitHandler}
        CFRunLoopObserver {order = 2000000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
        CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
            callout = _wrapRunLoopWithAutoreleasePoolHandler}

        // Timer
        CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
            next fire date = 453098071 (-4421.76019 @ 96223387169499),
            callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
    },

    modes = {
        CFRunLoopMode {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },

        CFRunLoopMode {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },

        CFRunLoopMode {
            sources0 = {
                CFRunLoopSource {order = 0, {
                    callout = FBSSerialQueueRunLoopSourceHandler}}
            },
            sources1 = (null),
            observers = {
                CFRunLoopObserver >{activities = 0xa0, order = 2000000,
                    callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
            )},
            timers = (null),
        },

        CFRunLoopMode {
            sources0 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventSignalCallback}}
            },
            sources1 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventCallback}}
            },
            observers = (null),
            timers = (null),
        },
        
        CFRunLoopMode {
            sources0 = (null),
            sources1 = (null),
            observers = (null),
            timers = (null),
        }
    }
}

 

App啟動後,,系統在主線程RunLoop 裡註冊兩個Observser,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其優先級最高,保證創建釋放池發生在其他所有回調之前。第二個 Observer 監視瞭兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 優先級最低,保證其釋放池子發生在其他所有回調之後。在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 瞭。

現在我們知道瞭AutoreleasePool是在RunLoop即將進入RunLoop和準備進入休眠這兩種狀態的時候被創建和銷毀的。所以AutoreleasePool的釋放有如下兩種情況。一種是Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入瞭自動釋放池Push和Pop。還有一種就是手動調用AutoreleasePool的釋放方法(drain方法)來銷毀AutoreleasePool。

NSTimer

RunLoop還可以用來開啟線程的運行循環。經過學習,我們知道主線程的運行循環是系統默認創建和開啟的。隻有那些子線程需要我們獲取RunLoop,並手動開啟。下面利用NSTimer的例子來介紹一下,如何開啟當前線程的RunLoop。

首先創建一個子線程並在子線程中執行打印任務。代碼如下:

 

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTag) userInfo:nil repeats:YES];
    });
}

- (void)testTag {
    NSLog(@"testTag");
}

運行看效果:沒有任何效果!!!

 

為什麼呢?不知道NSTimer看上去是不是能讓你們想到CFRunLoopTimerRef類。其實NSTimer可以看成就是CFRunLoopTimerRef。而CFRunLoopTimerRef是RunLoop機制中的定時源。還記得前面說過,要想運行RunLoop,就必須指定其運行模式,並向模式中添加輸入源、定時源或者觀察者。既然NSTimer是一種定時源,要想在子線程中運行就必須將該定時源添加到子線程的RunLoop中去才行啊,如下代碼:

 

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTag) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    });
}

- (void)testTag {
    NSLog(@"testTag");
}

再運行看看,還是沒有效果。這又是為什麼呢。我們想想,當前線程是一個子線程,子線程中的RunLoop默認是沒有開啟的呀。隻有主線程才會默認開啟。那麼我們來手動開啟該RunLoop。代碼如下:

 

 

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTag) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    });
}

- (void)testTag {
    NSLog(@"testTag");
}

再運行,成功瞭!!!可以得到,當當前線程為子線程的時候,要想運行RunLoop,必須手動開啟RunLoop。

 

其實與定時源描述相關聯的方法,在子線程中被調用都要手動開啟RunLoop。這些方法如下:

 

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

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;

- (void)performSelector:(SEL)aSelector target:(id)target argument:(id)arg order:(NSUInteger)order modes:(NSArray *)modes;

 

RunLoop實現

上面例子中,我們通過開啟一個線程的RunLoop來執行定時源的回調方法。而且RunLoop也是一個對象。前面也說過,RunLoop內部其實就是一個do-while循環在驅動著,那麼我們能不能自己實現一個RunLoop機制呢?下面我們來試試。

首先創建一個子線程,隻有線程存在,RunLoop才會存在。還記得開頭說的吧………提示一下全局的靜態字典。

 

- (void)testNewThread1{
    // 獲取當前線程RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
   // 向當前線程RunLoop添加源,並指定運行模式
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
   // 利用while循環驅動RunLoop運行
    while (!self.isCancelled && !self.isFinished) {
        @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
        }
    }
}

這樣實現的RunLoop機制會一直占用CPU資源,CPU資源不能合理分配。再看看下面的代碼,也是AFNetworking中實現的RunLoop機制的代碼:

 

 

+ (void)networkRequestThreadEntryPoint:(id)__unused object
{
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];     // 這裡主要是監聽某個 port,目的是讓這個 Thread 不會回收
        [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被開啟的線程會一直存在。因為在沒有事件發生的時候處於休眠狀態,有事件發生的時候處於工作狀態。以此來節約CPU資源。這樣就可以讓一個線程成為常駐線程,也就是說該線程一直存在。

 

RunLoop總結

RunLoop總結:

RunLoop是iOS事件響應與任務處理最核心的機制,它貫穿iOS整個系統。

RunLoop是一種事件運行循環機制,是保持應用程序持續運行的一種機制。正是由於該機制的存在,應用程序才能在沒有事件發生的時候處於休眠狀態,有事件發生的時候處於工作狀態。以此來節約CPU資源。這也是它的一大特點。

NSRunLoop是Cocoa框架中的類,與之對應的,在Core Foundation中是CFRunLoopRef類。兩者的區別是前者不是線程安全的,後者是線程安全的,且兩者可以相互轉化。

RunLoop和線程的關系:

RunLoop是用來管理線程的,每個線程對應一個RunLoop對象。我們不可以去創建當前線程的RunLoop對象,但是我們可以去獲取當前線程的RunLoop。RunLoop就是來監聽該線程有無事件發生,如果有就工作,如果沒有就休眠。

主線程的RunLoop對象默認開啟,其他線程默認不開啟。

RunLoop與AutoreleasePool;

RunLoop處理的事件類型;

RunLoop的運行模式mode;

發佈留言