你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS 開發runLoop 機制詳解

iOS 開發runLoop 機制詳解

編輯:IOS開發綜合

NSRunLoop機制

對於iOS開發,runLoop機制還是很有必要了解一下的,最近在做一個廣告圖的功能正好需要了解下runtime機制問題,在查看了官方文檔API以及論壇貼吧博客各位大牛的文章後,整理下關於我自己的理解和總結.

首先說介紹下NSRunLoop

OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。

NSRunLoop 與線程的關系

其實runLoop就是一個do … while()函數,每個runLoop對應一個線程他們是一一對應的關系,關系保存在一個全局的Dictionary裡邊,線程剛創建時沒有RunLoop,如果不主動獲取,是不會有的,RunLoop的創建發生在第一次獲取時,RunLoop的銷毀發生在線程結束,只能在一個線程的內部獲取它的RunLoop(主線程除外)主線程默認有個RunLoop.

Thread包含一個CFRunLoop,一個CFRunLoop包含一種CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。

RunLoop只能運行在一種mode下,如果要換mode當前的loop也需要停下重啟成新的。利用這個機制,ScrollView過程中NSDefaultRunLoopMode的mode會切換UITrackingRunLoopMode來保證ScrollView的流暢滑動不受只能在NSDefaultRunLoopMode時處理的事件影響滑動。同時mode還是可定制的。

NSDefaultRunLoopMode:默認,空閒狀態
UITrackingRunLoopMode:ScrollView滑動時
UIInitializationRunLoopMode:啟動時
NSRunLoopCommonModes:Mode集合 Timer計時會被scrollView的滑動影響的問題可以通過將timer添加到NSRunLoopCommonModes來解決

 

//然後再添加到NSRunLoopCommonModes裡
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

/// 全局的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的關系後,主要看下RunLoop和RunLoopModel,
RunLooprunLoop 在CoreFoundation對外一共有五大類最為對外的接口:

RunLoop

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

1.其中 CFRunLoopModeRef 類並沒有對外暴露,只是通過 CFRunLoopRef 的接口進行了封裝

這裡寫圖片描述
一個 RunLoZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcCCw/LqsyPS4ybj2IE1vZGWjrMO/uPYgTW9kZSDT1rD8uqzI9LjJuPYgU291cmNlL1RpbWVyL09ic2VydmVyoaPDv7TOtffTwyBSdW5Mb29wILXE1ve6r8r9yrGjrNa7xNzWuLaoxuTW0NK7uPYgTW9kZaOs1eK49k1vZGWxu7PG1/cgQ3VycmVudE1vZGWho8jnufvQ6NKqx9C7uyBNb2Rlo6zWu8Tczcuz9iBMb29wo6zU2dbY0MLWuLao0ru49iBNb2RlIL34yOuho9Xi0fnX9tb30qrKx86qwcu31rj0v6qyu82s1+m1xCBTb3VyY2UvVGltZXIvT2JzZXJ2ZXKjrMjDxuS7pbK707DP7KGjPC9wPg0KPGg0IGlkPQ=="2-cfrunloopsourceref">2. CFRunLoopSourceRef

是事件產生的地方,Source包含了兩個部分:Source0 和 Source1。
source0:處理如UIEvent,CFSocket這樣的事件
source1:Mach port驅動,CFMachport,CFMessagePort

Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

Source1 包含了一個 mach_port 和一個回調(函數指針),被用於和通過內核和其他 mach_port 相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

3. CFRunLoopTimerRef

NSTimer是對RunLoopTimer的封裝
是基於時間的觸發器,它和 NSTimer 是toll-free bridge 的。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調

4. CFRunLoopObserverRef

Cocoa框架中很多機制比如CAAnimation等都是由RunLoopObserver觸發的。observer到當前狀態的變化進行通知

觀察者,每個 Ovserver 都包含了一個回調(函數指針),當 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
};


 

RunLoopModel

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 
    ...
};

 

這裡有個概念叫 “CommonModes”:一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。RunLoop 會自動將 _commonModeItems 加入到具有 “Common” 標記的所有Mode裡。

應用場景舉例:主線程的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 並加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,並且也不會影響到滑動操作。

有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裡去。

RunLoop底層實現

從上面代碼可以看到,RunLoop的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。為了解釋這幾個概念,下面稍微介紹一下OSX/iOS的系統架構。

這裡寫圖片描述

蘋果官方將整個系統大致劃分為上述4個層次:

應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard等。

應用框架層即開發人員接觸到的 Cocoa 等框架。

核心框架層包括各種核心框架、OpenGL 等內容。

Darwin 即操作系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其所有源碼都可以在opensource.apple.com裡找到。

 

 

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 { //這是一個占位的 Mode,沒有實際作用。
            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),
        }
    }
}

這裡能看到蘋果所有的Model

事件響應

蘋果注冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細情況可以參考 這裡 。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給需要的App進程。隨後蘋果注冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

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

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。

蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回調。

當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。

界面更新

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全局的容器去。

蘋果注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,並更新 UI 界面。

NSRunLoop的應用

AFNetworking 創建一個常駐服務線程去處理數據返回
使用NSOperation+NSURLConnection並發模型都會面臨NSURLConnection下載完成前線程退出導致NSOperation對象接收不到回調的問題。
AFNetWorking解決這個問題的方法是按照官方的
guid上寫的NSURLConnection的delegate方法需要在connection發起的線程runloop中調用,於是AFNetWorking直接借鑒了Apple自己的一個IOS/samplecode/MVCNetworking/Introduction/Intro.html" target="_blank">Demo的實現方法單獨起一個global thread,內置一個runloop,所有的connection都由這個runloop發起,回調也是它接收,不占用主線程,也不耗CPU資源。

TableView中實現平滑滾動延遲加載圖片

利用CFRunLoopMode的特性,可以將圖片的加載放到NSDefaultRunLoopMode的mode裡,這樣在滾動UITrackingRunLoopMode這個mode時不會被加載而影響到。

 

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

使用RunLoop阻塞線程並且同時能夠處理其他source事件

CFRunLoopRunInMode(kCFRunLoopDefaultMode,second, NO);
CFRunLoopStop(CFRunLoopGetMain());

/// 用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);
}

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved